πŸ‘‹ Update: There is a much cleaner way to use Jetty WebSocket with Spring Boot now. If you are working with latest Spring Boot and Jetty version follow this blog post.


This post describes how to use Jetty WebSocket with Spring Boot.

About this App

I am going to build a websocket through which server will send random names to all it’s subscribers. At any given time all the subscribers will get same name.

To generate random names I have used Java Faker library.

1. Create Spring Boot App

You can create Spring Boot app using Spring Intializr. Choose Gradle, and add Web as a dependency and download project as zip file. Open this project in your favorite editor or IDE

Use Embedded Jetty instead of Embedded Apache Tomcat

By default Spring Initializr adds embedded apache tomcat as a dependency for Spring Web. We have to exclude the tomcat dependencies and introduce Embedded Jetty.

Your build.gradle should have dependencies section like this.

dependencies {
	compile("org.springframework.boot:spring-boot-starter-web") {
		exclude module: "spring-boot-starter-tomcat"
	}
	compile("org.springframework.boot:spring-boot-starter-jetty")
	compile(group: 'com.github.javafaker', name: 'javafaker', version: '0.12')

	testCompile('org.springframework.boot:spring-boot-starter-test')
}

2. Create RandomNameService

I have used @Service annotation. Spring will be able to create a bean with name randomNameSvc. In this class listenerSessions field maintains a set of open socket sessions. We have addSession() and removeSession() methods to manage the set.

The init() method is annotated with @PostConstruct. Hence this method will be called on bean after initiation. This method starts a thread which sends random name to all open sessions after every two seconds.

import org.eclipse.jetty.websocket.api.Session;
...

@Service("randomNameSvc")
public class RandomNameService {
    private Set<Session> listenerSessions = new CopyOnWriteArraySet<>();

    public void removeSession(Session session){
        listenerSessions.remove(session);
    }

    public void addSession(Session session){
        listenerSessions.add(session);
    }

    @PostConstruct
    private void init(){
        Runnable runnable = () -> {
            while (true){
                try {
                    Thread.sleep(2000);
                    String message = new Faker().name().fullName();
                    listenerSessions.stream()
                            .filter(s-> s.isOpen())
                            .forEach(s -> sendRandomName(s,message));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
    }

    private void sendRandomName(Session s, String message) {
        try {
            s.getRemote().sendString(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

3. Get RandomNameService bean outside of Spring Context

We are going to create WebSocket outside of Spring Context. The RandomNameService should be accessible to WebSocket.

Hence we need to create a Utility class which will give us the bean of RandomNameService. We have to register the object of this Utility class to Spring. Spring will provide applicationContext using setApplicationContext method.

public class DemoBeanUtil implements ApplicationContextAware {
    private static ApplicationContext appCxt;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        appCxt = applicationContext;
    }

    public static RandomNameService getRandomNameService() throws BeansException {
        return (RandomNameService) appCxt.getAutowireCapableBeanFactory().getBean("randomNameSvc");
    }
}

4. Create WebSocket

Here @OnWebSocketConnect and @OnWebSocketClose annotations used. When socket connection is established onConnect(Session) method will be called. When connection is closed by client (in our case browser) onClose(Session , int , String) method will be called.

In both methods we are using DemoBeanUtil to get RandomNameService bean created by Spring. Then we are adding new session on connection establishment. If connection is closed we no longer need to serve random names to those clients hence we are removing that session.

import com.dineshsawant.websocketdemo.util.DemoBeanUtil;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;

@WebSocket
public class RandomNameSocket {
    @OnWebSocketConnect
    public void onConnect(Session session) {
        DemoBeanUtil.getRandomNameService().addSession(session);
    }

    @OnWebSocketClose
    public void onClose(Session session, int _closeCode, String _closeReason) {
        DemoBeanUtil.getRandomNameService().removeSession(session);
    }
}

5. Create WebSocketServlet

Here we create special servlet and register our Socket class.

import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
...

public class RandomNameServlet extends WebSocketServlet {
    @Override
    public void configure(WebSocketServletFactory webSocketServletFactory) {
webSocketServletFactory.register(RandomNameSocket.class);
    }
}

6. Register WebSocketServlet with Spring

We have to use register the RandomNameServlet with Spring Web. This can be easily done with ServletRegistrationBean. We have given url mapping for our servlet. This servlet will be accessible at http://server:port/ws/random.

We are also injecting object of DemoBeanUtil which is extension of ApplicationContextAware.

@Configuration
public class AppConfig {
    @Bean
    public ServletRegistrationBean socketServlet(){
        return new ServletRegistrationBean(new RandomNameServlet(), "/ws/random");
    }

    @Bean
    public DemoBeanUtil randomNameBeanUtil(){
        return new DemoBeanUtil();
    }
}

7. Write Client for your WebSocket

I have added simple controller in Spring, which serves static html file.

@Controller
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/randomNames")
    public String randomNames() {
        return "/randomNames.html";
    }
}

DemoController will serve randomNames.html file from static resources/static directory.

In randomNames.html, A javascript file websocket-opener.js is getting loaded from resources/static/js directory.

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Demo : Random Number Generator</title>
  <script src="/js/websocket-opener.js"></script>
</head>
<body>
<div id ="holder">
    Waiting for Websocket...
</div>
</body>
</html>

In websocket-opener.js, we are using Javascript API to connect to websocket which is located at /ws/random path.

For each message on socket we are getting a random name, which is appened on page.

var ws = new WebSocket("ws://" + location.host + "/ws/random");


ws.onopen = function() {
    var newDiv = document.createElement("div");
    newDiv.innerHTML = "WebSocket Opened";

    var holder = document.getElementById("holder");
    holder.appendChild(newDiv);
}

ws.onmessage = function(evt) {
    var newDiv = document.createElement("div");
    newDiv.innerHTML = "> " + evt.data;

    var holder = document.getElementById("holder");
    holder.appendChild(newDiv);
}

ws.onclose = function() {
    var newDiv = document.createElement("div");
    newDiv.innerHTML = "WebSocked Closed. Refresh page to open new WebSocket.";

    var holder = document.getElementById("holder");
    holder.appendChild(newDiv);
}

ws.onerror = function() {
    var newDiv = document.createElement("div");
    newDiv.innerHTML = "WebSocked Error. Try refreshing the page.";

    var holder = document.getElementById("holder");
    holder.appendChild(newDiv);
}

Running Spring boot application

I have used gradle to setup dependencies and the src directory structure looks like this.

|-- build.gradle
|-- src
|   |-- main
|   |   |-- java
|   |   |   `-- com
|   |   |       `-- dineshsawant
|   |   |           `-- websocketdemo
|   |   |               |-- config
|   |   |               |   `-- AppConfig.java
|   |   |               |-- controller
|   |   |               |   `-- DemoController.java
|   |   |               |-- service
|   |   |               |   `-- RandomNameService.java
|   |   |               |-- servlet
|   |   |               |   `-- RandomNameServlet.java
|   |   |               |-- socket
|   |   |               |   `-- RandomNameSocket.java
|   |   |               |-- util
|   |   |               |   `-- DemoBeanUtil.java
|   |   |               `-- WebsocketdemoApplication.java
|   |   `-- resources
|   |       |-- application.properties
|   |       |-- static
|   |       |   |-- js
|   |       |   |   `-- websocket-opener.js
|   |       |   `-- randomNames.html
|   |       `-- templates
|   `-- test
|       `-- java
|           `-- com
|               `-- dineshsawant
|                   `-- websocketdemo
|                       `-- WebsocketdemoApplicationTests.java

The application can be started by running bootRun task defined by Spring Boot Gradle plugin.

gradle bootRun

Once application is up and running visit http://localhost:9441/demo/randomNames. The port 9441 is specified in application.properties file.

We will get webpage with random names appended at two seconds interval.

Output on web page will look something like this.

Waiting for Websocket...
WebSocket Opened
> Uriel Hartmann
> Julian Littel
> Layla Schmeler MD
> Johnny Rippin
> Adrianna Ziemann I
> Tobin Koelpin
> Delbert Stark
> Coty Cremin
> Mrs. Hudson Durgan
> Reed Block

Source Code Repository : https://github.com/dinsaw/Spring-Boot-Jetty-WebSocket-Example.