Build a Chat Application using Spring Boot + WebSocket + RabbitMQ | JavaInUse



Build a Chat Application using Spring Boot + WebSocket + RabbitMQ

In a previous post we had created a Spring Boot + WebSocket Hello World Example. In this post we will be creating a real time multi use chat application.
In a previous post we had also seen how to deploy Spring Boot + RabbitMQ applications to Pivotal Cloud Foundry. I have hosted the real time chat application that we are creating to Pivotal Cloud Foundry and use can see the demo at JavaInUse Chat Application.
JavaInUse Chat Application Demo
Spring Boot Websocket Chat Application Architecture
For this tutorial we will be making use of STOMP protocol. STOMP is a simple text-oriented messaging protocol used by our UI Client(browser) to connect to enterprise message brokers.
Clients can use the SEND or SUBSCRIBE commands to send or subscribe for messages along with a "destination" header that describes what the message is about and who should receive it.
It defines a protocol for clients and servers to communicate with messaging semantics. It does not define any implementation details, but rather addresses an easy-to-implement wire protocol for messaging integrations. The protocol is broadly similar to HTTP, and works over TCP using the following commands:
  • CONNECT
  • SEND
  • SUBSCRIBE
  • UNSUBSCRIBE
  • BEGIN
  • COMMIT
  • ABORT
  • ACK
  • NACK
  • DISCONNECT

Spring Boot Websocket Chat STOMP Architecture
When using Spring's STOMP support, the Spring WebSocket application acts as the STOMP broker to clients. Messages are routed to @Controller message-handling methods or to a simple, in-memory broker that keeps track of subscriptions and broadcasts messages to subscribed users.
You can also configure Spring to work with a dedicated STOMP broker (e.g. RabbitMQ, ActiveMQ, etc) for the actual broadcasting of messages. In that case Spring maintains TCP connections to the broker, relays messages to it, and also passes messages from it down to connected WebSocket clients.

Video

This tutorial is explained in the below Youtube Video.

Lets Begin-

Creating the Spring Boot WebSocket Application-

The project will be as follows -
Spring Boot Websocket Chat Maven Project

Define the pom.xml as follows- Add the spring-boot-starter-websocket and spring-boot-starter-amqp dependency.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>spring-boot-websocket-chat</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>spring-boot-websocket-chat</name>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.0.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-reactor-netty</artifactId>
		</dependency>
	</dependencies>
</project>
Define the domain class WebSocketChatMessage as follows-
package com.javainuse.domain;

public class WebSocketChatMessage {
	private String type;
	private String content;
	private String sender;

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getSender() {
		return sender;
	}

	public void setSender(String sender) {
		this.sender = sender;
	}
}
Define the WebSocket Configuration class.
@Configuration tells that it is a Spring configuration class. @EnableWebSocketMessageBroker enables WebSocket message handling, backed by a message broker. Here we are using STOMP as a mesage broker. The method configureMessageBroker() enables a rabbitmq message broker to carry the messages back to the client on destinations prefixed with "/topic" and "/queue".
Also here we have configured that all messages with "/app" prefix will be routed to @MessageMapping-annotated methods in controller class.
For example "/app/chat.sendMessage" is the endpoint that the WebSocketController.sendMessage() method is mapped to handle.
Spring Boot Websocker Flow
package com.javainuse.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketChatConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/websocketApp").withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.setApplicationDestinationPrefixes("/app");
		registry.enableStompBrokerRelay("/topic").setRelayHost("localhost").setRelayPort(61613).setClientLogin("guest")
				.setClientPasscode("guest");

	}
}
Define the WebSocker Listener class. This class listens to events such as a new user joining the chat or an user leaving the chat.
package com.javainuse.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

import com.javainuse.domain.WebSocketChatMessage;

@Component
public class WebSocketChatEventListener {

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        System.out.println("Received a new web socket connection");
    }

    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if(username != null) {

            WebSocketChatMessage chatMessage = new WebSocketChatMessage();
            chatMessage.setType("Leave");
            chatMessage.setSender(username);

            messagingTemplate.convertAndSend("/topic/public", chatMessage);
        }
    }
}
Define the controller class. Previously we have configured the websocket such that all messages coming from the client with prefix "/app" will be routed to the appropriate message handling methods annotated with @MessageMapping.
For example, a message with destination /app/chat.newUser will be routed to the newUser() method, and a message with destination /app/chat.sendMessage will be routed to the sendMessage() method.
package com.javainuse.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

import com.javainuse.domain.WebSocketChatMessage;

@Controller
public class WebSocketChatController {

	@MessageMapping("/chat.sendMessage")
	@SendTo("/topic/javainuse")
	public WebSocketChatMessage sendMessage(@Payload WebSocketChatMessage webSocketChatMessage) {
		return webSocketChatMessage;
	}

	@MessageMapping("/chat.newUser")
	@SendTo("/topic/javainuse")
	public WebSocketChatMessage newUser(@Payload WebSocketChatMessage webSocketChatMessage,
			SimpMessageHeaderAccessor headerAccessor) {
		headerAccessor.getSessionAttributes().put("username", webSocketChatMessage.getSender());
		return webSocketChatMessage;
	}

}
Finally Define the Spring Boot Class with @SpringBootApplication annotation
package com.javainuse;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootChatApplication {

	public static void main(String[] args) {

		SpringApplication.run(
				 SpringBootChatApplication.class , args);
	}
}

Define the index.html. Here we have defined the UI for our chat application. Also it makes use of the sockjs and stomp libraries. The HTML file contains the user interface for displaying the chat messages. It includes sockjs and stomp javascript libraries. SockJS is a browser JavaScript library that provides a WebSocket-like object. SockJS gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication channel between the browser and the web server.
STOMP JS is the stomp client for javascript.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
	content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>JavaInUse Chat Application | JavaInUse</title>
<link rel="stylesheet" href="/css/style.css" />
<link
	href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"
	rel="stylesheet" id="bootstrap-css">
<script
	src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
</head>
<body>

	<div id="welcome-page">
		<div class="welcome-page-container">
			<h1 class="title">Welcome - To join the chat group enter your
				name</h1>
			<form id="welcomeForm" name="welcomeForm">
				<div class="form-group">
					<input type="text" id="name" placeholder="name"
						class="form-control" />
				</div>
				<div class="form-group">
					<button type="submit" onclass="accent username-submit">Lets
						Begin</button>
				</div>
			</form>
		</div>
	</div>


	<div id="dialogue-page" class="hidden">
		<div class="dialogue-container">
			<div class="dialogue-header">
				<h2>JavaInUse Chat Application</h2>
			</div>
			<ul id="messageList">

			</ul>
			<form id="dialogueForm" name="dialogueForm" nameForm="dialogueForm">
				<div class="form-group">
					<div class="input-group clearfix">
						<input type="text" id="chatMessage"
							placeholder="Enter a message...." autocomplete="off"
							class="form-control" />
						<button type="submit" class="glyphicon glyphicon-share-alt">Send</button>
					</div>
				</div>
			</form>
		</div>
	</div>
	<script
		src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
	<script
		src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
	<script src="/js/script.js"></script>
</body>
</html>
Define the javascript file. The stompClient.subscribe() function takes a callback method which is called whenever a message arrives on the subscribed topic. The connect() function makes use of the SockJS and stomp client to establish connection to the to the /websocketApp endpoint that we configured in Spring Boot application. The client subscribes to /topic/javainuse destination.
'use strict';

document.querySelector('#welcomeForm').addEventListener('submit', connect, true)
document.querySelector('#dialogueForm').addEventListener('submit', sendMessage, true)

var stompClient = null;
var name = null;

function connect(event) {
	name = document.querySelector('#name').value.trim();

	if (name) {
		document.querySelector('#welcome-page').classList.add('hidden');
		document.querySelector('#dialogue-page').classList.remove('hidden');

		var socket = new SockJS('/websocketApp');
		stompClient = Stomp.over(socket);

		stompClient.connect({}, connectionSuccess);
	}
	event.preventDefault();
}

function connectionSuccess() {
	stompClient.subscribe('/topic/javainuse', onMessageReceived);

	stompClient.send("/app/chat.newUser", {}, JSON.stringify({
		sender : name,
		type : 'newUser'
	}))

}

function sendMessage(event) {
	var messageContent = document.querySelector('#chatMessage').value.trim();

	if (messageContent && stompClient) {
		var chatMessage = {
			sender : name,
			content : document.querySelector('#chatMessage').value,
			type : 'CHAT'
		};

		stompClient.send("/app/chat.sendMessage", {}, JSON
				.stringify(chatMessage));
		document.querySelector('#chatMessage').value = '';
	}
	event.preventDefault();
}

function onMessageReceived(payload) {
	var message = JSON.parse(payload.body);

	var messageElement = document.createElement('li');

	if (message.type === 'newUser') {
		messageElement.classList.add('event-data');
		message.content = message.sender + 'has joined the chat';
	} else if (message.type === 'Leave') {
		messageElement.classList.add('event-data');
		message.content = message.sender + 'has left the chat';
	} else {
		messageElement.classList.add('message-data');

		var element = document.createElement('i');
		var text = document.createTextNode(message.sender[0]);
		element.appendChild(text);

		messageElement.appendChild(element);

		var usernameElement = document.createElement('span');
		var usernameText = document.createTextNode(message.sender);
		usernameElement.appendChild(usernameText);
		messageElement.appendChild(usernameElement);
	}

	var textElement = document.createElement('p');
	var messageText = document.createTextNode(message.content);
	textElement.appendChild(messageText);

	messageElement.appendChild(textElement);

	document.querySelector('#messageList').appendChild(messageElement);
	document.querySelector('#messageList').scrollTop = document
			.querySelector('#messageList').scrollHeight;

}
Define the CSS-
{
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box;
}

html, body {
	height: 100%;
	overflow: hidden;
}

body {
	margin: 0;
	padding: 0;
	font-weight: 400;
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	font-size: 1rem;
	line-height: 1.58;
	color: #333;
	background-color: #f4f4f4;
	height: 100%;
}

.clearfix:after {
	display: block;
	content: "";
	clear: both;
}

.hidden {
	display: none;
}

input {
	padding-left: 10px;
	outline: none;
}

h1, h2, h3, h4, h5, h6 {
	margin-top: 20px;
	margin-bottom: 20px;
}

h1 {
	font-size: 1.7em;
}

a {
	color: #128ff2;
}

button {
	box-shadow: none;
	border: 1px solid transparent;
	font-size: 14px;
	outline: none;
	line-height: 100%;
	white-space: nowrap;
	vertical-align: middle;
	padding: 0.6rem 1rem;
	border-radius: 2px;
	transition: all 0.2s ease-in-out;
	cursor: pointer;
	min-height: 38px;
}

button.default {
	background-color: #e8e8e8;
	color: #333;
	box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
}

button.primary {
	background-color: #128ff2;
	box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
	color: #fff;
}

}
button.accent {
	background-color: #ff4743;
	box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
	color: #fff;
}

#welcome-page {
	text-align: center;
}

.welcome-page-container {
	background-color: grey;
	width: 100%;
	max-width: 500px;
	display: inline-block;
	margin-top: 42px;
	vertical-align: middle;
	position: relative;
	padding: 35px 55px 35px;
	min-height: 250px;
	position: absolute;
	top: 50%;
	left: 0;
	right: 0;
	margin: 0 auto;
	margin-top: -160px;
}

#dialogue-page {
	position: relative;
	height: 100%;
}

.dialogue-container {
	background-color: green;
	margin: 10px 0;
	max-width: 700px;
	margin-left: auto;
	margin-right: auto;
	box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
	margin-top: 30px;
	height: calc(100% - 60px);
	max-height: 600px;
	position: relative;
}

#dialogue-page ul {
	list-style-type: none;
	background-color: #FFF;
	margin: 0;
	overflow: auto;
	overflow-y: scroll;
	padding: 0 20px 0px 20px;
	height: calc(100% - 150px);
}

#dialogue-page #dialogueForm {
	padding: 20px;
}

#dialogue-page ul li {
	line-height: 1.5rem;
	padding: 10px 20px;
	margin: 0;
	border-bottom: 1px solid #f4f4f4;
}

#dialogue-page ul li p {
	margin: 0;
}

#dialogue-page .event-data {
	width: 100%;
	text-align: center;
	clear: both;
}

#dialogue-page .event-data p {
	color: #777;
	font-size: 14px;
	word-wrap: break-word;
}

#dialogue-page .message-data {
	padding-left: 68px;
	position: relative;
}

#dialogue-page .message-data i {
	position: absolute;
	width: 42px;
	height: 42px;
	overflow: hidden;
	left: 10px;
	display: inline-block;
	vertical-align: middle;
	font-size: 18px;
	line-height: 42px;
	color: #fff;
	text-align: center;
	border-radius: 50%;
	font-style: normal;
	text-transform: uppercase;
}

#dialogue-page .message-data span {
	color: #333;
	font-weight: 600;
}

#dialogue-page .message-data p {
	color: #43464b;
}

#dialogueForm .input-group input {
	border: 0;
	padding: 10px;
	background: whitesmoke;
	float: left;
	width: calc(100% - 85px);
}

#dialogueForm .input-group button {
	float: left;
	width: 80px;
	height: 38px;
	margin-left: 5px;
}

.dialogue-header {
	text-align: center;
	padding: 15px;
	border-bottom: 1px solid #ececec;
}

.dialogue-header h2 {
	margin: 0;
	font-weight: 500;
}

@media screen and (max-width: 730px) {
	.dialogue-container {
		margin-left: 10px;
		margin-right: 10px;
		margin-top: 10px;
	}
}
We are done with the required Java code. Now lets start RabbitMQ. As we had explained in detail in the Getting started with RabbitMQ perform the steps to start the RabbitMQ.
We will need to perform one additional step with RabbitMQ - Install STOMP plugin for RabbitMQ so that it can work with STOMP Messages
RabbitMQ STOMP Plugin
Next start the Spring Boot Chat application by running it as a Java Application. Hit the url as follows- http://localhost:8080
Spring Boot Websocker Group
Enter the username
We are then shown the chat window.
Spring Boot Chat Join If we got the rabbitMQconsole we can see it has created a queue.
Spring Boot Stomp Queue RabbitMQ

Download Source Code

Download it -
Spring Boot + WebSocket + RabbitMQ Chat Example