π 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.