Use WebSockets with Spring, SockJS and Stomp
This guide walks you through building a “hello world” application that sends messages back and forth between a browser and the server. WebSocket is a very thin, lightweight layer on top of TCP, which makes it a natural place to run a subprotocol that gives the raw bytes some structure. Here we use STOMP messaging with Spring to create an interactive web application.
Note: This post has been updated for Spring 6/7. The WebSocket setup now uses
@EnableWebSocketMessageBrokerJava configuration and the modern@stomp/stompjsclient. The original 2015 version targeted Spring 4.1 with XML<websocket:message-broker>config and the legacyStomp.over()API; both are obsolete and have been replaced throughout.
How the messages flow
Before the code, it helps to hold the round trip in your head, because every section below plugs into it:
- The browser opens a SockJS connection to the
/websocketendpoint and negotiates STOMP over it. - The client publishes to a destination under
/app(the application prefix). - Spring routes that destination to a
@MessageMapping(or@SubscribeMapping) controller method. - The method’s return value is sent back through the broker to whatever the client subscribed to: a per-user
/user/...destination, or a/topicbroadcast.
Maven Dependencies
First, add the Spring messaging and WebSocket modules to the POM:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>7.0.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>7.0.8</version>
</dependency>
If you are on Spring Boot, pull in spring-boot-starter-websocket instead and let the Boot BOM manage the versions.
WebSocket Configuration
Spring 4.1 configured the broker through an XML <websocket:message-broker> element. Modern Spring replaces that with a @Configuration class annotated @EnableWebSocketMessageBroker:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/message");
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
}
}
A few things worth calling out here:
enableSimpleBroker("/topic", "/message")registers an in-memory broker for those destination prefixes. For a real deployment you would point Spring at a full broker (RabbitMQ, ActiveMQ) withenableStompBrokerRelayinstead.setApplicationDestinationPrefixes("/app")andsetUserDestinationPrefix("/user")define the prefixes the controller and client agree on below.HttpSessionHandshakeInterceptorcopies the HTTP session attributes into the WebSocket session during the handshake. By default the HTTP session is not carried into the WebSocket request, so without this interceptor the controller could not read session-scoped data.
Spring MVC Controller
@Controller
public class WebSocketController {
@MessageMapping("/authorization.action")
@SendToUser("/message/authorization")
public Message authorizationAction(
SimpMessageHeaderAccessor headerAccessor, Message message) {
String csrfToken = message.value();
Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();
boolean isCsrfTokenValid = CsrfProtector.isCsrfTokenValid(csrfToken, sessionAttributes);
return new Message("isCsrfTokenValid", Boolean.toString(isCsrfTokenValid));
}
@SubscribeMapping("/getRealTimeJudgeResult.action/{submissionId}")
public Message getRealTimeJudgeResultAction(@DestinationVariable long submissionId) {
return new Message("Key", "Value # " + submissionId);
}
/* Payload exchanged over STOMP, serialized to/from JSON by Jackson. */
record Message(String key, String value) {}
}
The payload is a plain Java record: STOMP frames travel as JSON, so Jackson reads and writes the key/value components directly, and there is no need for the old Serializable plumbing.
What each annotation does
SimpMessageHeaderAccessorgives you the WebSocket session, including the HTTP session attributes copied in by the handshake interceptor. That is howauthorizationActionreachesgetSessionAttributes().@SendToUserroutes the return value to a single subscriber’s session-scoped destination. It only works becausesetUserDestinationPrefix("/user")is configured above.@SubscribeMappingreturns a value directly in response to a subscription, skipping the broker. It is handy for “fetch once when the client subscribes” data rather than ongoing broadcasts.@DestinationVariablebinds a path segment of the destination ({submissionId}) to a method argument, just like@PathVariabledoes for HTTP routes.
Client: SockJS and STOMP
On the browser side, use SockJS for the transport and the @stomp/stompjs Client for the protocol:
import { Client } from "@stomp/stompjs";
import SockJS from "sockjs-client";
const stompClient = new Client({
// SockJS provides a fallback for browsers without native WebSocket support.
webSocketFactory: () => new SockJS("/websocket"),
onConnect: () => {
displayWebSocketConnectionStatus(true);
// Send the CSRF token for the server to validate.
stompClient.publish({
destination: "/app/authorization.action",
body: JSON.stringify({ key: "csrfToken", value: "${csrfToken}" }),
});
// Receive the per-user reply.
stompClient.subscribe("/user/message/authorization", (message) => {
console.log(JSON.parse(message.body));
});
// Pull a one-off result on subscribe.
stompClient.subscribe(
"/app/getRealTimeJudgeResult.action/${submissionId}",
(message) => console.log(JSON.parse(message.body))
);
},
onWebSocketClose: () => displayWebSocketConnectionStatus(false),
});
stompClient.activate();
Each client destination lines up with a server handler, which is where the two halves of this post meet:
publishto/app/authorization.actionis dispatched to@MessageMapping("/authorization.action"); Spring strips the/appapplication prefix before matching.subscribeto/user/message/authorizationreceives whatever@SendToUser("/message/authorization")returns, scoped to this session by the/userprefix.subscribeto/app/getRealTimeJudgeResult.action/${submissionId}is answered by@SubscribeMapping, with the JSP-rendered${submissionId}flowing into the@DestinationVariable.
Conclusion
That is the full round trip: a single SockJS connection, a STOMP layer on top, and a controller whose annotations mirror the client’s publish/subscribe destinations. From here you can swap the in-memory broker for a real one, broadcast to /topic instead of a single user, or push server-side events as they happen. For the complete configuration and the broker-relay options, see the references below.
The Disqus comment system is loading ...
If the message does not appear, please check your Disqus configuration.