Kim Rudolph

Spring and WebSockets

The goal of this tutorial is to build a simple integration of a service sending random data to connected subscribers:

Highcharts chart filled with random data

Examples with more features are the spring-websocket-portfolio or the spring quickstart guides messaging-stomp-websocket and messaging-stomp-msgsjs.

WebApplication Configuration

First thing is to setup a web app with spring. The important part is the setAsyncSupported(true) setting to enable asynchronous operations which is needed for deployments (Apache Tomcat etc.).

package de.kimrudolph.tutorials.configuration;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(final ServletContext context) throws ServletException {

        final AnnotationConfigWebApplicationContext root = new AnnotationConfigWebApplicationContext();

        root.scan("de.kimrudolph.tutorials");

        context.addListener(new ContextLoaderListener(root));

        final ServletRegistration.Dynamic appServlet = context.addServlet(
            "appServlet",
            new DispatcherServlet(new GenericWebApplicationContext()));
        appServlet.setAsyncSupported(true);
        appServlet.setLoadOnStartup(1);
        appServlet.addMapping("/*");
    }

}

An @EnableScheduling annotation is added as it is needed for the random data generator.

package de.kimrudolph.tutorials.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableWebMvc
@EnableScheduling
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureDefaultServletHandling(
        final DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

The subscribers need to be authenticated. For this test case, a user:password configuration should be ok.

package de.kimrudolph.tutorials.configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests().anyRequest().authenticated();
    }

    @Override
    protected void configure(final AuthenticationManagerBuilder auth)
        throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("user").password("password");
    }
}

Available endpoints (here: /random) are configured via WebSocketMessageBrokerConfigurer.

package de.kimrudolph.tutorials.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
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(final StompEndpointRegistry registry) {
        registry.addEndpoint("/random").withSockJS();
    }

    @Override
    public void configureClientInboundChannel(
        final ChannelRegistration registration) {
    }

    @Override
    public void configureClientOutboundChannel(
        final ChannelRegistration registration) {
    }

    @Override
    public void configureMessageBroker(final MessageBrokerRegistry registry) {
    }

}

Random Data Generator

A configured scheduled delay of 1000ms will execute the sendDataUpdates() method once every second. The method sends a random integer to the /data destination.

package de.kimrudolph.tutorials.utils;

import java.util.Random;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.core.MessageSendingOperations;
import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class RandomDataGenerator implements
    ApplicationListener<BrokerAvailabilityEvent> {

    private final MessageSendingOperations<String> messagingTemplate;

    @Autowired
    public RandomDataGenerator(
        final MessageSendingOperations<String> messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    @Override
    public void onApplicationEvent(final BrokerAvailabilityEvent event) {
    }

    @Scheduled(fixedDelay = 1000)
    public void sendDataUpdates() {

        this.messagingTemplate.convertAndSend(
            "/data", new Random().nextInt(100));

    }
}

Application Javascript

Besides the needed frameworks mentioned above, there are two javascript parts to actually show the chart. First goes a standard chart definition with a randomData variable linked to the chart series. Changes to that variable will trigger an event to update the chart.

src/main/webapp/resources/application.js

var randomData;

$('#randomDataChart').highcharts({
  chart : {
    type : 'line',
    events : {
      load : function() {
        randomData = this.series[0];
      }
    }
  },
  title : {
    text : false
  },
  xAxis : {
    type : 'datetime',
    minRange : 60 * 1000
  },
  yAxis : {
    title : {
      text : false
    }
  },
  legend : {
    enabled : false
  },
  plotOptions : {
    series : {
      threshold : 0,
      marker : {
        enabled : false
      }
    }
  },
  series : [ {
    name : 'Data',
      data : [ ]
    } ]
});

And secondly the webocket handling. A connection is established and the client subscribes to the /data endpoint. Any messages received at that subscription will add a data point to randomData. If there are more than 60 data points, the oldest one will be removed.

src/main/webapp/resources/application.js

...
var socket = new SockJS('/spring-mvc-websockets/random');
var client = Stomp.over(socket);

client.connect('user', 'password', function(frame) {

  client.subscribe("/data", function(message) {
    var point = [ (new Date()).getTime(), parseInt(message.body) ];
    var shift = randomData.data.length > 60;
    randomData.addPoint(point, true, shift);
  });

});

Chart display

The HTML view is pretty straightforward as it only needs to define a randomDataChart placeholder for the chart and include the javascript files.

src/main/webapp/index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>WebSocket chart example</title>
  </head>
  <body>
    <div id="randomDataChart"></div>
  </body>
  <script type="text/javascript" src="resources/jquery.js" /></script>
  <script type="text/javascript" src="resources/sockjs.js" /></script>
  <script type="text/javascript" src="resources/highcharts.js" /></script>
  <script type="text/javascript" src="resources/stomp.js" /></script>
  <script type="text/javascript" src="resources/application.js" /></script>
</html>

Run the Example

The mvn jetty:run goal starts the example at http://0.0.0.0:8080/spring-mvc-websockets. When connected, the console should print:

Opening Web Socket...
Web Socket Opened...
>>> CONNECT
login:user
passcode:password
accept-version:1.1,1.0
heart-beat:10000,10000

<<< CONNECTED
heart-beat:0,0
version:1.1

connected to server undefined
>>> SUBSCRIBE
id:sub-0
destination:/data

<<< MESSAGE
content-type:application/json;charset=UTF-8
subscription:sub-0
message-id:k1e438ue-19
destination:/data
content-length:2

59
...

Source

The full application can be found at the spring-mvc-websocket repository.