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.

No comments:

Post a Comment