An Interest In:
Web News this Week
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
- March 26, 2024
- March 25, 2024
- March 24, 2024
Build a Real-Time Chat Application With Modulus and Spring Boot
In this tutoral, we will use Spring Boot for the web development environment, Websockets for real-time communication, Tomcat for the Java application container, Gradle for building and managing the dependencies, Thymeleaf for template rendering, MongoDB for data storage, and finally there will be no XML for bean configurations. Just to make you inspired, at the end of this article, you will see a fully working application like the one shown below.
1. Scenario
- Doe opens the chat page to communicate with his friends.
- He is prompted to choose a nickname.
- He enters the chat page and sends a message. The message is sent to the Spring MVC endpoint to be saved to the database and broadcast.
- The specified endpoint handles the message and broadcasts that message to all clients connected to the chat system.
2. Build Dependencies and Gradle Configuration
Before proceeding with the internal structure of the project, let me explain which libraries we will use for the project features listed above, and manage them by using Gradle. When you clone the project from GitHub, you will see a file called build.gradle
in the project root directory as below.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'war'
jar {
baseName = 'realtime-chat'
version = '0.1.0'
}
war {
baseName = 'ROOT'
}
sourceCompatibility = 1.7
targetCompatibility = 1.7
repositories {
mavenCentral()
}
sourceCompatibility = 1.7
targetCompatibility = 1.7
dependencies {
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-data-mongodb")
compile("org.springframework.boot:spring-boot-starter-websocket")
compile("org.springframework:spring-messaging")
testCompile("junit:junit")
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
I will not dive into the Gradle internals, but let me explain the parts that we need for our project. Spring Boot is built mainly for developing standalone applications in jar
format. In our project, we will generate a war
project instead of jar
. That is because Modulus needs a war file to deploy the project automatically to its cloud.
In order to generate a war file, we have used apply plugin: 'war'
. Modulus also expects the war name to be ROOT.war
by default, and that is why we have used:
war {
baseName: 'ROOT.war'
}
When you run the Gradle build
task, it will generate a war file to deploy to the Tomcat container. And finally, as you can guess, the dependencies section is for third-party libraries for specific actions.
That is all for the project dependencies section, and you can refer to the Gradle user guide for more about Gradle.
3. Software Design
If you want to develop a good application, it is best practice to define your project structure in small pieces. You can see the pieces of the entire architecture of our application.
3.1. Model
We are developing a chat application, so we can say that we have a ChatMessageModel
model (i.e. domain object). While we are saving or viewing the chat message detail, we can cast the chat object from or to this ChatMessageModel
model. Also, we can use the User
model for chat users, but to make the application simpler, we will use just nickname
as text. The ChatMessageModel
model has the following fields: text
, author
, and createDate
. The class representation of this model is as follows:
package realtime.domain;
import org.springframework.data.annotation.Id;
import java.util.Date;
/**
* @author huseyinbabal
*/
public class ChatMessageModel {
@Id
private String id;
private String text;
private String author;
private Date createDate;
public ChatMessageModel() {
}
public ChatMessageModel(String text, String author, Date createDate) {
this.text = text;
this.author = author;
this.createDate = createDate;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
@Override
public String toString() {
return "{" +
"\"id\":\"" + id + '\"' +
",\"text\":\"" + text + '\"' +
",\"author\":\"" + author + '\"' +
",\"createDate\":\"" + createDate + "\"" +
'}';
}
}
This domain object helps us to represent the chat message as JSON when needed. Our model is OK, so let's continue with the controllers.
3.2. Controller
The controller is the behavior of your application. This means you need to keep your controller simple and capable of easy interaction with domain models and other services. We are expecting our controllers to handle:
- Chat message save requests
- Listing the latest chat messages
- Serving the chat application page
- Serving the login page
- Broadcasting chat messages to clients
Here you can see the overall endpoints:
package realtime.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import realtime.domain.ChatMessageModel;
import realtime.message.ChatMessage;
import realtime.repository.ChatMessageRepository;
import java.util.Date;
import java.util.List;
/**
* @author huseyinbabal
*/
@Controller
public class ChatMessageController {
@Autowired
private ChatMessageRepository chatMessageRepository;
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/chat")
public String chat() {
return "chat";
}
@RequestMapping(value = "/messages", method = RequestMethod.POST)
@MessageMapping("/newMessage")
@SendTo("/topic/newMessage")
public ChatMessage save(ChatMessageModel chatMessageModel) {
ChatMessageModel chatMessage = new ChatMessageModel(chatMessageModel.getText(), chatMessageModel.getAuthor(), new Date());
ChatMessageModel message = chatMessageRepository.save(chatMessage);
List<ChatMessageModel> chatMessageModelList = chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent();
return new ChatMessage(chatMessageModelList.toString());
}
@RequestMapping(value = "/messages", method = RequestMethod.GET)
public HttpEntity list() {
List<ChatMessageModel> chatMessageModelList = chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent();
return new ResponseEntity(chatMessageModelList, HttpStatus.OK);
}
}
The first and second endpoints are just for serving the login and main chat page. The third action is for handling new chat message storage and broadcasting. After the message is stored, it will be notified to clients through the /topic/message
channel. To store message data to MongoDB, we will use a MongoDB repository.
As you can see, there are two types of endpoint /messages
: GET and POST. When you make a POST request to endpoint /messages
with proper message payload, it will be automatically cast to the ChatMessageModel class, and the message will be saved to MongoDB. After successful saving, it will be automatically pushed to the clients. But, how? In that action, there is an annotation @SendTo("/topic/newMessage")
. This will send the content returned from the function to the clients. And the returned content is like below:
...
return new ChatMessage(chatMessageModelList.toString());
...
This is the latest message from the database:
The above message will be converted to a format for WebSocket communication. This channel message will be handled on the client side with a third-party JavaScript library, and it will be handled in the following sections.
For message db operations, spring-boot-starter-data-mongodb
is used. This library helps us for repository operations, and to create a repository object for MongoDB is very simple. You can see the example ChatMessageRepository
below:
package realtime.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import realtime.domain.ChatMessageModel;
import java.util.List;
/**
* @author huseyinbabal
*/
public interface ChatMessageRepository extends MongoRepository<ChatMessageModel, String> {
List<ChatMessageModel> findAllByOrderByCreateDateAsc();
}
If you create an interface and extend MongoRepository<?, String>
, you will be able to automatically use CRUD operations like find()
, findAll()
, save()
, etc.
As you can see, MongoRepository
expects a domain object. We have already defined this model in the Model section of the tutorial. In this repository, we have defined a custom function called findAllByOrderByCreateDateAsc()
.
If you have ever used JPA before you can understand this easily, but let me explain this briefly. If you define a function name in an interface that extends MongoRepository
, this function name will be parsed to a query on the back end by Spring automatically. It will be something like:
SELECT * FROM ChatMessageModel WHERE 1 ORDER BY createDate ASC
In ChatMessageController
, we used this function, and also we have used the default functions of the MongoRepository
:
chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent()
findAll
is used a parameter for sorting and pagination. You can have a look at the guide on the Spring website for more details about Spring JPA.
3.3. View
In the view part, we have only two pages. One of them is the login page, to get the nickname of the user, and the second one is the main chat page to send messages to chat users.
As you can see in the controller section above, they are rendered by using two endpoints, /login
and /chat
. To create interactive pages, we will use some third-party JavaScript libraries. We will use them from CDN pages. You can see the login page below:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8"/>
<title></title>
<script src="//code.jquery.com/jquery-1.11.1.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script>
$(function(){
if ($.cookie("realtime-chat-nickname")) {
window.location = "/chat"
} else {
$("#frm-login").submit(function(event) {
event.preventDefault();
if ($("#nickname").val() !== '') {
$.cookie("realtime-chat-nickname", $("#nickname").val());
window.location = "/chat";
}
})
}
})
</script>
<link href="https://code.tutsplus.com//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://code.tutsplus.com//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container" style="padding-top: 50px">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="login-panel panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Choose a nickname to enter chat</h3>
</div>
<div class="panel-body">
<form role="form" id="frm-login">
<fieldset>
<div class="form-group">
<input class="form-control" placeholder="Enter Nickname" name="nickname" id="nickname" type="text" autofocus="" required=""/>
</div>
<button type="submit" class="btn btn-lg btn-success btn-block">Enter Chat</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
On the login page, we have a sample nickname text box. When you click Enter Chat, your nickname will be saved to a cookie. This nickname will be used to set the chat message author field. When you click Enter Chat, the chat page will be opened. If you are already logged in and go to the login page, you will be redirected to the chat page.
Here's the chat page:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8"/>
<title></title>
<script src="//code.jquery.com/jquery-1.11.1.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.0/jquery.timeago.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.0.0/sockjs.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script>
var stompClient = null;
function connect() {
var socket = new SockJS('/newMessage');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/newMessage', function(message){
refreshMessages(JSON.parse(JSON.parse(message.body).content));
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
}
function refreshMessages(messages) {
$(".media-list").html("");
$.each(messages.reverse(), function(i, message) {
$(".media-list").append('<li class="media"><div class="media-body"><div class="media"><div class="media-body">'
+ message.text + '<br/><small class="text-muted">' + message.author + ' | ' + new Date(message.createDate) + '</small><hr/></div></div></div></li>');
});
}
$(function(){
if (typeof $.cookie("realtime-chat-nickname") === 'undefined') {
window.location = "/login"
} else {
connect();
$.get("/messages", function (messages) {
refreshMessages(messages)
});
$("#sendMessage").on("click", function() {
sendMessage()
});
$('#messageText').keyup(function(e){
if(e.keyCode == 13)
{
sendMessage();
}
});
}
function sendMessage() {
$container = $('.media-list');
$container[0].scrollTop = $container[0].scrollHeight;
var message = $("#messageText").val();
var author = $.cookie("realtime-chat-nickname");
stompClient.send("/app/newMessage", {}, JSON.stringify({ 'text': message, 'author': author}));
$("#messageText").val("")
$container.animate({ scrollTop: $container[0].scrollHeight }, "slow");
}
})
</script>
<link href="https://code.tutsplus.com//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://code.tutsplus.com//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet"/>
<style type="text/css">
.fixed-panel {
min-height: 500px;
max-height: 500px;
}
.media-list {
overflow: auto;
}
</style>
</head>
<body>
<div class="container">
<div class="row " style="padding-top:40px;">
<h3 class="text-center">Realtime Chat Application with Spring Boot, Websockets, and MongoDB </h3>
<br/><br/>
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading">
<strong><span class="glyphicon glyphicon-list"></span> Chat History</strong>
</div>
<div class="panel-body fixed-panel">
<ul class="media-list">
</ul>
</div>
<div class="panel-footer">
<div class="input-group">
<input type="text" class="form-control" placeholder="Enter Message" id="messageText" autofocus=""/>
<span class="input-group-btn">
<button class="btn btn-info" type="button" id="sendMessage">SEND <span class="glyphicon glyphicon-send"></span></button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
This page is for simply viewing and sending messages. Messages are delivered to this page via WebSockets. On this page you can see sockjs
and stompjs
. Those are for handling notifications. Whenever a new message comes, the latest messages area is repopulated.
By the way, when you first open the chat page, the latest messages will be retrieved in the messages area. As you can see on the JavaScript side, our message channel is newMessage
. So, we are listening to this channel, and when you click the Send button, the message in the text box will be sent to the endpoint and that message will be broadcast to the connected clients after successful storage.
As you can see, the software architecture here is very simple and easy to develop. We have production-ready code, and let's deploy it to Modulus.
Modulus is one of the best PaaS for deploying, scaling and monitoring your application in the language of your choice.
4. Deployment
4.1. Prerequisites
Before deploying the application, let's create a database by using the Modulus admin panel. You need a Modulus account for dba creation and application deployment, so please create an account if you don't have one.
Go to the Modulus dashboard and create a database:
On the database creation screen please provide a database name, select the MongoDB version (I have used 2.6.3, so it will be better if you choose 2.6.3 too), and finally define a user to apply database read/write operations.
You can get a MongoDB URL after successfully creating the database. We will use the MongoDB URL in the environment variables to be used by the Spring Boot application.
To set the environment variables for MongoDB, you need to have an application. Go to Dashboard and click Projects. On this page, click Create New Project.
To continue with configuring the environment variables, please go to Dashboard and click Projects. Select your project, and click Administration. Scroll down the page, and set environment variables with the key SPRING_DATA_MONGODB_URI
and value of your database URI:
When you deploy your application, Spring will use that environment variable value. We have done with the requirements, and let's continue with the deployment part.
4.2. Deployment With CLI
In order to deploy the project, run a gradle build task:
gradle build
This task will generate a war file called ROOT.war
. Copy this file to a fresh folder and install modulus CLI if you haven't.
npm install -g modulus
Log in to the system;
modulus login
Now execute the following command to deploy ROOT.war
to Modulus.
modulus deploy
This will deploy the war file and you can tail project logs to see the status of your deployment by executing the following command:
modulus project logs tail
That's all with the deployment!
5. Conclusion
The main purpose of this tutorial is to show you how to create a real-time chat application with Spring Boot, WebSockets, and MongoDB.
In order to run the project in production, Modulus is used as a PaaS provider. Modulus has very simple steps for deployment, and it also has an internal database (MongoDB) for our projects. Beside this, you can use very helpful tools in the Modulus dashboard like Logs, Notifications, Auto-Scaling, Db Administration, and more.
Original Link:
TutsPlus - Code
Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.More About this Source Visit TutsPlus - Code