In my old blog post, Jetty Websocket was getting created outside of Spring Context, and there was much more glue code to integrate it with Spring Boot. This blog post will provide you with a much cleaner approach to using Jetty WebSocket in 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.

For this post, I am using Spring Boot version 2.7.5 and Jetty version 9.4.

1. Create Spring Boot App

You can create Spring Boot app using Spring Intializr. Choose Gradle Project, add Web and WebSocket as a dependency, and download project as a 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 {
  implementation('org.springframework.boot:spring-boot-starter-web') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
  }

  implementation('org.springframework.boot:spring-boot-starter-websocket') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
  }

  implementation 'org.springframework.boot:spring-boot-starter-jetty'

  implementation(group: 'com.github.javafaker', name: 'javafaker', version: '0.12')

  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2. Create WebSocketConfig and WebSocketHandler

In WebSocketConfig, we have to register WebSocket endpoint /ws/random. On this endpoint, the connection will be upgraded to WebSocket. We have to create the handler RandomNamesWebSocketHandler for our websocket.

package com.dineshsawant.jettywebsocket.config;

import com.dineshsawant.jettywebsocket.controller.RandomNamesWebSocketHandler;
import org.eclipse.jetty.websocket.api.*;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import javax.servlet.ServletContext;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    ServletContext servletContext;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(wsHandler(), "/ws/random")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

    @Bean
    public WebSocketHandler wsHandler() {
        return new RandomNamesWebSocketHandler();
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {
        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600_000); // 10 minutes of idle timeout

        WebSocketServerFactory factory = new WebSocketServerFactory(servletContext, policy);
        return new DefaultHandshakeHandler(new JettyRequestUpgradeStrategy(factory));
    }

}

RandomNamesWebSocketHandler maintains set of open web socket connections. It also creates a thread which sends a random name to client after every 2 seconds.

package com.dineshsawant.jettywebsocket.controller;

import com.github.javafaker.Faker;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class RandomNamesWebSocketHandler extends TextWebSocketHandler {
    private Set<WebSocketSession> listenerSessions = new CopyOnWriteArraySet<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        listenerSessions.add(session);
        System.out.println("WebSocket Connection Established");
    }

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        System.out.println("TextMessage Received = " + message.getPayload());
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        listenerSessions.remove(session);
        System.out.println("WebSocket Connection Closed");
    }

    private void sendRandomName(WebSocketSession s, String message) {
        try {
            s.sendMessage(new TextMessage(message));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

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

3. Write Client for your WebSocket

Let’s create simple controller in Spring, which serves static html file.

package com.dineshsawant.jettywebsocket.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@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.js is getting loaded from resources/static/js directory.

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Demo : Random Number Generator</title>
  <script src="/js/websocket.js"></script>
  <style>
      #holder {
          font-family: consolas;
      }


  </style>
</head>
<body>
<div id ="holder">
  Waiting for Websocket...
</div>
</body>
</html>

In websocket.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 will get appended on page.

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


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

  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.

.
|-- HELP.md
|-- build.gradle
|-- gradle
|   `-- wrapper
|       |-- gradle-wrapper.jar
|       `-- gradle-wrapper.properties
|-- gradlew
|-- gradlew.bat
|-- settings.gradle
`-- src
    |-- main
    |   |-- java
    |   |   `-- com
    |   |       `-- dineshsawant
    |   |           `-- jettywebsocket
    |   |               |-- JettyWebsocketApplication.java
    |   |               |-- config
    |   |               |   `-- WebSocketConfig.java
    |   |               `-- controller
    |   |                   |-- DemoController.java
    |   |                   `-- RandomNamesWebSocketHandler.java
    |   `-- resources
    |       |-- application.properties
    |       |-- static
    |       |   |-- js
    |       |   |   `-- websocket.js
    |       |   `-- randomNames.html
    |       `-- templates
    `-- test
        `-- java
            `-- com
                `-- dineshsawant
                    `-- jettywebsocket
                        `-- JettywebsocketApplicationTests.java

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

./gradlew bootRun

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

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

Output on web page will look something like this.

Waiting for Websocket...
WebSocket Opened
> Lynn Stokes
> Laney Wolff
> Theodora Turcotte
> Hector Kub I
> Myra Howell Sr.
> Miss Carlie Vandervort
> Ruthie Zulauf
> Lucy O'Hara
> Alisha Fritsch

Extra

In above code, our WebSocketHandler extends TextWebSocketHandler. To accept binary websocket messages from client, you can extend BinaryWebSocketHandler.