This part of the reference documentation covers Spring Framework’s support for WebSocket-style messaging in web applications including use of STOMP as an application level WebSocket sub-protocol.
Section 20.1, “Introduction” establishes a frame of mind in which to think about WebSocket, covering adoption challenges, design considerations, and thoughts on when it is a good fit.
Section 20.2, “WebSocket Server” reviews the Spring WebSocket API on the server-side while Section 20.3, “Fallback Options” explains the SockJS protocol and shows how to configure and use it.
Section 20.4.1, “Overview of STOMP” introduces the STOMP messaging protocol. Section 20.4.2, “Enable STOMP (over WebSocket)” demonstrates how to configure STOMP support in Spring. Section 20.4.3, “Overview of STOMP Message Handling” explains how to use it including writing annotated message handling methods, sending messages, choosing message broker options, as well as working with the special "user" destinations. Finally the section called “Testing Message Handling Controllers” lists three approaches to testing STOMP/WebSocket applications.
The WebSocket protocol RFC 6455 defines an important new capability for web applications: full-duplex, two-way communication between client and server. It is an exciting new capability on the heels of a long history of techniques to make the web more interactive including Java applets, XMLHttpRequest, Adobe Flash, ActiveXObject, various Comet techniques, server-sent events, and others.
A proper introduction of the WebSocket protocol is beyond the scope of this document. At a minimum however it’s important to understand that HTTP is used only for the initial handshake, which relies on a mechanism built into HTTP to request a protocol upgrade (or in this case a protocol switch) to which the server can respond with HTTP status 101 (switching protocols) if it agrees. Assuming the handshake succeeds the TCP socket underlying the HTTP upgrade request remains open and both client and server can use it to send messages to each other.
Spring Framework 4 includes a new spring-websocket
module with comprehensive
WebSocket support. It is compatible with the Java WebSocket API standard
(JSR-356)
and also provides additional value-add as explained in the rest of the introduction.
An important challenge to adoption is the lack of support for WebSocket in some browsers. Notably the first Internet Explorer version to support WebSocket is version 10 (see http://caniuse.com/websockets for support by browser versions). Furthermore, some restrictive proxies may be configured in ways that either preclude the attempt to do HTTP upgrade or otherwise break connection after some time because it has remained opened for too long. A good overview on this topic from Peter Lubbers is available in the InfoQ article "How HTML5 Web Sockets Interact With Proxy Servers".
Therefore to build a WebSocket application today, fallback options are required to simulate the WebSocket API where necessary. Spring Framework provides such transparent fallback options based on the SockJS protocol. These options can be enabled through configuration and do not require modifying the application otherwise.
Aside from short-to-midterm adoption challenges, using WebSocket brings up important design considerations that are important to recognize early on, especially in contrast to what we know about building web applications today.
Today REST is a widely accepted, understood, and supported architecture for building web applications. It is an architecture that relies on having many URLs (nouns), a handful of HTTP methods (verbs), and other principles such as using hypermedia (links), remaining stateless, etc.
By contrast a WebSocket application may use a single URL only for the initial HTTP handshake. All messages thereafter share and flow on the same TCP connection. This points to an entirely different, asynchronous, event-driven, messaging architecture. One that is much closer to traditional messaging applications (e.g. JMS, AMQP).
Spring Framework 4 includes a new spring-messaging
module with key
abstractions from the
Spring Integration project
such as Message
, MessageChannel
, MessageHandler
and others that can serve as
a foundation for such a messaging architecture. The module also includes a
set of annotations for mapping messages to methods, similar to the Spring MVC
annotation based programming model.
WebSocket does imply a messaging architecture but does not mandate the use of any specific messaging protocol. It is a very thin layer over TCP that transforms a stream of bytes into a stream of messages (either text or binary) and not much more. It is up to applications to interpret the meaning of a message.
Unlike HTTP, which is an application-level protocol, in the WebSocket protocol there is simply not enough information in an incoming message for a framework or container to know how to route it or process it. Therefore WebSocket is arguably too low level for anything but a very trivial application. It can be done, but it will likely lead to creating a framework on top. This is comparable to how most web applications today are written using a web framework rather than the Servlet API alone.
For this reason the WebSocket RFC defines the use of
sub-protocols.
During the handshake, client and server can use the header
Sec-WebSocket-Protocol
to agree on a sub-protocol, i.e. a higher, application-level
protocol to use. The use of a sub-protocol is not required, but
even if not used, applications will still need to choose a message
format that both client and server can understand. That format can be custom,
framework-specific, or a standard messaging protocol.
Spring Framework provides support for using STOMP — a simple, messaging protocol originally created for use in scripting languages with frames inspired by HTTP. STOMP is widely support and well suited for use over WebSocket and over the web.
With all the design considerations surrounding the use of WebSocket, it is reasonable to ask when is it appropriate to use?
The best fit for WebSocket is in web applications where client and server need to exchange events at high frequency and at low latency. Prime candidates include but are not limited to applications in finance, games, collaboration, and others. Such applications are both very sensitive to time delays and also need to exchange a wide variety of messages at high frequency.
For other application types, however, this may not be the case. For example, a news or social feed that shows breaking news as they become available may be perfectly okay with simple polling once every few minutes. Here latency is important, but it is acceptable if the news takes a few minutes to appear.
Even in cases where latency is crucial, if the volume of messages is relatively low (e.g. monitoring network failures) the use of long polling should be considered as a relatively simple alternative that works reliably and is comparable by efficiency (again assuming the volume of messages is relatively low).
It is the combination of both low latency and high frequency of messages that can make the use of the WebSocket protocol critical. Even in such applications, the choice remains whether all client-server communication should be done through WebSocket messages as opposed to using HTTP and REST? The answer is going to vary by application, however, it is likely that some functionality may be exposed over both WebSocket and as a REST API in order to provide clients with alternatives. Furthermore, a REST API call may need to broadcast a message to interested clients connected via WebSocket.
Spring Framework allows @Controller
classes to have both
HTTP request handling and WebSocket message handling methods.
Furthermore, a Spring MVC request handling method, or any application
method for that matter, can easily broadcast a message to all interested
WebSocket clients or to a specific user.
The Spring Framework provides a WebSocket API designed to adapt to various WebSocket engines. For example, it runs on JSR-356 runtimes such as Tomcat (7.0.47+) and GlassFish (4.0+) but can also adapt to other WebSocket runtimes such as the Jetty (9.0+) native WebSocket support.
Note | |
---|---|
As explained in the introduction, direct use of a WebSocket API is too low level for applications — until assumptions are made about the format of a message there is little a framework can do to interpret messages or route them via annotations. This is why applications should consider using a sub-protocol and Spring’s STOMP over WebSocket support. When using a higher level protocol, the details of the WebSocket API become less relevant, much like the details of TCP communication are not exposed to applications when using HTTP. Nevertheless this section covers the details of using WebSocket directly. |
Creating a WebSocket server is as simple as implementing WebSocketHandler
or more
likely extending either TextWebSocketHandler
or BinaryWebSocketHandler
:
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.TextMessage; public class MyHandler extends TextWebSocketHandler { @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // ... } }
There is dedicated WebSocket Java-config and XML namespace support for mapping the above WebSocket handler at a specific URL:
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
XML configuration equivalent:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
The above is for use in Spring MVC applications and should be included in the
configuration of a DispatcherServlet. However, Spring’s WebSocket
support does not depend on Spring MVC. It is relatively simple to integrate a WebSocketHandler
into other HTTP serving environments with the help of
WebSocketHttpRequestHandler.
The easiest way to customize the initial HTTP WebSocket handshake request is through
a HandshakeInterceptor
, which exposes "before" and "after" the handshake methods.
Such an interceptor can be used to preclude the handshake or to make any attributes
available to the WebSocketSession
. For example, there is a built-in interceptor
for passing HTTP session attributes to the WebSocket session:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyHandler(), "/myHandler") .addInterceptors(new HttpSessionHandshakeInterceptor()); } }
And the XML configuration equivalent:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> <websocket:handshake-interceptors> <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/> </websocket:handshake-interceptors> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
A more advanced option is to extend the DefaultHandshakeHandler
that performs
the steps of the WebSocket handshake, including validating the client origin,
negotiating a sub-protocol, and others. An application may also need to use this
option if it needs to configure a custom RequestUpgradeStrategy
in order to
adapt to a WebSocket server engine and version that is not yet supported
(also see Section 20.2.4, “Deployment Considerations” for more on this subject).
Both the Java-config and XML namespace make it possible to configure a custom
HandshakeHandler
.
Spring provides a WebSocketHandlerDecorator
base class that can be used to decorate
a WebSocketHandler
with additional behavior. Logging and exception handling
implementations are provided and added by default when using the WebSocket Java-config
or XML namespace. The ExceptionWebSocketHandlerDecorator
catches all uncaught
exceptions arising from any WebSocketHandler method and closes the WebSocket
session with status 1011
that indicates a server error.
The Spring WebSocket API is easy to integrate into a Spring MVC application where
the DispatcherServlet
serves both HTTP WebSocket handshake as well as other
HTTP requests. It is also easy to integrate into other HTTP processing scenarios
by invoking WebSocketHttpRequestHandler
. This is convenient and easy to
understand. However, special considerations apply with regards to JSR-356 runtimes.
The Java WebSocket API (JSR-356) provides two deployment mechanisms. The first
involves a Servlet container classpath scan (Servlet 3 feature) at startup; and
the other is a registration API to use at Servlet container initialization.
Neither of these mechanism make it possible to use a single "front controller"
for all HTTP processing — including WebSocket handshake and all other HTTP
requests — such as Spring MVC’s DispatcherServlet
.
This is a significant limitation of JSR-356 that Spring’s WebSocket support
addresses by providing a server-specific RequestUpgradeStrategy
even when
running in a JSR-356 runtime. At present such support is available on
Tomcat 7.0.47+, Jetty 9.0+, and GlassFish 4.0+. Additional support will be
added as more WebSocket runtimes become available.
Note | |
---|---|
A request to overcome the above limitation in the Java WebSocket API has been created and can be followed at WEBSOCKET_SPEC-211. Also note that Tomcat and Jetty already provide native API alternatives that makes it easy to overcome the limitation. We are hopeful that more servers will follow their example regardless of when it is addressed in the Java WebSocket API. |
A secondary consideration is that Servlet containers with JSR-356 support
are expected to perform an SCI scan that can slow down application startup,
in some cases dramatically. If a significant impact is observed after an
upgrade to a Servlet container version with JSR-356 support, it should
be possible to selectively enable or disable web fragments (and SCI scanning)
through the use of an <absolute-ordering />
element in web.xml
:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <absolute-ordering/> </web-app>
You can then selectively enable web fragments by name, such as Spring’s own
SpringServletContainerInitializer
that provides support for the Servlet 3
Java initialization API, if required:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <absolute-ordering> <name>spring_web</name> </absolute-ordering> </web-app>
Each underlying WebSocket engine exposes configuration properties that control runtime characteristics such as the size of message buffer sizes, idle timeout, and others.
For Tomcat, WildFly, and Glassfish add a WebSocketContainerFactoryBean
to your
WebSocket Java config:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Bean public WebSocketContainerFactoryBean createWebSocketContainer() { WebSocketContainerFactoryBean container = new WebSocketContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192); container.setMaxBinaryMessageBufferSize(8192); return container; } }
or WebSocket XML namespace:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <bean class="org.springframework...WebSocketContainerFactoryBean"> <property name="maxTextMessageBufferSize" value="8192"/> <property name="maxBinaryMessageBufferSize" value="8192"/> </bean> </beans>
For Jetty, you’ll need to supply a pre-configured Jetty WebSocketServerFactory
and plug
that into Spring’s DefaultHandshakeHandler
through your WebSocket Java config:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler()); } @Bean public DefaultHandshakeHandler handshakeHandler() { WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); policy.setInputBufferSize(8192); policy.setIdleTimeout(600000); return new DefaultHandshakeHandler( new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); } }
or WebSocket XML namespace:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers> <websocket:mapping path="/echo" handler="echoHandler"/> <websocket:handshake-handler ref="handshakeHandler"/> </websocket:handlers> <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler"> <constructor-arg ref="upgradeStrategy"/> </bean> <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy"> <constructor-arg ref="serverFactory"/> </bean> <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory"> <constructor-arg> <bean class="org.eclipse.jetty...WebSocketPolicy"> <constructor-arg value="SERVER"/> <property name="inputBufferSize" value="8092"/> <property name="idleTimeout" value="600000"/> </bean> </constructor-arg> </bean> </beans>
As explained in the introduction, WebSocket is not supported in all browsers yet and may be precluded by restrictive network proxies. This is why Spring provides fallback options that emulate the WebSocket API as close as possible based on the SockJS protocol.
SockJS is easy to enable through a configuration:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").withSockJS(); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
and the XML configuration equivalent:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> <websocket:sockjs/> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>
The above is for use in Spring MVC applications and should be included in the configuration of a DispatcherServlet. However, Spring’s WebSocket and SockJS support does not depend on Spring MVC. It is relatively simple to integrate into other HTTP serving environments with the help of SockJsHttpRequestHandler.
On the browser side, applications can use the sockjs-client that emulates the W3C WebSocket API and communicates with the server to select the best transport option depending on the browser it’s running in. Review the sockjs-client page and the list of transport types supported by browser. The client also provides several configuration options, for example, to specify which transports to include.
An in-depth description of how SockJS works is beyond the scope of this document. This section summarizes a few key facts to aid with understanding. The SockJS protocol itself is defined in a test suite, with comments explaining the protocol. There is also an HTML version of that test showing comments on the right and code on the left.
The SockJS client issues HTTP requests with this URL structure:
SockJS URL.
http://host:port/{path-to-sockjs-endpoint}/{server-id}/{session-id}/{transport}
The WebSocket transport type uses a single HTTP connection to perform a WebSocket handshake and establish an actual WebSocket session. HTTP-based transports on the other hand must simulate the WebSocket API and at any time may use two HTTP connections — one for server-to-client messages, via HTTP streaming or long polling, and another for sending client messages to the server via HTTP POST.
The session id is useful with HTTP transports to associate individual HTTP requests that belong to the same SockJS session. The server id is not used in the protocol but is added to help in clustered environments.
SockJS adds a minimal amount of message framing. For example, the server can send
an "open frame" (the letter o
), a "heartbeat frame" (the letter h
), or a
"close frame" (the letter c
); while the client sends messages as a JSON-encoded
array prepended with the letter a
(e.g. a["message1","message2"]
).
By default the server sends a heartbeat frame every 25 seconds to keep proxies and loadbalancers from timing out the connection.
In the Spring Framework, server-side support for the SockJS protocol is provided through a
hierarchy of classes that implement the SockJsService
interface, while
SockJsHttpRequestHandler
integrates the service into HTTP request processing.
To implement HTTP streaming and long polling in Servlet containers (both of which require an HTTP connection to remain open longer than usual), Spring’s SockJS support relies on Servlet 3 async support.
Warning | |
---|---|
Servlet 3 async support does not expose a notification when a client disconnects, e.g. a browser tab is closed, page is refreshed, etc (see SERVLET_SPEC-44). However, the Serlvet container will usually raise an IOException on the next attempt to write to the response; at which point the SockJS session will be closed. Since the SockJsService sends a heartbeat every 25 seconds, typically a disconnected client will be detected within that time period. |
The WebSocket protocol defines two main types of messages — text and binary — but leaves their content undefined. Instead it’s expected that client and server may agree on using a sub-protocol, i.e. a higher-level protocol that defines the message content. Using a sub-protocol is optional but either way client and server both need to understand how to interpret messages.
STOMP is a simple messaging protocol originally created for scripting languages (such as Ruby, Python and Perl) to connect to enterprise message brokers. It is designed to address a subset of commonly used patterns in messaging protocols. STOMP can be used over any reliable 2-way streaming network protocol such as TCP and WebSocket.
STOMP is a frame based protocol with frames modelled on HTTP. This is the structure of a frame:
COMMAND header1:value1 header2:value2 Body^@
For example, a client can use the SEND
command to send a message or the
SUBSCRIBE
command to express interest in receiving messages. Both of these commands
require a "destination"
header that indicates where to send a message to, or likewise
what to subscribe to.
Here is an example of a client sending a request to buy stock shares:
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
Here is an example of a client subscribing to receive stock quotes:
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
Note | |
---|---|
The meaning of a destination is intentionally left opaque in the STOMP spec. It can
be any string and it’s entirely up to STOMP servers to define the semantics and
the syntax of the destinations that they support. It is very common however, for
destinations to be path-like strings where |
STOMP servers can use the MESSAGE
command to broadcast messages to all subscribers.
Here is an example of a server sending a stock quote to a subscribed client:
MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
Note | |
---|---|
It’s important to know that a server cannot send unsolicited messages.
All messages from a server must be in response to a specific client subscription
and the |
The above overview is intended to provide the most basic understanding of the STOMP protocol. It is recommended to review the protocol specification, which is easy to follow and manageable in terms of size.
The following summarizes the benefits for an application from using STOMP over WebSocket:
Most importantly the use of STOMP (vs plain WebSocket) enables the Spring Framework to provide a programming model for application-level use in the same way that Spring MVC provides a programming model based on HTTP.
The Spring Framework provides support for using STOMP over WebSocket through
the spring-messaging
and spring-websocket
modules. It’s easy to enable it.
Here is an example of configuring a STOMP WebSocket endpoint with SockJS fallback
options. The endpoint is available for clients to connect to at URL path /portfolio
:
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } // ... }
XML configuration equivalent:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd"> <websocket:message-broker> <websocket:stomp-endpoint path="/portfolio"> <websocket:sockjs/> </websocket:stomp-endpoint> ... </websocket:message-broker> </beans>
On the browser side, a client might connect as follows using stomp.js and the sockjs-client:
var socket = new SockJS("/spring-websocket-portfolio/portfolio"); var stompClient = Stomp.over(socket);
Or if connecting via WebSocket (without SockJS):
var socket = new WebSocket("/spring-websocket-portfolio/portfolio"); var stompClient = Stomp.over(socket);
When a STOMP endpoint is configured, the Spring application effectively becomes the broker to connected clients, handling incoming messages and broadcasting messages back to them. This part of the documentation describes how STOMP messages are handled within the application.
As mentioned in the introduction the
spring-messaging
module contains key abstractions from the
Spring Integration project, including
Message
, MessageChannel
, MessageHandler
and a few others.
Note | |
---|---|
Spring Integration 4 will be the first version to start using the abstractions
from the package structure of the |
MessageChannel
is a simple contract for passing messages between components without
creating tight coupling among them.
SubscribableChannel extends
it, with the ability to register subscribers; and
ExecutorSubscribableChannel
is an implementation that passes messages to subscribers in
the same thread or a different thread depending on whether it has been provided with
a java.util.concurrent.Executor
. This enables assembling message
handling flows from various components and modifying them through configuration.
The provided Java-config @EnableWebSocketMessageBroker
and XML namespace
<websocket:message-broker>
each put together a default message handling
flow for applications to use, as explained next. This flow can be modified,
customized, or extended. For example, an application can add a
ChannelInterceptor
to any message channel in order to intercept messages passing through it;
it can register additional message handling components, alternate between
synchronous and asynchronous message passing, and so on.
Incoming client STOMP messages are passed to a message channel with the name
"clientInboundChannel"
. By default the messages are routed to annotated
methods as well as to a "simple" message broker. This simple message broker
automatically records subscriptions, in-memory, and broadcasts messages as
necessary. As explained later, you can also use a full-featured message broker
(e.g. RabbitMQ, ActiveMQ, and any other broker that supports STOMP) to manage
subscriptions and broadcast messages.
Below is example configuration:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic/"); registry.setApplicationDestinationPrefixes("/app"); } }
XML configuration equivalent:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd"> <websocket:message-broker application-destination-prefix="/app"> <websocket:stomp-endpoint path="/portfolio" /> <websocket:sockjs/> </websocket:stomp-endpoint> <websocket:simple-broker prefix="/topic"/> </websocket:message-broker> </beans>
The configuration example assigns destination prefixes — /app
for filtering
messages to annotated methods and /topic
for messages to the broker. The
examples below demonstrate how this can be used.
The destination prefix should not be included in annotation mappings. For
example, this method handles messages to destination /app/greetings
:
@Controller public class GreetingController { @MessageMapping("/greetings") { public void handle(String greeting) { // ... } }
The method accepts a String extracted from the payload of the message,
possibly converted based on its content type. The method can also return a
value, which is wrapped as the payload of a new message and sent to a message
channel named "brokerChannel"
(which is used for sending messages to the broker
from within the application). The new message is sent to the same destination
as that of the client message, but with the default prefix /topic
(you can also use @SendTo
for any other target destination):
@Controller public class GreetingController { @MessageMapping("/greetings") { public String handle(String greeting) { return "[" + getTimestamp() + ": " + greeting; } }
As a result, to put it all together, a client sends a greeting message to
destination /app/greetings
. The message is routed to GreetingController
,
which enriches the greeting with a timestamp and sends a new message to the
broker with destination /topic/greetings
. The broker then broadcasts the
message to all subscribed, connected clients.
The @MessageMapping
annotation is supported on methods of @Controller
-annotated classes.
It can be used for mapping methods to path-like message destinations. It is also
possible to combine with a type-level @MessageMapping
for expressing shared
mappings across all annotated methods within a controller.
Destination mappings can contain Ant-style patterns (e.g. "/foo*", "/foo/**")
and template variables (e.g. "/foo/{id}"), which can then be accessed via
@DestinationVariable
method arguments. This should be familiar to Spring MVC
users, in fact the same AntPathMatcher
is used for matching destinations based
on patterns and for extracting template variables.
The following method arguments are supported for @MessageMapping
methods:
Message
method argument to get access to the complete message being processed.
@Payload
-annotated argument for access to the payload of a message, converted with
a org.springframework.messaging.converter.MessageConverter
.
The presence of the annotation is not required since it is assumed by default.
Payload method arguments annotated with Validation annotations (like @Validated
) will
be subject to JSR-303 validation.
@Header
-annotated arguments for access to a specific header value along with
type conversion using an org.springframework.core.convert.converter.Converter
if necessary.
@Headers
-annotated method argument that must also be assignable to java.util.Map
for access to all headers in the message.
MessageHeaders
method argument for getting access to a map of all headers.
MessageHeaderAccessor
, SimpMessageHeaderAccessor
, or StompHeaderAccessor
for access to headers via typed accessor methods.
@DestinationVariable
-annotated arguments for access to template
variables extracted from the message destination. Values will be converted to
the declared method argument type as necessary.
java.security.Principal
method arguments reflecting the user logged in at
the time of the WebSocket HTTP handshake.
The return value from an @MessageMapping
method is converted with a
org.springframework.messaging.converter.MessageConverter
and used as the body
of a new message that is then sent, by default, to the "brokerChannel"
with
the same destination as the client message but using the prefix "/topic" by
default. An @SendTo
message level annotation can be used to specify any
other destination instead.
An @SubscribeMapping
annotation can also be used to map subscription requests
to @Controller
methods. It is supported on the method level, but can also be
combined with a type level @MessageMapping
annotation that expresses shared
mappings across all message handling methods within the same controller.
By default the return value from an @SubscribeMapping
method is sent as a
message directly back to the connected client and does not pass through the
broker. This is useful for implementing request-reply message interactions; for
example, to fetch application data when the application UI is being initialized.
Or alternatively an @SubscribeMapping
method can be annotated with @SendTo
in which case the resulting message is sent to the "brokerChannel"
using
the specified target destination.
What if you wanted to send messages to connected clients from any part of the
application? Any application component can send messages to the "brokerChannel"
.
The easist way to do that is to have a SimpMessagingTemplate
injected, and
use it to send messages. Typically it should be easy to have it injected by
type, for example:
@Controller public class GreetingController { private SimpMessagingTemplate template; @Autowired public GreetingController(SimpMessagingTemplate template) { this.template = template; } @RequestMapping(value="/greetings", method=POST) public void greet(String greeting) { String text = "[" + getTimestamp() + "]:" + greeting; this.template.convertAndSend("/topic/greetings", text); } }
But it can also be qualified by its name "brokerMessagingTemplate" if another bean of the same type exists.
The built-in, simple, message broker handles subscription requests from clients, stores them in memory, and broadcasts messages to connected clients with matching destinations. The broker supports path-like destinations, including subscriptions to Ant-style destination patterns.
The simple broker is great for getting started but supports only a subset of STOMP commands (e.g. no acks, receipts, etc), relies on a simple message sending loop, and is not suitable for clustering.
Instead, applications can use a full-featured message broker and use it for managing client subscriptions and broadcasting messages; while annotated methods can still be used for application processing. In other words, all else remains the same, except a full-featured broker replaces the simple broker.
Check your message broker STOMP documentation (e.g. RabbitMQ, ActiveMQ), install and run the broker with STOMP support enabled. Then enable the STOMP broker relay in the Spring configuration as an alternative to the simple broker.
Below is example configuration that enables use of a full-featured broker:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/topic/", "/queue/"); registry.setApplicationDestinationPrefixes("/app"); } }
XML configuration equivalent:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd"> <websocket:message-broker application-destination-prefix="/app"> <websocket:stomp-endpoint path="/portfolio" /> <websocket:sockjs/> </websocket:stomp-endpoint> <websocket:stomp-broker-relay prefix="/topic,/queue" /> </websocket:message-broker> </beans>
The STOMP "broker relay" from the above configuration manages TCP connections to the external broker, and forwards messages with matching destination prefixes to it. Likewise, any messages received from the external broker are matched and routed to connected clients.
In effect, messages are now broadcast through a full-featured, robust and scalable message broker.
An application can also send messages targeting a specific user.
To do so a user must first be authenticated. Although the STOMP CONNECT
frame
has authentication headers when used over WebSocket, it makes more sense to use
the same HTTP-based authentication already used to secure the application.
For example, an application can use Spring Security as usual to protect HTTP URLs,
including paths to STOMP WebSocket endpoint(s). The authenticanted user, based
on the value returned from HttpServletRequest.getUserPrincipal()
, will be
saved in the WebSocket session and subsequently added as a header named user
to all incoming messages for that STOMP/WebSocket session.
Spring’s STOMP support recognizes destinations prefixed with /user/
.
For example, a client can subscribe to destination /user/position-updates
.
This destination will be handled by the UserDestinationMessageHandler
and
transformed into a destination unique to the user’s session,
e.g. /user/position-updates-123
. This provides the convenience of subscribing
to a generically named destination, while also ensuring that it doesn’t "collide"
with any other user that also subscribes to /user/position-updates
in order to receive stock position updates unique to them.
On the sending side, messages can be sent to a destination such as
/user/{username}/position-updates
, which in turn will be translated
by the UserDestinationMessageHandler
into the same unique destination
belonging to the specified user name.
This allows any component within the application to send messages to a specific user without necessarily knowing anything more than their name and a generic destination.
When this is used with an external message broker, check the broker documentation
on how to manage inactive queues, so that when the user session is over, all
unique user queues are removed. For example, RabbitMQ creates auto-delete queues
when destinations like /exchange/amq.direct/position-updates
are used.
So in that case the client could subscribe to /user/exchange/amq.direct/position-updates
.
ActiveMQ has configuration options
for purging inactive destinations.
There are two main approaches to testing applications using Spring’s STOMP over WebSocket support. The first is to write server-side tests verifying the functionality of controllers and their annotated message handling methods. The second is to write full end-to-end tests that involve running a client and a server.
The two approaches are not mutually exclusive. On the contrary each has a place in an overall test strategy. Server-side tests are more focused and easier to write and maintain. End-to-end integration tests on the other hand are more complete and test much more but they’re also more involved to write and maintain.
The simplest form of server-side tests is to write controller unit tests. However this is not useful enough since much of what a controller does depends on its annotations. Pure unit tests simply can’t test that.
Ideally controllers under test should be invoked as they are at runtime, much like the approach to testing controllers handling HTTP requests using the Spring MVC Test framework. i.e. without running a Servlet container but relying on the Spring Framework to invoke the annotated controllers. Just like with Spring MVC Test here there are two two possible alternatives, either using a "context-based" or "standalone" setup:
SimpAnnotationMethodMessageHandler
) and pass messages for
controllers directly to it.
Both of these setup scenarios are demonstrated in the tests for the stock portfolio sample application.
The second approach is to create end-to-end integration tests. For that you will need to run a WebSocket server in embedded mode and connect to it as a WebSocket client sending WebSocket messages containing STOMP frames. The tests for the stock portfolio sample application also demonstrate this approach using Tomcat as the embedded WebSocket server and a simple STOMP client for test purposes.