Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Build a Chat Application Using Spring Boot + WebSocket + RabbitMQ

DZone's Guide to

Build a Chat Application Using Spring Boot + WebSocket + RabbitMQ

Learn how to create a web-based chat app by using several interesting technologies across a full-stack application.

· Web Dev Zone ·
Free Resource

Learn how to add document editing and viewing to your web app on .Net (C#), Node.JS, Java, PHP, Ruby, etc.

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 the 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.

Let's begin.

Creating the Spring Boot WebSocket Application

The project will be structured as follows:

Spring Boot Websocket Chat Maven Project

Define the pom.xml as follows. Add the spring-boot-starter-websocket and spring-boot-starter-amqpdependency.

<?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 us that it is a Spring configuration class. @EnableWebSocketMessageBroker enables WebSocket message handling, backed by a message broker. Here we are using STOMP as a message 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 the 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.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 the @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 file. 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 the 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">Let's 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 a connection to the to the /websocketApp endpoint that we configured in Spring Boot application. The client subscribes to the /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, let's start RabbitMQ. As we had explained in detail in the Getting started with RabbitMQ and perform the steps to start the RabbitMQ.

We will need to perform one additional step with RabbitMQ: install the 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. Navigate to the following URL: http://localhost:8080.

Spring Boot Websocker Group

Enter the username.

We are then shown the chat window. 

Spring Boot Chat Join If we go to the RabbitMQ console, we can see it has created a queue. 

Spring Boot Stomp Queue RabbitMQ

Extend your web service functionality with docx, xlsx and pptx editing. Check out ONLYOFFICE document editors for integration.

Topics:
websocket api ,rabbitmq ,web dev ,spring boot tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}