Monday, August 8, 2016

Build a Real-Time Chat Application With Modulus and Spring Boot_part 1

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
  1. Doe opens the chat page to communicate with his friends.
  2. He is prompted to choose a nickname.
  3. 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.
  4. 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.
  1. buildscript {
  2.     repositories {
  3.         mavenCentral()
  4.     }
  5.     dependencies {
  6.         classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE")
  7.     }
  8. apply plugin: 'java'
  9. apply plugin: 'eclipse'
  10. apply plugin: 'idea'
  11. apply plugin: 'spring-boot'
  12. apply plugin: 'war' 
  13. jar {
  14.     baseName = 'realtime-chat'
  15.     version =  '0.1.0'
  16. war {
  17.     baseName = 'ROOT'
  18. sourceCompatibility = 1.7
  19. targetCompatibility = 1.7 
  20. repositories {
  21.     mavenCentral()
  22. sourceCompatibility = 1.7
  23. targetCompatibility = 1.7
  24. dependencies {
  25.     providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
  26.     compile("org.springframework.boot:spring-boot-starter-web")
  27.     compile("org.springframework.boot:spring-boot-starter-thymeleaf")
  28.     compile("org.springframework.boot:spring-boot-starter-data-mongodb")
  29.     compile("org.springframework.boot:spring-boot-starter-websocket")
  30.     compile("org.springframework:spring-messaging")
  31.     testCompile("junit:junit")
  32. }

  33. task wrapper(type: Wrapper) {
  34.     gradleVersion = '2.3'
  35. }
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:
  1. war {
  2.     baseName: 'ROOT.war'
  3. }
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: textauthor, and createDate. The class representation of this model is as follows:
  1. package realtime.domain; 
  2. import org.springframework.data.annotation.Id; 
  3. import java.util.Date; 
  4. /**
  5.  * @author huseyinbabal
  6.  */
  7. public class ChatMessageModel { 
  8.     @Id
  9.     private String id; 
  10.     private String text;
  11.     private String author;
  12.     private Date createDate; 
  13.     public ChatMessageModel() {
  14.     } 
  15.     public ChatMessageModel(String text, String author, Date createDate) {
  16.         this.text = text;
  17.         this.author = author;
  18.         this.createDate = createDate;
  19.     }
  20.     public String getText() {
  21.         return text;
  22.     } 
  23.     public void setText(String text) {
  24.         this.text = text;
  25.     } 
  26.     public String getAuthor() {
  27.         return author;
  28.     } 
  29.     public void setAuthor(String author) {
  30.         this.author = author;
  31.     } 
  32.     public Date getCreateDate() {
  33.         return createDate;
  34.     } 
  35.     public void setCreateDate(Date createDate) {
  36.         this.createDate = createDate;
  37.     }
  38.     @Override
  39.     public String toString() {
  40.         return "{" +
  41.                 "\"id\":\"" + id + '\"' +
  42.                 ",\"text\":\"" + text + '\"' +
  43.                 ",\"author\":\"" + author + '\"' +
  44.                 ",\"createDate\":\"" + createDate + "\"" +
  45.                 '}';
  46.     }
  47. }
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:
  1. Chat message save requests
  2. Listing the latest chat messages
  3. Serving the chat application page
  4. Serving the login page
  5. Broadcasting chat messages to clients
Here you can see the overall endpoints:
  1. package realtime.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.domain.PageRequest;
  4. import org.springframework.data.domain.Sort;
  5. import org.springframework.http.HttpEntity;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.ResponseEntity;
  8. import org.springframework.messaging.handler.annotation.MessageMapping;
  9. import org.springframework.messaging.handler.annotation.SendTo;
  10. import org.springframework.stereotype.Controller;
  11. import org.springframework.web.bind.annotation.RequestMapping;
  12. import org.springframework.web.bind.annotation.RequestMethod;
  13. import realtime.domain.ChatMessageModel;
  14. import realtime.message.ChatMessage;
  15. import realtime.repository.ChatMessageRepository; 
  16. import java.util.Date;
  17. import java.util.List; 
  18. /**
  19.  * @author huseyinbabal
  20.  */ 
  21. @Controller
  22. public class ChatMessageController {
  23.     @Autowired
  24.     private ChatMessageRepository chatMessageRepository; 
  25.     @RequestMapping("/login")
  26.     public String login() {
  27.         return "login";
  28.     } 
  29.     @RequestMapping("/chat")
  30.     public String chat() {
  31.         return "chat";
  32.     } 
  33.     @RequestMapping(value = "/messages", method = RequestMethod.POST)
  34.     @MessageMapping("/newMessage")
  35.     @SendTo("/topic/newMessage")
  36.     public ChatMessage save(ChatMessageModel chatMessageModel) {
  37.         ChatMessageModel chatMessage = new ChatMessageModel(chatMessageModel.getText(), chatMessageModel.getAuthor(), new Date());
  38.         ChatMessageModel message = chatMessageRepository.save(chatMessage);
  39.         List<ChatMessageModel> chatMessageModelList = chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent();
  40.         return new ChatMessage(chatMessageModelList.toString());
  41.     } 
  42.     @RequestMapping(value = "/messages", method = RequestMethod.GET)
  43.     public HttpEntity list() {
  44.         List<ChatMessageModel> chatMessageModelList = chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent();
  45.         return new ResponseEntity(chatMessageModelList, HttpStatus.OK);
  46.     }
  47. }
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:
  1. ...
  2. return new ChatMessage(chatMessageModelList.toString());
  3. ...
  4. 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:
  1. package realtime.repository; 
  2. import org.springframework.data.mongodb.repository.MongoRepository;
  3. import realtime.domain.ChatMessageModel; 
  4. import java.util.List; 
  5. /**
  6.  * @author huseyinbabal
  7.  */
  8. public interface ChatMessageRepository extends MongoRepository<ChatMessageModel, String> {
  9.     List<ChatMessageModel> findAllByOrderByCreateDateAsc();
  10. }
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:
  1. 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:
  1. 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.
Written by Hüseyin Babal

If you found this post interesting, follow and support us.
Suggest for you:

No comments:

Post a Comment