Showing posts with label spring boot. Show all posts
Showing posts with label spring boot. Show all posts

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.

Wednesday, January 18, 2023

Spring Boot MockMvc Example

This article is an example of Spring Boot's MockMvc. We'll be utilising the code from Test-Driven Development with Spring Boot Starter Test. We'll move our MathFun code to the service layer and create a REST controller that will call it.

We will not dive into much detail on how to build the project as our focus will be on testing the web layer with MockMvc. The beauty of MockMvc is it doesn't start an HTTP server. It handles the incoming HTTP request and hands it off to our controller. Our code is called exactly the same way, just like a real HTTP request. With the power of annotations, Spring's injects MockMvc.

Create the Project

Create your project, head to Spring Initializr. We'll be using Maven, Java 8, Spring Boot 2.7.7, and with developer tools and web (see image) as dependencies. This is what I've got:

Project Metadata

  • Group: com.blogspot.jpllosa
  • Artifact: tdd-mockmvc
  • Name: TDD Web layer
  • Description: Test-Driven Development with MockMvc
  • Package name: com.blogspot.jpllosa

When you are ready, import the project to your IDE. I use Spring Tool Suite 4. If you are keen on the finished project, clone it here, github.com/jpllosa/tdd-mockmvc.

Spring Boot MockMvc Example
Spring Boot MockMvc Example

Create the Model

Create a package named com.blogspot.jpllosa.model and create GreatestCommonFactor. This object will be returned by our controller. Don't worry, Spring will automatically send the response body in JSON format because of the @RestController annotation.


package com.blogspot.jpllosa.model;

public class GreatestCommonFactor {
	Integer firstNumber;
	Integer secondNumber;
	Integer gcf;
	
	public Integer getFirstNumber() {
		return firstNumber;
	}
	
	public void setFirstNumber(Integer firstNumber) {
		this.firstNumber = firstNumber;
	}
	
	public Integer getSecondNumber() {
		return secondNumber;
	}
	
	public void setSecondNumber(Integer secondNumber) {
		this.secondNumber = secondNumber;
	}
	
	public Integer getGcf() {
		return gcf;
	}
	
	public void setGcf(Integer gcf) {
		this.gcf = gcf;
	}

}

Create the Service

Create a package named com.blogspot.jpllosa.service and create MathFunService. This is our service layer, the layer that handles the business logic. The service layer also serves as a traffic cop between the controller and persistence layer. Hence, this bean is annotated with @Service.


package com.blogspot.jpllosa.service;

import org.springframework.stereotype.Service;

import com.blogspot.jpllosa.model.GreatestCommonFactor;

@Service
public class MathFunService {

	public GreatestCommonFactor findGcf(int x, int y) {
		int result = getGCF(x, y);
		
		GreatestCommonFactor gcf = new GreatestCommonFactor();
		gcf.setFirstNumber(x);
		gcf.setSecondNumber(y);
		gcf.setGcf(result);
		
		return gcf;
	}
	
	public int getGCF(int x, int y) {
		return (y == 0) ? x : getGCF(y, x % y);
	}
}

Create the Controller

Last but not the least. Create a package named com.blogspot.jpllosa.controller and create MathFunController. This is our web layer. GET HTTP requests to the resource /get-gcf are routed here. The method checks for two query parameters and are automatically injected to the method parameters. Then sends it to the service layer for processing. It returns the type GreatestCommonFactor which is automatically marshalled into a JSON body as mentioned earlier.


package com.blogspot.jpllosa.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class MathFunController {
	
	@Autowired
	private MathFunService service;

	@GetMapping("/get-gcf")
	public GreatestCommonFactor getGcf(@RequestParam int firstNumber, @RequestParam int secondNumber) {
		return service.findGcf(firstNumber, secondNumber);
	}
}

Running the App

Now that everything is wired up, we should be able to run the application and see the response. To run the app in STS4, right click, Run As + Spring Boot App.


$ curl -G -d "firstNumber=12" -d "secondNumber=16" http://localhost:8080/get-gcf
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    44    0    44    0     0   1419      0 --:--:-- --:--:-- --:--:--  1419{"firstNumber":12,"secondNumber":16,"gcf":4}

Above, we are finding the GCF of 12 and 16. The GCF is 4.


$ curl "http://localhost:8080/get-gcf?firstNumber=0&secondNumber=6"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    42    0    42    0     0   2800      0 --:--:-- --:--:-- --:--:--  2800{"firstNumber":0,"secondNumber":6,"gcf":6}

Above is another way of sending the curl command, we are finding the GCF of 0 and 6. The GCF is 6. If you don't have curl, you can use the browser or Postman to send your HTTP request.

Spring Boot MockMvc

Now the thing we have been waiting for. Create a package named com.blogspot.jpllosa.controller under src/test/java/ and create MathFunControllerTest. This is the class under test. To run the test in STS4, right click, Run As + JUnit test.


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 org.junit.jupiter.api.Test;
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;

@SpringBootTest
@AutoConfigureMockMvc
public class MathFunControllerTest {
	
	@Autowired
	private MockMvc mockMvc;
	
	@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)));
	}

}

Now that we have seen the code, let's explain what it does. The @SpringBootTest annotation tells Spring the following:

  • To use the default ContextLoader, in this case, it's SpringBootContextLoader.
  • To automatically search for @SpringBootConfiguration.
  • To register a TestRestTemplate and/or WebTestClient. But we won't be using these.
  • To load a WebApplicationContext and provide a mock servlet environment. Used in conjunction with @AutoConfigureMockMvc for MockMvc based testing. As the name suggests, Spring will auto-confiure MockMvc and automatically inject (@Autowired) it for us.

The @Test annotation tells JUnit that the method is to be run as a test case. What does this method do? Using MockMvc, it performs a HTTP GET request on /get-gcf resource with two query parameters. It then prints the response and checks for a 200 status. By using jsonPath dot notation, we can assert the response body values with the expected values. The dollar sign represents the root member object.

Spring Boot MockMvc Example
JUnit Passed

print() output


2023-01-18 07:52:27.115  INFO 19520 --- [           main] c.b.j.controller.MathFunControllerTest   : Started MathFunControllerTest in 2.381 seconds (JVM running for 4.218)

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /get-gcf
       Parameters = {firstNumber=[12], secondNumber=[16]}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.blogspot.jpllosa.controller.MathFunController
           Method = com.blogspot.jpllosa.controller.MathFunController#getGcf(int, int)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"firstNumber":12,"secondNumber":16,"gcf":4}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

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