WebSocket-based Notification System using Spring

WebSocket-based Notification System using Spring

What if your system needs for the back end to initiate a message to one or more clients? For example, the server might need to inform the application of a finished task. Or it might want to notify that a new message is available…
To solve this, we will build a WebSocket-based notification system using Spring. We will also see how to interact with that system using either JQuery or Angular.

You can find the whole code of this tutorial on GitHub.

The problem to solve

When a web-based client needs to submit a request to its back end, it will likely use a REST call. For instance, the application might want to fetch or submit data. But what if the back end needs to tell the client something?

You can always rely on short-polling: making the client call the back end at regular interval. However that might not be optimal. Depending on the situation, the client might actually need to use small intervals. Hence that might end up being heavy for the server. It might also eat your mobile device’s data if the client is meant to run on a phone or tablet!

WebSockets brings a decent solution to that problem. You might also opt for long-polling or Server-Sent Events, depending on your use case.

However WebSockets provide a well-integrated solution which is quite easy to use. Indeed, Spring supports them out-of-the-box. And JavaScript or Angular-based clients can rely on one of the many available libraries.

So let’s dive into it!

WebSocket-based Notification System using Spring

Ours is a Spring Boot application, which uses Jetty and the WebSocket starter.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Thus we start by defining our WebSocket configuration as follows:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/notification");
        registry.setApplicationDestinationPrefixes("/swns");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/notifications")
                .setAllowedOrigins("http://localhost:4200", "http://127.0.0.1:4200")
                .withSockJS();
    }
}

Pay special attention to these:

  • Our client app will read messages from our broker by using “/notification“.
  • It will address its messages to the server with “/swns“.
  • It will use the STOMP protocol to interact with the “/notifications” end point. Furthermore, it enables SockJS to fall back on other methods if WebSocket is not supported.

We then use Spring’s @Controller to define our end points. Unlike REST end points, we will use @MessageMapping to map messages to our methods.

For instance when the client sends a message to “/swns/start” then the start() method is invoked.

@Controller
public class NotificationsController {

    private final NotificationDispatcher dispatcher;

    @Autowired
    public NotificationsController(NotificationDispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    @MessageMapping("/start")
    public void start(StompHeaderAccessor stompHeaderAccessor) {
        dispatcher.add(stompHeaderAccessor.getSessionId());
    }

    @MessageMapping("/stop")
    public void stop(StompHeaderAccessor stompHeaderAccessor) {
        dispatcher.remove(stompHeaderAccessor.getSessionId());
    }
}

Dispatch this!

The controller’s start() method registers the user against a dispatcher. It identifies him by his session ID, which is provided in the header by the client when establishing the connection.

The dispatcher shall then send notifications to its registered users:

private Set<String> listeners = new HashSet<>();

public NotificationDispatcher(SimpMessagingTemplate template) {
    this.template = template;
}

...
@Scheduled(fixedDelay = 2000)
public void dispatch() {
    for (String listener : listeners) {
        LOGGER.info("Sending notification to " + listener);

        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(listener);
        headerAccessor.setLeaveMutable(true);

        int value = (int) Math.round(Math.random() * 100d);
        template.convertAndSendToUser(
                listener, 
                "/notification/item",
                new Notification(Integer.toString(value)),
                headerAccessor.getMessageHeaders());
    }
}

These are the important parts:

  • We invoke the dispatch() method every two seconds. For that purpose, we enable scheduling with in the root Application class using Spring’s @EnableScheduling.
  • We create a new header accessor which includes the user’s session ID.
  • The injected SimpMessagingTemplate instance is used to emit our Notification payload, but only to the identified user. We also have to add the message headers, including the one defining the session ID. If we omit that last bit, your client will not receive the messages from the back end!

In order to handle disconnecting clients, we also define this event listener:

@EventListener
public void sessionDisconnectionHandler(SessionDisconnectEvent event) {
    String sessionId = event.getSessionId();
    LOGGER.info("Disconnecting " + sessionId + "!");
    remove(sessionId);
}

Of course, the app can also send a “/swns/stop” message. However the event listener handles other cases, such as the user closing the browser or sudden disconnections.

Implementing a JQuery client

On the client side we can easily use JQuery to interact with our WebSocket back end. We’ll also need two libraries to help us with SockJS and the STOMP protocol.

jQuery(function ($) {
  let stompClient;

  $("#js-connect").click(function () {
    if (!stompClient) {
      const socket = new SockJS("http://localhost:8080/notifications");
      stompClient = Stomp.over(socket);
      stompClient.connect({}, function () {

        stompClient.subscribe('/user/notification/item', function (response) {
          console.log('Got ' + response);
          $("#js-notifications").append(JSON.parse(response.body).text + ' ')
        });

        console.info('connected!')
      });
    }
  });

  $('#js-disconnect').click(function () {
    if (stompClient) {
      stompClient.disconnect();
      stompClient = null;
      console.info("disconnected :-/");
    }
  });

  $("#js-start").click(function () {
    if (stompClient) {
      stompClient.send("/swns/start", {});
    }
  });

  $("#js-stop").click(function () {
    if (stompClient) {
      stompClient.send("/swns/stop", {});
    }
  });
});

The code to connect, send and receive messages over WebSocket is pretty straightforward.

As can be seen, we create a SockJS object using the “/notifications” end point. We also emit “start” and “stop” messages with “/swns“. However we receive the dispatcher’s notifications on “/user/notification/item“! So where did that “/user” bit come from?

The “/user/” prefix tells Spring that the client only wants to listen to his messages. When the app subscribes to “/user/notification/item“, Spring transforms it into the session-specific “/notification/item-{sessionId}“. This is provided by Spring’s UserDestinationMessageHandler.

Run the example project!

Let’s run the project and play with it!

Run the Application Spring Boot class to start the back end. Then run npm installin the “ng-app” folder to download the JavaScript libraries. Finally run ng serveto serve the Angular app.

Use the buttons in the “With JQuery” section to connect and start listening. Soon enough, numbers should appear…

How cool is that!

An Angular-based client

With an Angular client, we’ll go for Observables. Luckily, this neat project provides us with all we need!

The component’s code ends up looking like this:

@Component({
  selector: 'app-notifications-rx',
  templateUrl: './notifications-rx.component.html'
})
export class NotificationsRxComponent {
  private client: RxStomp;
  public notifications: string[] = [];

  connectClicked() {
    if (!this.client || this.client.connected) {
      this.client = new RxStomp();
      this.client.configure({
        webSocketFactory: () => new SockJS('http://localhost:8080/notifications'),
        debug: (msg: string) => console.log(msg)
      });
      this.client.activate();

      this.watchForNotifications();

      console.info('connected!');
    }
  }

  private watchForNotifications() {
    this.client.watch('/user/notification/item')
      .pipe(
        map((response) => {
          const text: string = JSON.parse(response.body).text;
          console.log('Got ' + text);
          return text;
        }))
      .subscribe((notification: string) => this.notifications.push(notification));
  }

  disconnectClicked() {
    if (this.client && this.client.connected) {
      this.client.deactivate();
      this.client = null;
      console.info("disconnected :-/");
    }
  }

  startClicked() {
    if (this.client && this.client.connected) {
      this.client.publish({destination: '/swns/start'});
    }
  }

  stopClicked() {
    if (this.client && this.client.connected) {
      this.client.publish({destination: '/swns/stop'});
    }
  }
}

The code is quite simple. We publish messages to send to the server, and we watch destinations to receive notifications. Other that that it resembles the JavaScript version.

WebSocket-based Notification System using Spring… and CORS?

So what about CORS issues? Well the good news is: there is no browser-enforced CORS with WebSocket. Yay!
Just remember to configure the clients’ origins:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    ...
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/notifications")
                .setAllowedOrigins("http://localhost:4200", "http://127.0.0.1:4200")
                .withSockJS();
    }
}

In our case they are set to Angular’s server port.

Of course, that might spark security concerns depending on your use case. Then again, by using STOMP we can rely on Spring Security to harden our application. It’s out of topic for this tutorial, though. Maybe in a future post?

The big picture

Let’s sum up this thing by focusing on the big picture:

  1. A n app connects to a back end WebSocket end point named “/notifications”.
  2. It sends a message to “/swns/start” to start listening for notifications.
  3. The server registers its request by its session ID.
  4. A dispatcher sends messages to registered clients at regular intervals.
  5. The clients listen to their messages on “/user/notification/item”.
  6. Your boss loves what you achieved and gives you a raise.

Not bad eh?

Don’t forget: the whole code is available here, just for you!

Until next time… Cheers!

[Photo by La Miko from Pexels]

2 comments

  1. A Novice Reply
    02/11/2020 at 15:24

    It seems like sockjs adds a /info to the url passed to the constructor (https://stackoverflow.com/questions/35052020/why-does-sockjs-add-a-info-to-a-given-websocket-url-path) – why is this not part of the tutorial?

    • Diego Reply
      03/11/2020 at 11:39

      Hi There,
      It’s not in the tutorial because it’s exactly that: a tutorial, not a complete book on the subject!
      However, thanks to your comment, now readers of this post will also be aware of that extra nugget of a feature. Therefore… Thanks a lot for sharing!
      Cheers!

Leave A Comment

Please be polite. We appreciate that. Your email address will not be published and required fields are marked

This site uses Akismet to reduce spam. Learn how your comment data is processed.