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.