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.