Spring Boot (2) Websockets. Subscriptions of only one member

 1. Introduction

WebSockets are useful for informing a user of the progress of a heavy process. There are a lot of blogs and videos about this question. 

Spring boot uses the STOMP protocol

2. Creating the project

Use Spring boot initializer for creating the project, use these dependencies:

  • Spring boot dev tools
  • Lombok
  • Spring Web
  • Thymeleaf
  • Websocket
And use gradle format

Then import it from Eclipse as File > Import > Gradle > Existing Gradle Project

The final project structure after adding all the needed classes, html and js file is:





3. Add the needed dependencies to the build.gradle file

Add javascript dependencies to the build.gradle
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.5.1'
implementation 'org.webjars:stomp-websocket:2.3.4'
implementation 'org.webjars:bootstrap:3.3.7'
implementation 'org.webjars:jquery:3.6.4'
Add also the Gson dependencies, and the build.xml file will be

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.5'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'websocket1'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    implementation 'com.google.code.gson:gson:2.10.1'
    
    implementation 'org.webjars:webjars-locator-core'
    implementation 'org.webjars:sockjs-client:1.5.1'
    implementation 'org.webjars:stomp-websocket:2.3.4'
    implementation 'org.webjars:bootstrap:3.3.7'
    implementation 'org.webjars:jquery:3.6.4'
}

tasks.named('test') {
    useJUnitPlatform()
}

4. The configuration class


In the simple configuration class, you should notice:
  1. The endpoint "/hello" in this class must match the @MessageMapping("/hello") from the controller class and the new SockJS('/hello') of the javascript 
  2. The enableSimpleBorker param "/queue" can be multiple (for instance registry.enableSimpleBroker("/queue", "/error") and must match as prefix to the subscription of the javascript code stompClient.subscribe('/queue/reply/'+randNum, function(message) {
  3. If you want to send messages only to one user, in the javascript subscribe, a unique suffix can be added so that it is unique for this client, in this case a random number randNum  
Note also the annotations and the interface implementations
package websocket.config;


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 WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

5. The Controller class

In this class in order to address only one client, the destination of the messages must contain a unique identifier that matches the "subscribe" method of the javascript.

Note the @MessageMapping("/hello") that must match the endpoint "/hello" of the previous configuration class and the new SockJS('/hello') of the javascript 

Note the @Autowire annotation for the SimpleMessageSendingOperations class that is used for sending messages to the "subscribe" javascript channel "/queue/reply/"+idSession  of the client. In this example, 2 messages are returned on the receipt of one message from the client.

package websocket.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @MessageMapping("/hello")
    /*
    //Si que va
    @SendToUser("/queue/reply")
    public String send(String username)  {
        return "Hello........, " + username;
    }
    */
     public void send(Message message)  {
        String username=message.getName();
        String text=message.getText();
        String idSession=message.getIdSession();
        this.messagingTemplate.convertAndSend("/queue/reply/"+idSession, text + "  Hello...++....., " + username+ " " + idSession);
        this.messagingTemplate.convertAndSend("/queue/reply/"+idSession, text + "  Hello...+++....., " + username);
    }
}

6. The model to pass

Here is a simple model. Node the idSession field that matches the randNumber in the javascript file
package websocket.controllers;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor @AllArgsConstructor @Getter @Setter
public class Message {
    private String name;      // name of the user
    private String text;      // message to pass
    private String idSession; // Random number indicating the session
}

7. The simple Html file (index.html) of the client

It could be used thymeleaf, bootstrap, css and other improvements but for sake of simplicity it is only displayed how to reference bootstrap, stomp, jquery .... in this file. The most remarkable is:
  • Including js dependencies indicated in the build.gradle (sockjs-client, stomp-websocket, jquery)
  • A referenced to the js file app.js
  • 2 textboxes (one for the name of the client and another for the message)
  • 1 textarea for collecting the messages form the server
<!DOCTYPE html>
<html>
    <head>
        <title>Hello WebSocket</title>
        <script src="/webjars/sockjs-client/sockjs.min.js"></script>
        <script src="/webjars/jquery/jquery.min.js"></script>

        <script src="/webjars/stomp-websocket/stomp.min.js"></script>
        <script src="/app.js"></script>
    </head>
    <body>
        <div id="main-content">
            <div>
                <form>
                    <div>
                        <label>What is your name?</label>
                        <input type="text" name="name" id="name" placeholder="Your name here...">
                    </div>
                    <div>
                        <label>What is your message?</label>
                        <input type="text" name="text" id="text" placeholder="Your message here...">
                    </div>
                    <button id="send" type="submit">Send</button>
                </form>
            </div>
        </div>
        <div>
            <label>Message from server: </label><span id="message"></span>
            <textarea id="mytextarea" rows="4" cols="30">1234</textarea>
        </div>
    </body>
</html>

8. The js file (app.js)

We could have created a more useful javascript file indicating disconnection and more facilities but for sake of simplicity here is the file.

Note the "stombClient.subscribe""/queue/reply/"+idSession  that SimpleMessageSendingOperations class in the Controller  that is used for sending messages, and that  enableSimpleBorker param "/queue" of the Configuration class is the prefix of the subscribe

var randNum =  (Math.floor(Math.random() * 1000000000)).toString(); 
$(document).ready(function() {
connect();
});

function connect() {
    var socket = new SockJS('/hello');
    
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function() {
        console.log('Web Socket is connected');
        //stompClient.subscribe('/user/queue/reply', function(message) {
        //stompClient.subscribe('/queue/reply', function(message) {
        stompClient.subscribe('/queue/reply/'+randNum, function(message) {    
            $("#message").text(message.body);
            document.getElementById("mytextarea").value +="\n"+message.body;
            alert(message.body);
        });
    });
}
 
$(function() {
    $("form").on('submit', function(e) {
        e.preventDefault();
    });
    $("#send").click(function() {
        //stompClient.send("/app/hello", {}, $("#name").val()+'#$#'+randNum);
        const formElement = document.querySelector("form");
        const formData = new FormData(formElement);
        var objFormData=Object.fromEntries(formData);
        objFormData.idSession = randNum;
        alert(JSON.stringify(objFormData));
        stompClient.send("/app/hello", {}, 
            JSON.stringify(objFormData));
    });
});

9. Running the applications

The main class that has been generated by the Spring Boot Initializer is:
package websocket;

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

@SpringBootApplication
public class Websocket1Application {

    public static void main(String[] args) {
        SpringApplication.run(Websocket1Application.class, args);
    }
}
right-clicking on in and selecting Run As > Java Application and opening a browser at

http://localhost:8080 


And this message is only sent to a client (unless the random number is duplicated!)
















Comentarios

Entradas populares de este blog

SpringBoot (14) Let's start (2/10). Defining users

SpringBoot (6) Spring Data JPA (1)