Showing posts with label maven. Show all posts
Showing posts with label maven. Show all posts

Sunday, September 17, 2023

GitHub Actions Java with Maven Example

What do you hate the most about pull requests? Is it the formatting changes that clearly add no value and just makes the diff confusing? Or a PR that breaks the build? Don't you just hate a PR that breaks the build? In this example we will automate the code review by checking for a broken build when a pull request is opened. How is this done? By the power of GitHub actions.

GitHub Actions

We'll utilize my old GitHub project, github.com/jpllosa/tdd-junit from Test-Driven Development with Spring Boot Starter Test. Clicking on the Actions tab, GitHub will offer GitHub actions if you haven't created any. If you already have an Action then hit the New workflow button. For our example repo, we should see somthing like below:

GitHub Actions Java with Maven Example
GitHub Actions Java with Maven

Clicking Configure will create a template maven.yml file in .github/workflows folder as shown below. Editing a yml file in GitHub is also a nice way to search for actions. Commit then pull to have a local copy. Let's create a development branch and a feature branch (github-action-java-maven). What we would like to happen is when a pull request into development, it would automatically trigger a build, test and create a review comment if it's a broken build.

GitHub Actions Java with Maven Example
Finding GitHub Actions

$ git pull
From https://github.com/jpllosa/tdd-junit
 * [new branch]      development -> origin/development
 * [new branch]      github-action-java-maven -> origin/github-action-java-maven
Already up to date.

$ git checkout github-action-java-maven

maven.yml

Let's edit the maven.yml template. If you're using Spring Tool Suite and can't find your file, you might have to untick *. resources. Then you should be able to see the yml file.

GitHub Actions Java with Maven Example
Spring Tool Suite Filter
GitHub Actions Java with Maven Example
Spring Tool Suite Java Element Filter
GitHub Actions Java with Maven Example
GitHub Workflow YML file

Your yml file should look like below:


name: Java CI with Maven

on:
  pull_request:
    branches:
      - 'development'

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3.6.0
      - name: Set up JDK
        uses: actions/setup-java@v3.12.0
        with:
          java-version: '8'
          distribution: 'semeru'
          cache: maven
      - name: Build with Maven
        run: mvn -B clean package
      - name: View context attributes
        if: ${{ failure() }}
        uses: actions/github-script@v6.4.1
        with:
          script: |
            console.log(context);
          github-token: ${{ secrets.GITHUB_TOKEN }}
          debug: true
      - name: Create PR Comment
        if: ${{ failure() }}
        uses: actions/github-script@v6.4.1
        with:
          script: |
            github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
              event: "COMMENT",
              body: "It is a mortal sin to PR a broken build! :rage:",
              comments: [],
            })
          github-token: ${{ secrets.GITHUB_TOKEN }}
          debug: true

I won't be explaining workflows in too much detail. I'll let the GitHub Docs do that for me. I will however explain this YAML file. Let's start from the top. Our workflow name is Java CI with Maven. This workflow is triggered when a pull request is opened on branch development. Our workflow run is made up of a single job, with a job ID of build. The type of machine that will process our job is the latest version of Ubuntu.

Steps, the place where all the grunt work is done. First is the Checkout. It uses the Checkout GitHub action to checkout our repository. Second it Set up JDK. It uses the Setup Java JDK GitHub action to download and set up Java version 8. Third is Build with Maven which runs the mvn -B clean package command. Fourth is the View context attributes. It uses the GitHub Script GitHub action to help us write scripts in our workflow that uses the GitHub API and the workflow run context. This step is ran when any of the previous steps has failed. The failure() is a status check function. This step is for debugging purposes only, hence the log to the console call. Fifth is the Create PR Comment action. It uses the same GitHub action as the previous one. If the Build with Maven step fails then this will create a review comment saying that it is a mortal sin to create a pull request with a broken build. LOL!

GitHub Actions in Action

Let's add a unit that fails in MathFunTest.java, push it then create a PR. Our source branch is github-action-java-maven and the target development so that the workflow gets triggered.


package com.blogspot.jpllosa;

// snipped...

@SpringBootTest
public class MathFunTest {
	// snipped...
	
	@Test
	public void testThatFails() {
		fail("Not yet implemented");
	}
}

What's the outcome? You should see something like below when creating a pull request with a broken build:

GitHub Actions Java with Maven Example
Pull Request Review Comment

Remove the failing test then push the code and you should have something like below:

GitHub Actions Java with Maven Example
GitHub Actions Success

GitHub Actions Java with Maven Summary

GitHub Actions did all the work for us. From setting up the machine with all the required Java tools. Checking out the repo and then building and running the tests. And finally creating the review comment via the GitHub Script Github action API. There you have it. An automated code review comment for those PRs breaking the build. Hope you had fun reading and applying a similar GitHub action. I know I did.

Sunday, August 13, 2023

React FE Spring Boot Example

In this article we will demonstrate a React Front-End app served through Spring Boot's embedded Tomcat. Why do it this way? We can serve a React app in different ways (e.g. Apache Web Server, Nginx, etc.). What is so special about serving a React app through Spring Boot's Tomcat? There is nothing special. A reason could be that the development team is already well versed in Spring Boot with Tomcat. They have mastered configuring Tomcat and investing in a new web server (e.g. Nginx) is of little value. Another could be the deployment pipeline is already set up to do Java/Maven deployment (e.g. Jenkins). Lastly, it could be that you are a new hire and the development team already do it that way. That is, we don't have a choice but to support an existing React FE app served through Spring Boot.

Our focus will be on building the React app so that it can be deployed through a Spring Boot fat jar. We will lightly touch on starting a Spring Boot and React apps. It is assumed that the reader has knowledge regarding Node, React, Java, Maven, Spring Boot, etc. The reader should know how to set up the mentioned technology stack.

If you have been reading my past blogs, then the previous paragraphs sound so familiar. That is because I have done a similar thing for an Angular app. Essentially this article is similar to Angular FE Spring Boot Example. But I added in an extra. In my Angular FE Spring Boot Example, we had to do two steps to build the fat jar. In this article, we will build the fat jar in one step. Sounds good? Let's do it!

Building

Same as in my Angular FE Spring Boot Example, there were two ways to build a React app so that it can be deployed as single Spring Boot fat jar. First one is to use the exec-maven-plugin. Second is to use the maven-resources-plugin.

But wait. There's more. We'll be using the maven-resource-plugin in combination with frontend-maven-plugin. With these two plugins working together, we only need to run one command to build the fat jar. This means, we have leveled up from Angular FE Spring Boot Example.

Spring Initializr

Head over to Spring Initializr and it should look like below:

React FE Sprint Boot Example
Spring Initializr

React Bit

Under the root directory, create a dev folder. This is where all the React stuff goes. Assuming you have Node, Node Version Manager and Node Package Manager ready, run npx create-react-app my-app to set up a web app. We should have something like below:


D:\workspace\react-fe-spring-boot\dev>npx create-react-app my-app
Need to install the following packages:
  create-react-app@5.0.1
Ok to proceed? (y) y

Creating a new React app in D:\workspace\react-fe-spring-boot\dev\my-app.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

Created git commit.

Success! Created my-app at D:\workspace\react-fe-spring-boot\dev\my-app
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd my-app
  npm start

Happy hacking!

We follow the bottom two commands (i.e. cd my-app, npm start) and we should have something like below. For more details, head over to Create React App. Thank you Create React App for creating our project. The React app should be available at localhost:3000.

React FE Sprint Boot Example
React app on port 3000

Build the React project, npm run build. This will create files in the build directory. Take note of this folder as we will need it in our POM file. We should see something like below after the build:


D:\workspace\react-fe-spring-boot\dev>npm run build

> my-app@0.1.0 build
> react-scripts build

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  46.61 kB  build\static\js\main.46f5c8f5.js
  1.78 kB   build\static\js\787.28cb0dcd.chunk.js
  541 B     build\static\css\main.073c9b0a.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment

Maven Bit

We don't need to change any Java code. The key is in the POM. We will copy the build files over to the classes/static folder and then Maven will take care of making the fat jar. Add the maven-resources-plugin like below (make sure you get the diretories right ;) ):


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>validate</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals> 
			<configuration>
				<outputDirectory>${build.directory}/classes/static/</outputDirectory>
				<resources>
					<resource>
						<directory>dev/my-app/build</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>

Running the Fat Jar

Do a mvn clean package. Now, run java -jar ./target/react-fe-spring-boot-0.0.1-SNAPSHOT.jar and we should be able to see the it running on localhost:8080. It's now being served by Tomcat.

React FE Sprint Boot Example
React app on port 8080

Wait? What?! That was 2 steps to build it into a fat jar! What gives?

One Command to Build Them All

To build it all in one command, we'll enlist the help of the frontend-maven-plugin and a minor change on maven-resource-plugin. Before we do that, let's review the goals bound to the package phase, run mvn help:describe -Dcmd=package.


D:\workspace\react-fe-spring-boot>mvn help:describe -Dcmd=package

[INFO] 'package' is a phase corresponding to this plugin:
org.apache.maven.plugins:maven-jar-plugin:2.4:jar

It is a part of the lifecycle for the POM packaging 'jar'. This lifecycle includes the following phases:
* validate: Not defined
* initialize: Not defined
* generate-sources: Not defined
* process-sources: Not defined
* generate-resources: Not defined
* process-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:resources
* compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
* process-classes: Not defined
* generate-test-sources: Not defined
* process-test-sources: Not defined
* generate-test-resources: Not defined
* process-test-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
* test-compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
* process-test-classes: Not defined
* test: org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
* prepare-package: Not defined
* package: org.apache.maven.plugins:maven-jar-plugin:2.4:jar
* pre-integration-test: Not defined
* integration-test: Not defined
* post-integration-test: Not defined
* verify: Not defined
* install: org.apache.maven.plugins:maven-install-plugin:2.4:install
* deploy: org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.947 s
[INFO] Finished at: 2023-08-06T23:04:08+01:00
[INFO] ------------------------------------------------------------------------

First change is to make maven-resources-plugin execute on the generate-sources phase. Change validate like so.


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<!-- <phase>validate</phase> -->
			<phase>generate-sources</phase>
			...snipped...

Next is add the frontend-maven-plugin like so.


<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
	<version>1.11.3</version>
	<configuration>
		<workingDirectory>dev/my-app</workingDirectory>
		<nodeVersion>v18.10.0</nodeVersion>
		<yarnVersion>v1.22.17</yarnVersion>
	</configuration>
	<executions>
		<execution>
			<id>install-frontend-tools</id>
			<goals>
				<goal>install-node-and-yarn</goal>
			</goals>
			<phase>validate</phase>
		</execution>
		<execution>
			<id>build-frontend</id>
			<goals>
				<goal>yarn</goal>
			</goals>
			<phase>initialize</phase>
			<configuration>
				<arguments>build</arguments>
			</configuration>
		</execution>
	</executions>
</plugin>

Now, do you know why I had to show you the Maven package phases? During the package phase, the first thing that will be done by the frontend plugin will be to install node and yarn (i.e. validate phase). After that is building the React app, yarn build (i.e. initialize phase). And lastly, resources will copy the files over (i.e. generate-sources phase). Viola! We're ready to run the build again. Remove the built files (e.g. mvn clean or delete target directory, delete the React app build directory) and then do a mvn clean package. You should see something like below. After the build you should be able to run the jar like before.


D:\workspace\react-fe-spring-boot>mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.blogspot.jpllosa:react-fe-spring-boot >--------------
[INFO] Building react-fe-spring-boot 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.2.0:clean (default-clean) @ react-fe-spring-boot ---
[INFO]
[INFO] --- frontend-maven-plugin:1.11.3:install-node-and-yarn (install-frontend-tools) @ react-fe-spring-boot ---
[INFO] Installing node version v18.10.0
[INFO] Copying node binary from C:\Users\jpllosa\.m2\repository\com\github\eirslett\node\18.10.0\node-18.10.0-win-x64.exe to D:\workspace\react-fe-spring-boot\dev\my-app\node\node.exe
[INFO] Installed node locally.
[INFO] Installing Yarn version v1.22.17
[INFO] Unpacking C:\Users\jpllosa\.m2\repository\com\github\eirslett\yarn\1.22.17\yarn-1.22.17.tar.gz into D:\workspace\react-fe-spring-boot\dev\my-app\node\yarn
[INFO] Installed Yarn locally.
[INFO]
[INFO] --- frontend-maven-plugin:1.11.3:yarn (build-frontend) @ react-fe-spring-boot ---
[INFO] Running 'yarn build' in D:\workspace\react-fe-spring-boot\dev\my-app
[INFO] yarn run v1.22.17
[INFO] $ react-scripts build
[INFO] Creating an optimized production build...
[INFO] Compiled successfully.
[INFO]
[INFO] File sizes after gzip:
[INFO]
[INFO]   46.61 kB  build\static\js\main.46f5c8f5.js
[INFO]   1.78 kB   build\static\js\787.28cb0dcd.chunk.js
[INFO]   541 B     build\static\css\main.073c9b0a.css
[INFO]
[INFO] The project was built assuming it is hosted at /.
[INFO] You can control this with the homepage field in your package.json.
[INFO]
[INFO] The build folder is ready to be deployed.
[INFO] You may serve it with a static server:
[INFO]
[INFO]   npm install -g serve
[INFO]   serve -s build
[INFO]
[INFO] Find out more about deployment here:
[INFO]
[INFO]   https://cra.link/deployment
[INFO]
[INFO] Done in 8.49s.
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:copy-resources (copy-resources) @ react-fe-spring-boot ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 15 resources
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ react-fe-spring-boot ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource

There you have it. Serving a Rect app through Spring Boot's fat jar and embedded Tomcat. Plus building it in one command! Grab the full repo here, github.com/jpllosa/react-fe-spring-boot

Friday, July 28, 2023

Spring ResourceTransformer Example

Here's an example of how to handle static web resources in Spring. Let us pretend that one of the business requirement of our Angular FE Spring Boot app is A/B testing. A/B testing is a method of comparing two versions of an app or webpage against each other to determine which one performs better. In short, we'll need a control webpage and a variant webpage. For the control webpage, there will be no changes. For the variant webpage, we'll need to inject "ab-testing.js" in the head of our webpage.

We will not be talking about how to do A/B testing. Instead, we will focus on how to inject the script tag in the head of our webpage. Requirements clear enough? Let's begin. Below is our control webpage.

Spring ResourceTransformer Example
Control Page

We will utilize the code from my previous blog, Angular FE Spring Boot Example and you can clone the code here, github.com/jpllosa/angular-fe-spring-boot.

Two Strategies

As far as I know, there are two ways to fulfill this business requirement.

First one is to insert the script tag when ngOnInit is called in a Component class.

Second is to use Spring's ResourceTransformer. We will use this method because the script tag is injected server side. Prior to serving the HTML file, it is transformed. The first strategy happens on the client side which is why I think this is a better solution. This also seems better because the "ab-testing.js" is already in the head prior to DOM manipulation by Angular. So let's get to it.

POM

Update the POM file. We need the dependency below for IOUtils.


<dependency>
	<groupId>commons-io</groupId>
	<artifactId>commons-io</artifactId>
	<version>2.11.0</version>
</dependency>

index.html

Add a marker text in dev/my-app/src/index.html, like so:


<head>
  ... snipped ...
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- INJECT_HERE -->
</head>

Java Code

Create a com.blogspot.jpllosa.transformer package and add the following files below:


package com.blogspot.jpllosa.transformer;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;


@Configuration
public class IndexTransformationConfigurer extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("index.html")
			.addResourceLocations("classpath:/static/")
			.resourceChain(false)
			.addTransformer(new IndexTransformer());
	}
}

We create the class above so that we can add a handler when serving the index.html file. Here it specifies that the index.html file from the classpath:/static/ location will have to pass through a Transformer. Other static resources will not be affected. We are not caching the result of the resource resolution (i.e. resourceChain(false)).


package com.blogspot.jpllosa.transformer;

import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceTransformerChain;
import org.springframework.web.servlet.resource.TransformedResource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Service
public class IndexTransformer implements ResourceTransformer {
	@Override
	public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain chain) throws IOException {
		String html = IOUtils.toString(resource.getInputStream(), "UTF-8");
		html = html.replace("<!-- INJECT_HERE -->", 
			"<script src=\"//third.party.com/ab-testing.js\"> </script>");
		return new TransformedResource(resource, html.getBytes());
	}
}

We've kept our transformer simple for this example. The code is readable enough as to what it is doing, isn't it? We are replacing the placeholder text with the script tag. Job done.

Demonstration

If you have read the Angular FE Spring Boot Example then you should know what to do next. In case you have not. I'll reiterate.

Build the Angular project, ng build on dev/my-app. This will create files in the dist directory.

Run mvn clean package on the project root directory. After that, run java -jar ./target/angular-fe-spring-boot-0.0.1-SNAPSHOT.jar

and we should be able to see something like below running on localhost:8080. Is "ab-testing.js" there?
Spring ResourceTransformer Example
Variant Page

There you have it. A quick example of how to handle or transform static web resources in Spring. Grab the full repo here, github.com/jpllosa/angular-fe-spring-boot

Monday, June 26, 2023

Angular FE Spring Boot Example

In this article we will demonstrate an Angular Front-End app served through Spring Boot's embedded Tomcat. Why do it this way? We can serve an Angular app in different ways (e.g. Apache Web Server, Nginx, etc.). What is so special about serving an Angular app through Spring Boot's Tomcat? There is nothing special. A reason could be that the development team is already well versed in Spring Boot with Tomcat. They have mastered configuring Tomcat and investing in a new web server (e.g. Nginx) is of little value. Another could be the deployment pipeline is already set up to do Java/Maven deployment (e.g. Jenkins). Lastly, it could be that you are a new hire and the development team already do it that way. That is, we don't have a choice but to support an existing Angular FE app served through Spring Boot.

Our focus will be on building the Angular app so that it can be deployed through a Spring Boot fat jar. We will lightly touch on starting a Spring Boot and Angular apps. It is assumed that the reader has knowledge regarding Node, Angular CLI, Java, Maven, Spring Boot, etc. The reader should know how to set up the mentioned technology stack.

Two Ways to Build

As far as I know, there are two ways to build an Angular app so that it can be deployed as single Spring Boot fat jar.

First one is to use the exec-maven-plugin. This plugin runs the ng build and the outputPath of the Angular app must point to src/main/resources/static so that Angular dist files are bundled in the jar during packaging.

Second is to use the maven-resources-plugin. This plugin copies the Angular dist files to the classes/static directory of the Spring Boot app. This is the method we will be demonstrating. This way seems to be cleaner because it doesn't populate any files under the Java section of the code. The Angular build code goes straight into the fat jar. This way, all Angular development code and work remain in a separate directory from the Java side of things. And only become one when we build the fat jar. So let's get to it.

Spring Initializr

Head over to Spring Initializr and it should look like below:

Spring Initialzr

Angular Bit

Under the root directory, create a dev folder. This is where all the Angular stuff goes. Assuming you have Node, Node Version Manager and Node Package Manager ready, run npm install -g @angular/cli to install the Angular CLI. Check your installation, ng -v. We should have something like below:

Angular Version

Create an Angualr starter app under the dev folder, ng new my-app. This will create the scaffolding. Change directory to my-app, cd my-app, then run g serve. Open localhost:4200, we should have something like below:

My-App on 4200

Build the Angular project, ng build. This will create files in the dist directory. Take note of this folder as we will need it in our POM file. We should see something like below after the build:

Built Files

These are the built files found in the dist folder.

dist Contents

Maven Bit

We don't need to change any Java code. The key is in the POM. We will copy the dist files over to the classes/static folder and then Maven (mvn clean package) will take care of making the fat jar. Add the maven-resources-plugin like below (make sure you get the diretories right ;) ):


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>validate</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals> 
			<configuration>
				<outputDirectory>${build.directory}/classes/static/</outputDirectory>
				<resources>
					<resource>
						<directory>dev/my-app/dist/my-app</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>

Below is the key bit when running mvn clean package. This tells us the files copied from Angular to Java classes. The number of files should match!

mvn clean package

Demonstration

Now, run java -jar ./target/angular-fe-spring-boot-0.0.1-SNAPSHOT.jar

and we should be able to see the it running on localhost:8080. Did you notice that it's on a new port this time? It's now being served by Tomcat.
My-App Served Through Spring Boot's Tomcat

There you have it. A nice way of serving an Angular app through Spring Boot's fat jar and embedded Tomcat. Grab the full repo here, github.com/jpllosa/angular-fe-spring-boot

Monday, May 8, 2023

Spring Boot WebSocket Example

In this article we will demonstrate the benefit of using Spring Boot and WebSocket to create an interactive web application. WebSocket is layer above TCP (Transmission Control Protocol). We will do STOMP (Streaming Text Oriented Messaging Protocol) messaging between client and server. STOMP is the protocol operating on top of WebSocket.

In my previous article, Spring Boot JMS Example, we tried to solve the UI "freezing" problem by incorporating JMS into our web application. Incorporating WebSocket is another way to solve the UI "freezing" problem. As mentioned before, we would like to avoid "blocking" the user interface or experience. If we know a user request will take a long time then we'll need a mechanism to allow the UI to continue working and not appear to "freeze". Similar to what SwingUtilities.invokeLater does. An interactive web application is a common use of WebSocket.

We will build upon the code from Spring Boot JMS Example. We will not dive into much detail on how to build the project as our focus will be on the practical application of WebSocket in our app. If you want to create the project yourself, please look at my other blogs (e.g. Spring Boot MockMvc Example) where I used Spring Initializr to create one.

That said, you can clone the finished project here, github.com/jpllosa/jms-async. We'll just go straight into explaining the code.

Dependencies

The following are the dependencies added to the POM. We won't be using any Bootstrap but just in case you want to make it look pretty, it is ready for you.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>sockjs-client</artifactId>
	<version>1.0.2</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>stomp-websocket</artifactId>
	<version>2.3.3</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.3.7</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>3.1.1-1</version>
</dependency>

WebSocket Configuration


package com.blogspot.jpllosa.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		config.enableSimpleBroker("/topic");
		config.setApplicationDestinationPrefixes("/app");
	}
	
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/fibonacci-websocket").withSockJS();
	}
}

The above code enables WebSocket and STOMP messaging in Spring. This is a Spring configuration class as the descriptive annotation says @Configuration. This class also enables WebSocket message handling, backed by a message broker (@EnableWebSocketMessageBroker).

In WebSocketConfig, we override two default methods. First, the configureMessageBroker() method which implements the default method in WebSocketMessageBrokerConfigurer to configure the message broker. The enableSimpleBroker() enables a simple memory-based message broker to carry the messages back to the client on destinations prefixed with /topic. It also designates the /app prefix for messages that are bound for methods annotated with @MessageMapping. This prefix will be used to define all the message mappings. For example, /app/fibonacci-id is the endpoint that the FibonacciController.fibonacciCompute() method is mapped to handle.

The registerStompEndpoints() method registers the /fibonacci-websocket endpoint, enabling SockJS fallback options so that alternate transports can be used if WebSocket is not available. The SockJS client will attempt to connect to /fibonacci-websocket and use the best available transport (websocket, xhr-streaming, xhr-polling, etc.).

Message-handling Controller


package com.blogspot.jpllosa.controller;

// ...imports snipped...

@Controller
public class FibonacciController {

	@Autowired
	JmsTemplate jmsTemplate;
	
	@Autowired
	HashMap myMap;
	
	private Fibonacci fib = new Fibonacci();
	
	// ...code snipped...
	
	@GetMapping("/stomp-fib")
	public String stompFib(@RequestParam(name="numbers", required=true) String numbers, Model model) {
		final String PENDING = "pending";
		
		UUID uuid = UUID.randomUUID();
		
		FibonacciMessage fibMsg = new FibonacciMessage(uuid.toString(),
				Integer.parseInt(numbers),
				PENDING);
		
		myMap.put(fibMsg.getId(), fibMsg);

		model.addAttribute("numbers", fibMsg.getNumbers());
		model.addAttribute("result", fibMsg.getResult());
		model.addAttribute("id", fibMsg.getId());
		
		return "stomp-fib";
	}
	
	@MessageMapping("/fibonacci-id")
	@SendTo("/topic/fibonacci-result")
	public FibonacciMessage fibonacciCompute(StompMessage message) {
		FibonacciMessage fibMsg = myMap.get(message.getId());
		
		if (fibMsg.getResult().equals("pending")) {
			String result = fib.fibonacci(fibMsg.getNumbers());
			fibMsg.setResult(result);
			System.out.println("stomp websocket: " + fibMsg);
			
			myMap.put(fibMsg.getId(), fibMsg);
		}
		
		return fibMsg;	
	}
}

In Spring, STOMP messages can be routed to @Controller classes. Tha above code is added into our controller. The fibonacciCompute() method is called when a message is sent to the fibonacci-id destination. The payload is automatically bound to StompMessage. The FibonacciMessage is then broadcasted to all subscribers of /topic/fibonacci-result when the method is finished.

This methods just checks the map if it contains the ID of the fibonacci compute request. If the compute request is still pending, it then computes the fibonacci sequence then places it back into the map. Otherwise, it returns with a result. It will print the result on the console as well (e.g. stomp websocket: FibonacciMessage [id=0170491e-b44b-4831-9e2f-29422fc60482, numbers=5, result=1, 1, 2, 3, 5]) when the fibonacci sequence has been computed.

The stompFib simply serves the stomp-fib.html web content.

Message Model


package com.blogspot.jpllosa.websocket;

public class StompMessage {
	String id;
	
	public StompMessage() {
	}
	
	public StompMessage(String id) {
		this.id = id;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}
}

As explained earlier, the STOMP payload is automatically bound to StompMessage. Spring handles the JSON marshalling automatically, we just need to provide the model. This models the message that carries the ID.

JavaScript Client


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head> 
    <title>Fibonacci Result via WebSocket</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
	<h1>Fibonacci Result via WebSocket</h1>
	<p th:text="'Numbers: ' + ${numbers}" />
    <p id="result" th:text="'Result: ' + ${result}" />
    <p>The result will update in a few seconds (<span id="sec"></span>).</p>
</body>

<script th:inline="javascript">
$(document).ready(function() {
	var socket = new SockJS("/fibonacci-websocket");
	var stompClient = Stomp.over(socket);
	var $result = $("#result");
	var $sec = $("#sec");
	var timer;
	var counter = 1;
	
	stompClient.connect({}, function(frame) {
		console.log("connected");
		stompClient.subscribe("/topic/fibonacci-result", function(message) {
			var fibMsg = JSON.parse(message.body);
			$result.text("Result: " + fibMsg.result);
			clearInterval(timer);
			
			setTimeout(function() {
				if (stompClient !== null) {
			        stompClient.disconnect();
			        console.log("disconnected");
			    }
			}, 5000);
		});
		
		setTimeout(function() {
			stompClient.send("/app/fibonacci-id", {}, JSON.stringify({
				id: [[${id}]],
			}));
			
			timer = setInterval(function() {
				$sec.text(counter);
				counter++;
			}, 1000);
		}, 1000);
	});
});
</script>
</html>

Our stomp-fib.html has a lot more code compared to the HTMLs in Spring Boot JMS Example. Here we import the sockjs.min.js and stomp.min.js libraries that will be used to communicated with our server through STOMP over WebSocket.

Here is what the JavaScript code section does. When the HTML document is fully loaded it instantiates a SockJS object which opens a connection to /fibonacci-websocket then use STOMP over it. We then subscribe for messages to the /topic/fibonacci-result destination. Whenever a message is received, it will update the DOM and clear the timer, then disconnect. Upon connection, we wait for a second then send a STOMP message to trigger the fibonacci computation and then interactively update the DOM showing the elapsed time in seconds.

Demonstration

To demonstrate, open the /stomp-fib endpoint (e.g. http://localhost:8080/stomp-fib?numbers=5). Don't forget to run the Spring Boot App! You should have something like below:

There you have it. Another way of solving the UI "freezing" problem with Spring Boot WebSocket.

Tuesday, April 18, 2023

Spring Boot JMS Example

In this article we will demonstrate the benefit of using Spring Boot Java Message Service (JMS) for asynchronous processing. JMS is used as a way to allow asynchronous request processing in our web application. One reason you would to like do this is to avoid "blocking" the user interface or experience. If you know a user request will take a long time then you'll need a mechanism to allow the UI to continue working and not appear to "freeze". Similar to what SwingUtilities.invokeLater does. There are also other uses for JMS but generally and most common is queueing requests that take too long to process.

We will not dive into much detail on how to build the project as our focus will be on the practical application of JMS in our app. If you want to create the project yourself, please look at my other blogs (e.g. Spring Boot MockMvc Example) where I used Spring Initializr to start one. Here's what your Spring Initializr should look like:

That said, you can clone the finished project here, github.com/jpllosa/jms-async. We'll just go straight into explaining the code.

JMS Configuration


package com.blogspot.jpllosa;

import java.util.HashMap;

import javax.jms.ConnectionFactory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;

import com.blogspot.jpllosa.messaging.FibonacciMessage;

@SpringBootApplication
@EnableJms
public class JmsAsynchronousProcessingApplication {

	public static void main(String[] args) {
		SpringApplication.run(JmsAsynchronousProcessingApplication.class, args);
	}

	@Bean
	public JmsListenerContainerFactory myFactory(ConnectionFactory connectionFactory,
			DefaultJmsListenerContainerFactoryConfigurer configurer) {
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();

		configurer.configure(factory, connectionFactory);

		return factory;
	}

	@Bean
	public MessageConverter jacksonJmsMessageConverter() {
		MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
		converter.setTargetType(MessageType.TEXT);
		converter.setTypeIdPropertyName("_type");
		return converter;
	}
	
	@Bean
	public HashMap myMap() {
		return new HashMap();
	}
}

In the above code, we have configured JMS with Spring to send and receive messages. The @SpringBootApplication is a convenience annotation that does the following:

  • Tags the class a source of beand definitions akin to @Configuration.
  • Tells Spring Boot to add beans based on the classpath settings, other beans and property settings.
  • Tells Spring Boot to find other components, configurations and services in the package akin to @ComponentScan.
The @EnableJms triggers the discovery of methods annotated with @JmsListener and then Spring Boot creates the message listener for this. The myFactory is referenced in the JmsListener of the receiver, FibonacciCompute.receiveFibonacciMessage(). We use Jackson to serialize the content in text format. Spring Boot detects the MessageConverter and associates it to both the default JmsTemplate and any JmsListenerContainerFactory.

Last but not the least is the so called "database". Could have created a static map here but opted instead to make it a bean so I can just auto wire it.

Message Reveiver


package com.blogspot.jpllosa.messaging;

import java.util.HashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import com.blogspot.jpllosa.service.Fibonacci;

@Component
public class FibonacciCompute {
	
	@Autowired
	HashMap myMap;
	
	private Fibonacci fib = new Fibonacci();
	
	@JmsListener(destination = "fibonacciCompute", containerFactory = "myFactory")
	public void receiveFibonacciMessage(FibonacciMessage fibMsg) {
		
		String result = fib.fibonacci(fibMsg.getNumbers());
		fibMsg.setResult(result);
		System.out.println(fibMsg);
		
		myMap.put(fibMsg.getId(), fibMsg);
	}

}

The JmsListener annotation defines the name of the destination that this method should listen to and the reference to the JmsListenerContainerFactory to use. When a message is received, it computes the fibonacci sequence. Once it has a result, it is stored in the map "database". This map will then be accessed later as you will see. It will print the result on the console as well (e.g. FibonacciMessage [id=f2f175e3-05d9-4ea2-89ae-02777242c7de, numbers=8, result=1, 1, 2, 3, 5, 8, 13, 21]) when the fibonacci sequence has been computed.

Controller and Service


package com.blogspot.jpllosa.controller;

import java.util.HashMap;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import com.blogspot.jpllosa.messaging.FibonacciMessage;
import com.blogspot.jpllosa.service.Fibonacci;

@Controller
public class FibonacciController {
	
	@Autowired
	JmsTemplate jmsTemplate;
	
	@Autowired
	HashMap myMap;

	@GetMapping("/fib")
	public String fib(@RequestParam(name="numbers", required=true) String numbers, Model model) {
		Fibonacci fib = new Fibonacci();
		model.addAttribute("numbers", numbers);
		model.addAttribute("result", fib.fibonacci(Integer.parseInt(numbers)));
		
		return "fib";
	}
	
	@GetMapping("/jms-fib")
	public String JmsFib(@RequestParam(name="numbers", required=true) String numbers, Model model) {
		final String PENDING = "pending";
		
		UUID uuid = UUID.randomUUID();
		
		FibonacciMessage fibMsg = new FibonacciMessage(uuid.toString(),
				Integer.parseInt(numbers),
				PENDING);
		
		jmsTemplate.convertAndSend("fibonacciCompute", fibMsg);

		model.addAttribute("numbers", fibMsg.getNumbers());
		model.addAttribute("result", fibMsg.getResult());
		model.addAttribute("id", fibMsg.getId());
		
		return "jms-fib";
	}
	
	@GetMapping("/fib-updates/{id}")
	public String FibUpdates(@PathVariable String id, Model model) {
		
		FibonacciMessage fibMsg = myMap.get(id);

		if (fibMsg != null) {
			model.addAttribute("numbers", fibMsg.getNumbers());
			model.addAttribute("result", fibMsg.getResult());
		} else {
			model.addAttribute("numbers", "");
			model.addAttribute("result", "");
		}
		
		return "fib-updates";
	}
}

package com.blogspot.jpllosa.service;

public class Fibonacci {

	public String fibonacci(int numbers) {
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			// don't care
		}
		
		if (numbers < 0) return "";
		if (numbers == 0) return "0";
		if (numbers == 1) return "1";
		
		StringBuilder sb = new StringBuilder();
		int num1 = 0;
		int num2 = 1;
		sb.append(num2);
		for (int i = 1; i < numbers; i++) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			
			int sumOfPreviousTwo = num1 + num2;
			sb.append(sumOfPreviousTwo);
			num1 = num2;
			num2 = sumOfPreviousTwo;
		}
		
		return sb.toString().trim();
	}
	
	private int fibonacciCompute(int numbers) {
		if (numbers == 1 || numbers == 2) return 1;

		return fibonacciCompute(numbers - 2) + fibonacciCompute(numbers - 1);
	}
	
	public String fibonacciRecursion(int numbers) {
		if (numbers == 0) return "0";
		
		StringBuilder sb = new StringBuilder();
		for (int i = 1; i <= numbers; i ++) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			
			sb.append(fibonacciCompute(i));
		}
		
		return sb.toString().trim();
	}
	
}

I'll explain the code along with the demonstration below.

Demonstration

First off, let's see what happens when we don't use JMS and a long process is going to take place. To demonstrate this, hit the /fib endpoint (e.g. http://localhost:8080/fib?numbers=5). I've intentionally added the 5 second delay to simulate a long process. As you will notice, the user inteface took a long while to render the page. A user might assume that the web page has frozen. We don't want that. Opening web tools + Network tab, we see the turle icon when we did the request. The turtle means it had a slow server response time (x secs). The recommended limit is 500ms.

Now let's see what happens when we use JMS and a long process is going to take place. To demonstrate this, hit the /jms-fib endpoint (e.g. http://localhost:8080/jms-fib?numbers=8). As you might have noticed, there is a little difference in the implementation. We had to create a unique ID associating the request. We also returned a "pending" message right away along with a link the user can click for updates of the fibonacci computation request. When we received the request it was simple call to convertAndSend. With the destination specified and plain old Java object, of course. In contrast to the previous /fib call, we got a web page straight away and it took like 5ms.

For the results, we click the link below (e.g. http://localhost:8080/fib-updates/) and if it is still pending we can reload the page. Then we see something like below:

Now you've seen JMS in action. We have demonstrated the benefit of Spring Boot Java Message Service for asynchronous processing. There you have it. A simple example of how to use JMS to allow asynchronous request processing in our web application and not "freezing" the UI.

Wednesday, March 1, 2023

Spring Session with Redis Example

In this article, we will talk about Spring session with Redis. We will demonstrate the difference between maintaining a session with HttpSession versus a session with Redis. Why would we want our session to be backed with Redis? Read on and we'll find out why. By the way, Spring session can also persist data using JDBC, Gemfire, or MongoDB.

Overview

Offloading session management from the servlet container (e.g. Tomcat's HTTP session) because of its limitations and eating up of server memory are some reasons we want to persist session data somewhere else. The most likely reason you'ld want to use Spring session with Redis is when you are scaling up. For example, you have multiple instances of a microservice running behind a load balancer and you'ld want to keep track of session state. Why have multiple instances? Because you want zero downtime of your service. With Redis, our session data can be shared between multiple microservices. By persisting sessions, multiple application instances can serve the same session and individual instances can crash without impacting other application instances.

Redis

I will not be providing instructions on how to set up Redis on your local machine. For that, I'll point you to Redis and make your way to the Get Started section. I'm using Windows 10 so I followed the Install on Windows instructions. I also had to set up my Windows Subsystem for Linux and got Ubuntu running on my local machine. In the end and by default, your Redis server should be running on port 6379 and you should be able to ping it. You should have something like below:

Spring Session with Redis
Redis Running

Spring Session with Redis Project

I will not provide instructions on how to create this project as well. My focus will be demostrating HttpSession versus Spring session with Redis. Anyway, if you want to create the project, you can follow the steps from my previous blogs (e.g. Spring Boot MockMvc Example). Here is what your Spring Initialzr would look like:

Spring Session with Redis
Spring Initializr

Here is the finished project, clone it from github.com/jpllosa/spring-session-redis.

@RestController


package com.blogspot.jpllosa.controller;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpSession;

import com.blogspot.jpllosa.model.UserData;

@RestController
public class HttpSessionController {

	@PostMapping("/save-to-http-session")
	public HttpEntity saveToHttpSession(@RequestBody UserData userData, HttpSession httpSession) {
		httpSession.setAttribute("userData", userData);
		return new ResponseEntity<>("", HttpStatus.ACCEPTED);
	}
	
	@GetMapping("/get-from-http-session")
	public HttpEntity getFromHttpSession(HttpSession httpSession) {
		return new ResponseEntity<>((UserData) httpSession.getAttribute("userData"), HttpStatus.OK);
	}
}

This is where all the action happens. Our microservice is pretty simple really. It accepts user data via an HTTP POST then we can send it back via an HTTP GET if it was saved. The response to a successful POST is a 202, meaning it was accepted.

Background

In our demonstration, we will run two instances of our microservice. MS 80, microservice on port 8080 and MS 81, microservice on port 8081. We'll pretend both are behind a load balancer. A network load balancer will decide where in coming requests go to which microservice. But we won't actually have a load balancer.

Tomcat HTTP Session

We'll need to comment out a few bits in the configuration. And just to be sure, we will stop the Redis server. To stop the server execute sudo service redis-server stop then you should see this message "Stopping redis-server: redis-server.". Comment out spring.session.store-type=redis in your application.properties and comment out the two Redis dependencies in you POM, like so:


<!--
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>
-->

Open two command prompts/terminals and run MS 80 on one and MS 81 on the other. So that's mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8080 for MS 80 and mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081 for MS 81. You should see something like 2023-02-23 16:27:13.547 INFO 25148 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http). Of course 8080 will show for MS 80. Now, let's POST some user data. Let's send a POST via Postman to MS 80 like so:

Spring Session with Redis
HTTP POST to MS 80

Now, let's check if our user data has persisted on both microservices. Let's do a GET on MS 80 and 81, like so:

Spring Session with Redis
HTTP GET from MS 80
Spring Session with Redis
HTTP GET from MS 81

As you can see, only MS 80 returned the user data. Now, do you see the problem if we had to maintain session state between multiple instances? Only one MS maintained the state. Imagine if we were going to shutdown or restart MS 80 to update it or something, the session data will be lost. What if the session data was banking transaction? Oh dear!

Spring Session with Redis

Now the fun part. Start the Redis server, sudo service redis-server start. Then undo the comments you made on your POM and application.properties. This should make your app Redis ready. With all that done, restart MS 80 and MS 81. So that's Ctrl+C then execute the maven command again as mentioned above. Let's do the POST again on MS 80 and do a GET on MS 80 and 81. Now the GET response will show the user data like so:

Spring Session with Redis
HTTP POST to MS 80 with Redis
Spring Session with Redis
HTTP GET from MS 80 with Redis
Spring Session with Redis
HTTP GET from MS 81 with Redis

There you have it. MS 80 and MS 81 are sending back the user data information. The magic that caused it all is spring.session.store-type=redis in your application.properties. Thanks to Spring Boot, a lot of steps have been done for us with just a single line in the application properties file. What did Spring Boot do for us? It applied a configuration that is equivalent to adding an @EnableRedisHttpSession to a class. This created a Spring bean with the name of springSessionRepositoryFilter that implements Filter. The filter is in charge of replacing the HttpSession implementation to be backed by Spring Session. Then Spring Boot automatically created a RedisConnectionFactory that connects Spring Session to a Redis Server on localhost on port 6379 (default port).

In a production environment, I would recommend to add more configuration and/or create a configuration class annotated with @EnableRedisHttpSession for more fine grained control over a session. Here are some of the configurations you can add:

  1. server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
  2. spring.session.redis.flush-mode=on_save # Sessions flush mode.
  3. spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
  4. spring.redis.host=localhost # Redis server host.
  5. spring.redis.password= # Login password of the redis server.
  6. spring.redis.port=6379 # Redis server port.

There you have it. Hope you had much fun playing with Spring Session with Redis, I know I did.

Friday, February 10, 2023

Spring Boot Mockito Example

In this article, we will make a shallow dive into mocking using Mockito. We'll be utilising the code from Spring Boot MockMvc Example. We'll be adding a unit test that makes use of Mockito.

We will not dive into much detail on how to build the project as our focus will be on mocking with Mockito. Why do we need to mock? Mocks are useful if you have a dependency to an external system. Such as a database connection, a third party web API, etc. In order for our unit tests to be quick and simple, we need to mock (fake) the database connection, a file read or whatever that may be.

Please refer to the source blog if you want to create the project yourself. Here is the finished project, clone it here, github.com/jpllosa/tdd-mockmvc. We'll just go straight into creating the additional test with mocks.

Mockito

For this example, we are going to pretend that the service layer is a call to a third party web service. Imagine if the third party web service is down, how can you test your code? To be able to run the unit test, we'll need to mock it. Read through the code below. Pay particular attention to getGcfTestWithMockedService(). It will all be explained below.


package com.blogspot.jpllosa.controller;

import static org.hamcrest.CoreMatchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import com.blogspot.jpllosa.model.GreatestCommonFactor;
import com.blogspot.jpllosa.service.MathFunService;

@SpringBootTest
@AutoConfigureMockMvc
public class MathFunControllerTest {
	
	@Autowired
	private MockMvc mockMvc;
	
	@InjectMocks
	private MathFunController controller;
	
	@Mock
	private MathFunService service;
	
	@Test
	public void getGcfTest() throws Exception {
		this.mockMvc.perform(get("/get-gcf?firstNumber=12&secondNumber=16"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(jsonPath("$.firstNumber", is(12)))
			.andExpect(jsonPath("$.secondNumber", is(16)))
			.andExpect(jsonPath("$.gcf", is(4)));
	}

	@Test
	public void getGcfTestWithMockedService() throws Exception {
		GreatestCommonFactor gcf = new GreatestCommonFactor();
		gcf.setFirstNumber(12);
		gcf.setSecondNumber(16);
		gcf.setGcf(4);
		
		when(service.findGcf(12, 16)).thenReturn(gcf);
		GreatestCommonFactor actual = controller.getGcf(12, 16);
		
		verify(service).findGcf(anyInt(), anyInt());
		
		assertEquals(4, actual.getGcf());
	}
}

First off, we mark our controllor @InjectMocks. This means mock injection is performed here. Then we mark our service @Mock. This is our mocked object which will be injected to the controller. Now that it's all set up, we go to getGcfTestWithMockedService(). The three things we usually do when mocking an object is: do something when it's called (when), return something based on the call (thenReturn), and verify if the method was actually invoked by our code (verify). Before the controller gets hit, we prepare the service on what to do when findGcf is invoked with 12 and 16 as parameters. When it is invoked, we return a greatest common factor object that we also prepared beforehand. After the controller gets hit, we verify that findGcf was actually invoked. Here, we are happy that any integer was passed. We only care that it was invoked, the third party system does the actual finding of the greatest common factor. We should have two passing tests like below:

Spring Boot Mockito Example
Spring Boot Mockito Example

There you have it. A simple example of how to use Mockito.