STOMP

The WebSocket protocol defines two types of messages (text and binary), but their content is undefined. The protocol defines a mechanism for client and server to negotiate a sub-protocol (that is, a higher-level messaging protocol) to use on top of WebSocket to define what kind of messages each can send, what the format is, the content of each message, and so on. The use of a sub-protocol is optional but, either way, the client and the server need to agree on some protocol that defines message content.

Overview

STOMP (Simple Text Oriented Messaging Protocol) was originally created for scripting languages (such as Ruby, Python, and Perl) to connect to enterprise message brokers. It is designed to address a minimal subset of commonly used messaging patterns. STOMP can be used over any reliable two-way streaming network protocol, such as TCP and WebSocket. Although STOMP is a text-oriented protocol, message payloads can be either text or binary.

STOMP is a frame-based protocol whose frames are modeled on HTTP. The following listing shows the structure of a STOMP frame:

COMMAND
header1:value1
header2:value2

Body^@

Clients can use the SEND or SUBSCRIBE commands to send or subscribe for messages, along with a destination header that describes what the message is about and who should receive it. This enables a simple publish-subscribe mechanism that you can use to send messages through the broker to other connected clients or to send messages to the server to request that some work be performed.

When you use Spring’s STOMP support, the Spring WebSocket application acts as the STOMP broker to clients. Messages are routed to @Controller message-handling methods or to a simple in-memory broker that keeps track of subscriptions and broadcasts messages to subscribed users. You can also configure Spring to work with a dedicated STOMP broker (such as RabbitMQ, ActiveMQ, and others) for the actual broadcasting of messages. In that case, Spring maintains TCP connections to the broker, relays messages to it, and passes messages from it down to connected WebSocket clients. Thus, Spring web applications can rely on unified HTTP-based security, common validation, and a familiar programming model for message handling.

The following example shows a client subscribing to receive stock quotes, which the server may emit periodically (for example, via a scheduled task that sends messages through a SimpMessagingTemplate to the broker):

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

The following example shows a client that sends a trade request, which the server can handle through an @MessageMapping method:

SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

After the execution, the server can broadcast a trade confirmation message and details down to the client.

The meaning of a destination is intentionally left opaque in the STOMP spec. It can be any string, and it is 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 /topic/.. implies publish-subscribe (one-to-many) and /queue/ implies point-to-point (one-to-one) message exchanges.

STOMP servers can use the MESSAGE command to broadcast messages to all subscribers. The following example shows 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}^@

A server cannot send unsolicited messages. All messages from a server must be in response to a specific client subscription, and the subscription-id header of the server message must match the id header of the client subscription.

The preceding overview is intended to provide the most basic understanding of the STOMP protocol. We recommended reviewing the protocol specification in full.

Benefits

Using STOMP as a sub-protocol lets the Spring Framework and Spring Security provide a richer programming model versus using raw WebSockets. The same point can be made about HTTP versus raw TCP and how it lets Spring MVC and other web frameworks provide rich functionality. The following is a list of benefits:

  • No need to invent a custom messaging protocol and message format.

  • STOMP clients, including a Java client in the Spring Framework, are available.

  • You can (optionally) use message brokers (such as RabbitMQ, ActiveMQ, and others) to manage subscriptions and broadcast messages.

  • Application logic can be organized in any number of @Controller instances and messages can be routed to them based on the STOMP destination header versus handling raw WebSocket messages with a single WebSocketHandler for a given connection.

  • You can use Spring Security to secure messages based on STOMP destinations and message types.

Enable STOMP

STOMP over WebSocket support is available in the spring-messaging and spring-websocket modules. Once you have those dependencies, you can expose a STOMP endpoints, over WebSocket with websocket-fallback, as the following example shows:

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();  (1)
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		config.setApplicationDestinationPrefixes("/app"); (2)
		config.enableSimpleBroker("/topic", "/queue"); (3)
	}
}
1 /portfolio is the HTTP URL for the endpoint to which a WebSocket (or SockJS) client needs to connect for the WebSocket handshake.
2 STOMP messages whose destination header begins with /app are routed to @MessageMapping methods in @Controller classes.
3 Use the built-in message broker for subscriptions and broadcasting and route messages whose destination header begins with /topic `or `/queue to the broker.

The following example shows the XML configuration equivalent of the preceding example:

<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
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:message-broker application-destination-prefix="/app">
		<websocket:stomp-endpoint path="/portfolio">
			<websocket:sockjs/>
		</websocket:stomp-endpoint>
		<websocket:simple-broker prefix="/topic, /queue"/>
	</websocket:message-broker>

</beans>
For the built-in simple broker, the /topic and /queue prefixes do not have any special meaning. They are merely a convention to differentiate between pub-sub versus point-to-point messaging (that is, many subscribers versus one consumer). When you use an external broker, check the STOMP page of the broker to understand what kind of STOMP destinations and prefixes it supports.

To connect from a browser, for SockJS, you can use the sockjs-client. For STOMP, many applications have used the jmesnil/stomp-websocket library (also known as stomp.js), which is feature-complete and has been used in production for years but is no longer maintained. At present the JSteunou/webstomp-client is the most actively maintained and evolving successor of that library. The following example code is based on it:

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) {
}

Alternatively, if you connect through WebSocket (without SockJS), you can use the following code:

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
}

Note that stompClient in the preceding example does not need to specify login and passcode headers. Even if it did, they would be ignored (or, rather, overridden) on the server side. See websocket-stomp-handle-broker-relay-configure and websocket-stomp-authentication for more information on authentication.

For more example code see:

WebSocket Server

To configure the underlying WebSocket server, the information in websocket-server-runtime-configuration applies. For Jetty, however you need to set the HandshakeHandler and WebSocketPolicy through the StompEndpointRegistry:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/portfolio").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)));
	}
}

Flow of Messages

Once a STOMP endpoint is exposed, the Spring application becomes a STOMP broker for connected clients. This section describes the flow of messages on the server side.

The spring-messaging module contains foundational support for messaging applications that originated in Spring Integration and was later extracted and incorporated into the Spring Framework for broader use across many Spring projects and application scenarios. The following list briefly describes a few of the available messaging abstractions:

Both the Java configuration (that is, @EnableWebSocketMessageBroker) and the XML namespace configuration (that is,<websocket:message-broker>) use the preceding components to assemble a message workflow. The following diagram shows the components used when the simple built-in message broker is enabled:

message flow simple broker

The preceding diagram shows three message channels:

  • clientInboundChannel: For passing messages received from WebSocket clients.

  • clientOutboundChannel: For sending server messages to WebSocket clients.

  • brokerChannel: For sending messages to the message broker from within server-side application code.

The next diagram shows the components used when an external broker (such as RabbitMQ) is configured for managing subscriptions and broadcasting messages:

message flow broker relay

The main difference between the two preceding diagrams is the use of the “broker relay” for passing messages up to the external STOMP broker over TCP and for passing messages down from the broker to subscribed clients.

When messages are received from a WebSocket connection, they are decoded to STOMP frames, turned into a Spring Message representation, and sent to the clientInboundChannel for further processing. For example, STOMP messages whose destination headers start with /app may be routed to @MessageMapping methods in annotated controllers, while /topic and /queue messages may be routed directly to the message broker.

An annotated @Controller that handles a STOMP message from a client may send a message to the message broker through the brokerChannel, and the broker broadcasts the message to matching subscribers through the clientOutboundChannel. The same controller can also do the same in response to HTTP requests, so a client can perform an HTTP POST, and then a @PostMapping method can send a message to the message broker to broadcast to subscribed clients.

We can trace the flow through a simple example. Consider the following example, which sets up a server:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/portfolio");
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.setApplicationDestinationPrefixes("/app");
		registry.enableSimpleBroker("/topic");
	}
}

@Controller
public class GreetingController {

	@MessageMapping("/greeting") {
	public String handle(String greeting) {
		return "[" + getTimestamp() + ": " + greeting;
	}
}

The preceding example supports the following flow:

  1. The client connects to http://localhost:8080/portfolio and, once a WebSocket connection is established, STOMP frames begin to flow on it.

  2. The client sends a SUBSCRIBE frame with a destination header of /topic/greeting. Once received and decoded, the message is sent to the clientInboundChannel and is then routed to the message broker, which stores the client subscription.

  3. The client sends a aSEND frame to /app/greeting. The /app prefix helps to route it to annotated controllers. After the /app prefix is stripped, the remaining /greeting part of the destination is mapped to the @MessageMapping method in GreetingController.

  4. The value returned from GreetingController is turned into a Spring Message with a payload based on the return value and a default destination header of /topic/greeting (derived from the input destination with /app replaced by /topic). The resulting message is sent to the brokerChannel and handled by the message broker.

  5. The message broker finds all matching subscribers and sends a MESSAGE frame to each one through the clientOutboundChannel, from where messages are encoded as STOMP frames and sent on the WebSocket connection.

The next section provides more details on annotated methods, including the kinds of arguments and return values that are supported.

Annotated Controllers

Applications can use annotated @Controller classes to handle messages from clients. Such classes can declare @MessageMapping, @SubscribeMapping, and @ExceptionHandler methods, as described in the following topics:

@MessageMapping

You can use @MessageMapping to annotate methods that route messages based on their destination. It is supported at the method level as well as at the type level. At the type level, @MessageMapping is used to express shared mappings across all methods in a controller.

By default, the mapping values are Ant-style path patterns (for example /thing*, /thing/**), including support for template variables (for example, /thing/{id}). The values can be referenced through @DestinationVariable method arguments. Applications can also switch to a dot-separated destination convention for mappings, as explained in websocket-stomp-destination-separator.

Supported Method Arguments

The following table describes the method arguments:

Method argument Description

Message

For access to the complete message.

MessageHeaders

For access to the headers within the Message.

MessageHeaderAccessor, SimpMessageHeaderAccessor, and StompHeaderAccessor

For access to the headers through typed accessor methods.

@Payload

For access to the payload of the message, converted (for example, from JSON) by a configured MessageConverter.

The presence of this annotation is not required since it is, by default, assumed if no other argument is matched.

You can annotate payload arguments with @javax.validation.Valid or Spring’s @Validated, to have the payload arguments be automatically validated.

@Header

For access to a specific header value — along with type conversion using an org.springframework.core.convert.converter.Converter, if necessary.

@Headers

For access to all headers in the message. This argument must be assignable to java.util.Map.

@DestinationVariable

For access to template variables extracted from the message destination. Values are converted to the declared method argument type as necessary.

java.security.Principal

Reflects the user logged in at the time of the WebSocket HTTP handshake.

Return Values

By default, the return value from a @MessageMapping method is serialized to a payload through a matching MessageConverter and sent as a Message to the brokerChannel, from where it is broadcast to subscribers. The destination of the outbound message is the same as that of the inbound message but prefixed with /topic.

You can use the @SendTo and @SendToUser annotations to customize the destination of the output message. @SendTo is used to customize the target destination or to specify multiple destinations. @SendToUser is used to direct the output message to only the user associated with the input message. See websocket-stomp-user-destination.

You can use both @SendTo and @SendToUser at the same time on the same method, and both are supported at the class level, in which case they act as a default for methods in the class. However, keep in mind that any method-level @SendTo or @SendToUser annotations override any such annotations at the class level.

Messages can be handled asynchronously and a @MessageMapping method can return ListenableFuture, CompletableFuture, or CompletionStage.

Note that @SendTo and @SendToUser are merely a convenience that amounts to using the SimpMessagingTemplate to send messages. If necessary, for more advanced scenarios, @MessageMapping methods can fall back on using the SimpMessagingTemplate directly. This can be done instead of, or possibly in addition to, returning a value. See websocket-stomp-handle-send.

@SubscribeMapping

@SubscribeMapping is similar to @MessageMapping but narrows the mapping to subscription messages only. It supports the same method arguments as @MessageMapping. However for the return value, by default, a message is sent directly to the client (through clientOutboundChannel, in response to the subscription) and not to the broker (through brokerChannel, as a broadcast to matching subscriptions). Adding @SendTo or @SendToUser overrides this behavior and sends to the broker instead.

When is this useful? Assume that the broker is mapped to /topic and /queue, while application controllers are mapped to /app. In this setup, the broker stores all subscriptions to /topic and /queue that are intended for repeated broadcasts, and there is no need for the application to get involved. A client could also subscribe to some /app destination, and a controller could return a value in response to that subscription without involving the broker without storing or using the subscription again (effectively a one-time request-reply exchange). One use case for this is populating a UI with initial data on startup.

When is this not useful? Do not try to map broker and controllers to the same destination prefix unless you want both to independently process messages, including subscriptions, for some reason. Inbound messages are handled in parallel. There are no guarantees whether a broker or a controller processes a given message first. If the goal is to be notified when a subscription is stored and ready for broadcasts, a client should ask for a receipt if the server supports it (simple broker does not). For example, with the Java STOMP client, you could do the following to add a receipt:

@Autowired
private TaskScheduler messageBrokerTaskScheduler;

// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);

// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
	// Subscription ready...
});

A server side option is to register an ExecutorChannelInterceptor on the brokerChannel and implement the afterMessageHandled method that is invoked after messages, including subscriptions, have been handled.

@MessageExceptionHandler

An application can use @MessageExceptionHandler methods to handle exceptions from @MessageMapping methods. You can declare exceptions in the annotation itself or through a method argument if you want to get access to the exception instance. The following example declares an exception through a method argument:

@Controller
public class MyController {

	// ...

	@MessageExceptionHandler
	public ApplicationError handleException(MyException exception) {
		// ...
		return appError;
	}
}

@MessageExceptionHandler methods support flexible method signatures and support the same method argument types and return values as @MessageMapping methods.

Typically, @MessageExceptionHandler methods apply within the @Controller class (or class hierarchy) in which they are declared. If you want such methods to apply more globally (across controllers), you can declare them in a class marked with @ControllerAdvice. This is comparable to the similar support available in Spring MVC.

Sending Messages

What if you want to send messages to connected clients from any part of the application? Any application component can send messages to the brokerChannel. The easiest way to do so is to inject a SimpMessagingTemplate and use it to send messages. Typically, you would inject it by type, as the following example shows:

@Controller
public class GreetingController {

	private SimpMessagingTemplate template;

	@Autowired
	public GreetingController(SimpMessagingTemplate template) {
		this.template = template;
	}

	@RequestMapping(path="/greetings", method=POST)
	public void greet(String greeting) {
		String text = "[" + getTimestamp() + "]:" + greeting;
		this.template.convertAndSend("/topic/greetings", text);
	}

}

However, you can also qualify it by its name (brokerMessagingTemplate), if another bean of the same type exists.

Simple Broker

The built-in simple message broker handles subscription requests from clients, stores them in memory, and broadcasts messages to connected clients that have matching destinations. The broker supports path-like destinations, including subscriptions to Ant-style destination patterns.

Applications can also use dot-separated (rather than slash-separated) destinations. See websocket-stomp-destination-separator.

If configured with a task scheduler, the simple broker supports STOMP heartbeats. For that, you can declare your own scheduler or use the one that is automatically declared and used internally. The following example shows how to declare your own scheduler:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	private TaskScheduler messageBrokerTaskScheduler;

	@Autowired
	public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) {
		this.messageBrokerTaskScheduler = taskScheduler;
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {

		registry.enableSimpleBroker("/queue/", "/topic/")
				.setHeartbeatValue(new long[] {10000, 20000})
				.setTaskScheduler(this.messageBrokerTaskScheduler);

		// ...
	}
}

External Broker

The simple broker is great for getting started but supports only a subset of STOMP commands (it does not support acks, receipts, and some other features), relies on a simple message-sending loop, and is not suitable for clustering. As an alternative, you can upgrade your applications to use a full-featured message broker.

See the STOMP documentation for your message broker of choice (such as RabbitMQ, ActiveMQ, and others), install the broker, and run it with STOMP support enabled. Then you can enable the STOMP broker relay (instead of the simple broker) in the Spring configuration.

The following example configuration enables 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");
	}

}

The following example shows the XML configuration equivalent of the preceding example:

<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
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.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 in the preceding configuration is a Spring MessageHandler that handles messages by forwarding them to an external message broker. To do so, it establishes TCP connections to the broker, forwards all messages to it, and then forwards all messages received from the broker to clients through their WebSocket sessions. Essentially, it acts as a “relay” that forwards messages in both directions.

Add io.projectreactor.netty:reactor-netty and io.netty:netty-all dependencies to your project for TCP connection management.

Furthermore, application components (such as HTTP request handling methods, business services, and others) can also send messages to the broker relay, as described in websocket-stomp-handle-send, to broadcast messages to subscribed WebSocket clients.

In effect, the broker relay enables robust and scalable message broadcasting.

Connecting to a Broker

A STOMP broker relay maintains a single “system” TCP connection to the broker. This connection is used for messages originating from the server-side application only, not for receiving messages. You can configure the STOMP credentials (that is, the STOMP frame login and passcode headers) for this connection. This is exposed in both the XML namespace and Java configuration as the systemLogin and systemPasscode properties with default values of guest and guest.

The STOMP broker relay also creates a separate TCP connection for every connected WebSocket client. You can configure the STOMP credentials that are used for all TCP connections created on behalf of clients. This is exposed in both the XML namespace and Java configuration as the clientLogin and `clientPasscode properties with default values of guest`and `guest.

The STOMP broker relay always sets the login and passcode headers on every CONNECT frame that it forwards to the broker on behalf of clients. Therefore, WebSocket clients need not set those headers. They are ignored. As the websocket-stomp-authentication section explains, WebSocket clients should instead rely on HTTP authentication to protect the WebSocket endpoint and establish the client identity.

The STOMP broker relay also sends and receives heartbeats to and from the message broker over the “system” TCP connection. You can configure the intervals for sending and receiving heartbeats (10 seconds each by default). If connectivity to the broker is lost, the broker relay continues to try to reconnect, every 5 seconds, until it succeeds.

Any Spring bean can implement ApplicationListener<BrokerAvailabilityEvent> to receive notifications when the “system” connection to the broker is lost and re-established. For example, a Stock Quote service that broadcasts stock quotes can stop trying to send messages when there is no active “system” connection.

By default, the STOMP broker relay always connects, and reconnects as needed if connectivity is lost, to the same host and port. If you wish to supply multiple addresses, on each attempt to connect, you can configure a supplier of addresses, instead of a fixed host and port. The following example shows how to do that:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

	// ...

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
		registry.setApplicationDestinationPrefixes("/app");
	}

	private ReactorNettyTcpClient<byte[]> createTcpClient() {
		return new ReactorNettyTcpClient<>(
				client -> client.addressSupplier(() -> ... ),
				new StompReactorNettyCodec());
	}
}

You can also configure the STOMP broker relay with a virtualHost property. The value of this property is set as the host header of every CONNECT frame and can be useful (for example, in a cloud environment where the actual host to which the TCP connection is established differs from the host that provides the cloud-based STOMP service).

Dots as Separators

When messages are routed to @MessageMapping methods, they are matched with AntPathMatcher. By default, patterns are expected to use slash (/) as the separator. This is a good convention in web applications and similar to HTTP URLs. However, if you are more used to messaging conventions, you can switch to using dot (.) as the separator.

The following example shows how to do so in Java configuration:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	// ...

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.setPathMatcher(new AntPathMatcher("."));
		registry.enableStompBrokerRelay("/queue", "/topic");
		registry.setApplicationDestinationPrefixes("/app");
	}
}

The following example shows the XML configuration equivalent of the preceding example:

<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
				https://www.springframework.org/schema/beans/spring-beans.xsd
				http://www.springframework.org/schema/websocket
				https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
		<websocket:stomp-endpoint path="/stomp"/>
		<websocket:stomp-broker-relay prefix="/topic,/queue" />
	</websocket:message-broker>

	<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
		<constructor-arg index="0" value="."/>
	</bean>

</beans>

After that, a controller can use a dot (.) as the separator in @MessageMapping methods, as the following example shows:

@Controller
@MessageMapping("red")
public class RedController {

	@MessageMapping("blue.{green}")
	public void handleGreen(@DestinationVariable String green) {
		// ...
	}
}

The client can now send a message to /app/red.blue.green123.

In the preceding example, we did not change the prefixes on the “broker relay”, because those depend entirely on the external message broker. See the STOMP documentation pages for the broker you use to see what conventions it supports for the destination header.

The “simple broker”, on the other hand, does rely on the configured PathMatcher, so, if you switch the separator, that change also applies to the broker and the way the broker matches destinations from a message to patterns in subscriptions.

Authentication

Every STOMP over WebSocket messaging session begins with an HTTP request. That can be a request to upgrade to WebSockets (that is, a WebSocket handshake) or, in the case of SockJS fallbacks, a series of SockJS HTTP transport requests.

Many web applications already have authentication and authorization in place to secure HTTP requests. Typically, a user is authenticated through Spring Security by using some mechanism such as a login page, HTTP basic authentication, or another way. The security context for the authenticated user is saved in the HTTP session and is associated with subsequent requests in the same cookie-based session.

Therefore, for a WebSocket handshake or for SockJS HTTP transport requests, typically, there is already an authenticated user accessible through HttpServletRequest#getUserPrincipal(). Spring automatically associates that user with a WebSocket or SockJS session created for them and, subsequently, with all STOMP messages transported over that session through a user header.

In short, a typical web application needs to do nothing beyond what it already does for security. The user is authenticated at the HTTP request level with a security context that is maintained through a cookie-based HTTP session (which is then associated with WebSocket or SockJS sessions created for that user) and results in a user header being stamped on every Message flowing through the application.

Note that the STOMP protocol does have login and passcode headers on the CONNECT frame. Those were originally designed for and are still needed, for example, for STOMP over TCP. However, for STOMP over WebSocket, by default, Spring ignores authorization headers at the STOMP protocol level, assumes that the user is already authenticated at the HTTP transport level, and expects that the WebSocket or SockJS session contain the authenticated user.

Spring Security provides WebSocket sub-protocol authorization that uses a ChannelInterceptor to authorize messages based on the user header in them. Also, Spring Session provides a WebSocket integration that ensures the user HTTP session does not expire when the WebSocket session is still active.

Token Authentication

Spring Security OAuth provides support for token based security, including JSON Web Token (JWT). You can use this as the authentication mechanism in Web applications, including STOMP over WebSocket interactions, as described in the previous section (that is, to maintain identity through a cookie-based session).

At the same time, cookie-based sessions are not always the best fit (for example, in applications that do not maintain a server-side session or in mobile applications where it is common to use headers for authentication).

The WebSocket protocol, RFC 6455 “doesn’t prescribe any particular way that servers can authenticate clients during the WebSocket handshake.” In practice, however, browser clients can use only standard authentication headers (that is, basic HTTP authentication) or cookies and cannot (for example) provide custom headers. Likewise, the SockJS JavaScript client does not provide a way to send HTTP headers with SockJS transport requests. See sockjs-client issue 196. Instead, it does allow sending query parameters that you can use to send a token, but that has its own drawbacks (for example, the token may be inadvertently logged with the URL in server logs).

The preceding limitations are for browser-based clients and do not apply to the Spring Java-based STOMP client, which does support sending headers with both WebSocket and SockJS requests.

Therefore, applications that wish to avoid the use of cookies may not have any good alternatives for authentication at the HTTP protocol level. Instead of using cookies, they may prefer to authenticate with headers at the STOMP messaging protocol level Doing so requires two simple steps:

  1. Use the STOMP client to pass authentication headers at connect time.

  2. Process the authentication headers with a ChannelInterceptor.

The next example uses server-side configuration to register a custom authentication interceptor. Note that an interceptor needs only to authenticate and set the user header on the CONNECT Message. Spring notes and saves the authenticated user and associate it with subsequent STOMP messages on the same session. The following example shows how register a custom authentication interceptor:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.interceptors(new ChannelInterceptor() {
			@Override
			public Message<?> preSend(Message<?> message, MessageChannel channel) {
				StompHeaderAccessor accessor =
						MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
				if (StompCommand.CONNECT.equals(accessor.getCommand())) {
					Authentication user = ... ; // access authentication header(s)
					accessor.setUser(user);
				}
				return message;
			}
		});
	}
}

Also, note that, when you use Spring Security’s authorization for messages, at present, you need to ensure that the authentication ChannelInterceptor config is ordered ahead of Spring Security’s. This is best done by declaring the custom interceptor in its own implementation of WebSocketMessageBrokerConfigurer that is marked with @Order(Ordered.HIGHEST_PRECEDENCE + 99).

User Destinations

An application can send messages that target a specific user, and Spring’s STOMP support recognizes destinations prefixed with /user/ for this purpose. For example, a client might subscribe to the /user/queue/position-updates destination. This destination is handled by the UserDestinationMessageHandler and transformed into a destination unique to the user session (such as /queue/position-updates-user123). This provides the convenience of subscribing to a generically named destination while, at the same time, ensuring no collisions with other users who subscribe to the same destination so that each user can receive unique stock position updates.

On the sending side, messages can be sent to a destination such as /user/{username}/queue/position-updates, which in turn is translated by the UserDestinationMessageHandler into one or more destinations, one for each session associated with the user. This lets any component within the application send messages that target a specific user without necessarily knowing anything more than their name and the generic destination. This is also supported through an annotation and a messaging template.

A message-handling method can send messages to the user associated with the message being handled through the @SendToUser annotation (also supported on the class-level to share a common destination), as the following example shows:

@Controller
public class PortfolioController {

	@MessageMapping("/trade")
	@SendToUser("/queue/position-updates")
	public TradeResult executeTrade(Trade trade, Principal principal) {
		// ...
		return tradeResult;
	}
}

If the user has more than one session, by default, all of the sessions subscribed to the given destination are targeted. However, sometimes, it may be necessary to target only the session that sent the message being handled. You can do so by setting the broadcast attribute to false, as the following example shows:

@Controller
public class MyController {

	@MessageMapping("/action")
	public void handleAction() throws Exception{
		// raise MyBusinessException here
	}

	@MessageExceptionHandler
	@SendToUser(destinations="/queue/errors", broadcast=false)
	public ApplicationError handleException(MyBusinessException exception) {
		// ...
		return appError;
	}
}
While user destinations generally imply an authenticated user, it is not strictly required. A WebSocket session that is not associated with an authenticated user can subscribe to a user destination. In such cases, the @SendToUser annotation behaves exactly the same as with broadcast=false (that is, targeting only the session that sent the message being handled).

You can send a message to user destinations from any application component by, for example, injecting the SimpMessagingTemplate created by the Java configuration or the XML namespace. (The bean name is "brokerMessagingTemplate" if required for qualification with @Qualifier.) The following example shows how to do so:

@Service
public class TradeServiceImpl implements TradeService {

	private final SimpMessagingTemplate messagingTemplate;

	@Autowired
	public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
		this.messagingTemplate = messagingTemplate;
	}

	// ...

	public void afterTradeExecuted(Trade trade) {
		this.messagingTemplate.convertAndSendToUser(
				trade.getUserName(), "/queue/position-updates", trade.getResult());
	}
}
When you use user destinations with an external message broker, you should 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 you use destinations such as /exchange/amq.direct/position-updates. So, in that case, the client could subscribe to /user/exchange/amq.direct/position-updates. Similarly, ActiveMQ has configuration options for purging inactive destinations.

In a multi-application server scenario, a user destination may remain unresolved because the user is connected to a different server. In such cases, you can configure a destination to broadcast unresolved messages so that other servers have a chance to try. This can be done through the userDestinationBroadcast property of the MessageBrokerRegistry in Java configuration and the user-destination-broadcast attribute of the message-broker element in XML.

Order of Messages

Messages from the broker are published to the clientOutboundChannel, from where they are written to WebSocket sessions. As the channel is backed by a ThreadPoolExecutor, messages are processed in different threads, and the resulting sequence received by the client may not match the exact order of publication.

If this is an issue, enable the setPreservePublishOrder flag, as the following example shows:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	protected void configureMessageBroker(MessageBrokerRegistry registry) {
		// ...
		registry.setPreservePublishOrder(true);
	}

}

The following example shows the XML configuration equivalent of the preceding example:

<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
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:message-broker preserve-publish-order="true">
		<!-- ... -->
	</websocket:message-broker>

</beans>

When the flag is set, messages within the same client session are published to the clientOutboundChannel one at a time, so that the order of publication is guaranteed. Note that this incurs a small performance overhead, so you should enable it only if it is required.

Events

Several ApplicationContext events are published and can be received by implementing Spring’s ApplicationListener interface:

  • BrokerAvailabilityEvent: Indicates when the broker becomes available or unavailable. While the “simple” broker becomes available immediately on startup and remains so while the application is running, the STOMP “broker relay” can lose its connection to the full featured broker (for example, if the broker is restarted). The broker relay has reconnect logic and re-establishes the “system” connection to the broker when it comes back. As a result, this event is published whenever the state changes from connected to disconnected and vice-versa. Components that use the SimpMessagingTemplate should subscribe to this event and avoid sending messages at times when the broker is not available. In any case, they should be prepared to handle MessageDeliveryException when sending a message.

  • SessionConnectEvent: Published when a new STOMP CONNECT is received to indicate the start of a new client session. The event contains the message that represents the connect, including the session ID, user information (if any), and any custom headers the client sent. This is useful for tracking client sessions. Components subscribed to this event can wrap the contained message with SimpMessageHeaderAccessor or StompMessageHeaderAccessor.

  • SessionConnectedEvent: Published shortly after a SessionConnectEvent when the broker has sent a STOMP CONNECTED frame in response to the CONNECT. At this point, the STOMP session can be considered fully established.

  • SessionSubscribeEvent: Published when a new STOMP SUBSCRIBE is received.

  • SessionUnsubscribeEvent: Published when a new STOMP UNSUBSCRIBE is received.

  • SessionDisconnectEvent: Published when a STOMP session ends. The DISCONNECT may have been sent from the client or it may be automatically generated when the WebSocket session is closed. In some cases, this event is published more than once per session. Components should be idempotent with regard to multiple disconnect events.

When you use a full-featured broker, the STOMP “broker relay” automatically reconnects the “system” connection if broker becomes temporarily unavailable. Client connections, however, are not automatically reconnected. Assuming heartbeats are enabled, the client typically notices the broker is not responding within 10 seconds. Clients need to implement their own reconnecting logic.

Interception

websocket-stomp-appplication-context-events provide notifications for the lifecycle of a STOMP connection but not for every client message. Applications can also register a ChannelInterceptor to intercept any message and in any part of the processing chain. The following example shows how to intercept inbound messages from clients:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.interceptors(new MyChannelInterceptor());
	}
}

A custom ChannelInterceptor can use StompHeaderAccessor or SimpMessageHeaderAccessor to access information about the message, as the following example shows:

public class MyChannelInterceptor implements ChannelInterceptor {

	@Override
	public Message<?> preSend(Message<?> message, MessageChannel channel) {
		StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
		StompCommand command = accessor.getStompCommand();
		// ...
		return message;
	}
}

Applications can also implement ExecutorChannelInterceptor, which is a sub-interface of ChannelInterceptor with callbacks in the thread in which the messages are handled. While a ChannelInterceptor is invoked once for each message sent to a channel, the ExecutorChannelInterceptor provides hooks in the thread of each MessageHandler subscribed to messages from the channel.

Note that, as with the SesionDisconnectEvent described earlier, a DISCONNECT message can be from the client or it can also be automatically generated when the WebSocket session is closed. In some cases, an interceptor may intercept this message more than once for each session. Components should be idempotent with regard to multiple disconnect events.

STOMP Client

Spring provides a STOMP over WebSocket client and a STOMP over TCP client.

To begin, you can create and configure WebSocketStompClient, as the following example shows:

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

In the preceding example, you could replace StandardWebSocketClient with SockJsClient, since that is also an implementation of WebSocketClient. The SockJsClient can use WebSocket or HTTP-based transport as a fallback. For more details, see websocket-fallback-sockjs-client.

Next, you can establish a connection and provide a handler for the STOMP session, as the following example shows:

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

When the session is ready for use, the handler is notified, as the following example shows:

public class MyStompSessionHandler extends StompSessionHandlerAdapter {

	@Override
	public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
		// ...
	}
}

Once the session is established, any payload can be sent and is serialized with the configured MessageConverter, as the following example shows:

session.send("/topic/something", "payload");

You can also subscribe to destinations. The subscribe methods require a handler for messages on the subscription and returns a Subscription handle that you can use to unsubscribe. For each received message, the handler can specify the target Object type to which the payload should be deserialized, as the following example shows:

session.subscribe("/topic/something", new StompFrameHandler() {

	@Override
	public Type getPayloadType(StompHeaders headers) {
		return String.class;
	}

	@Override
	public void handleFrame(StompHeaders headers, Object payload) {
		// ...
	}

});

To enable STOMP heartbeat, you can configure WebSocketStompClient with a TaskScheduler and optionally customize the heartbeat intervals (10 seconds for write inactivity, which causes a heartbeat to be sent, and 10 seconds for read inactivity, which closes the connection).

When you use WebSocketStompClient for performance tests to simulate thousands of clients from the same machine, consider turning off heartbeats, since each connection schedules its own heartbeat tasks and that is not optimized for a large number of clients running on the same machine.

The STOMP protocol also supports receipts, where the client must add a receipt header to which the server responds with a RECEIPT frame after the send or subscribe are processed. To support this, the StompSession offers setAutoReceipt(boolean) that causes a receipt header to be added on every subsequent send or subscribe event. Alternatively, you can also manually add a receipt header to the StompHeaders. Both send and subscribe return an instance of Receiptable that you can use to register for receipt success and failure callbacks. For this feature, you must configure the client with a TaskScheduler and the amount of time before a receipt expires (15 seconds by default).

Note that StompSessionHandler itself is a StompFrameHandler, which lets it handle ERROR frames in addition to the handleException callback for exceptions from the handling of messages and handleTransportError for transport-level errors including ConnectionLostException.

WebSocket Scope

Each WebSocket session has a map of attributes. The map is attached as a header to inbound client messages and may be accessed from a controller method, as the following example shows:

@Controller
public class MyController {

	@MessageMapping("/action")
	public void handle(SimpMessageHeaderAccessor headerAccessor) {
		Map<String, Object> attrs = headerAccessor.getSessionAttributes();
		// ...
	}
}

You can declare a Spring-managed bean in the websocket scope. You can inject WebSocket-scoped beans into controllers and any channel interceptors registered on the clientInboundChannel. Those are typically singletons and live longer than any individual WebSocket session. Therefore, you need to use a scope proxy mode for WebSocket-scoped beans, as the following example shows:

@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {

	@PostConstruct
	public void init() {
		// Invoked after dependencies injected
	}

	// ...

	@PreDestroy
	public void destroy() {
		// Invoked when the WebSocket session ends
	}
}

@Controller
public class MyController {

	private final MyBean myBean;

	@Autowired
	public MyController(MyBean myBean) {
		this.myBean = myBean;
	}

	@MessageMapping("/action")
	public void handle() {
		// this.myBean from the current WebSocket session
	}
}

As with any custom scope, Spring initializes a new MyBean instance the first time it is accessed from the controller and stores the instance in the WebSocket session attributes. The same instance is subsequently returned until the session ends. WebSocket-scoped beans have all Spring lifecycle methods invoked, as shown in the preceding examples.

Performance

There is no silver bullet when it comes to performance. Many factors affect it, including the size and volume of messages, whether application methods perform work that requires blocking, and external factors (such as network speed and other issues). The goal of this section is to provide an overview of the available configuration options along with some thoughts on how to reason about scaling.

In a messaging application, messages are passed through channels for asynchronous executions that are backed by thread pools. Configuring such an application requires good knowledge of the channels and the flow of messages. Therefore, it is recommended to review websocket-stomp-message-flow.

The obvious place to start is to configure the thread pools that back the clientInboundChannel and the clientOutboundChannel. By default, both are configured at twice the number of available processors.

If the handling of messages in annotated methods is mainly CPU-bound, the number of threads for the clientInboundChannel should remain close to the number of processors. If the work they do is more IO-bound and requires blocking or waiting on a database or other external system, the thread pool size probably needs to be increased.

ThreadPoolExecutor has three important properties: the core thread pool size, the max thread pool size, and the capacity for the queue to store tasks for which there are no available threads.

A common point of confusion is that configuring the core pool size (for example, 10) and max pool size (for example, 20) results in a thread pool with 10 to 20 threads. In fact, if the capacity is left at its default value of Integer.MAX_VALUE, the thread pool never increases beyond the core pool size, since all additional tasks are queued.

See the javadoc of ThreadPoolExecutor to learn how these properties work and understand the various queuing strategies.

On the clientOutboundChannel side, it is all about sending messages to WebSocket clients. If clients are on a fast network, the number of threads should remain close to the number of available processors. If they are slow or on low bandwidth, they take longer to consume messages and put a burden on the thread pool. Therefore, increasing the thread pool size becomes necessary.

While the workload for the clientInboundChannel is possible to predict — after all, it is based on what the application does — how to configure the "clientOutboundChannel" is harder, as it is based on factors beyond the control of the application. For this reason, two additional properties relate to the sending of messages: sendTimeLimit and sendBufferSizeLimit. You can use those methods to configure how long a send is allowed to take and how much data can be buffered when sending messages to a client.

The general idea is that, at any given time, only a single thread can be used to send to a client. All additional messages, meanwhile, get buffered, and you can use these properties to decide how long sending a message is allowed to take and how much data can be buffered in the meantime. See the javadoc and documentation of the XML schema for important additional details.

The following example shows a possible configuration:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
		registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
	}

	// ...

}

The following example shows the XML configuration equivalent of the preceding example:

<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
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:message-broker>
		<websocket:transport send-timeout="15000" send-buffer-size="524288" />
		<!-- ... -->
	</websocket:message-broker>

</beans>

You can also use the WebSocket transport configuration shown earlier to configure the maximum allowed size for incoming STOMP messages. In theory, a WebSocket message can be almost unlimited in size. In practice, WebSocket servers impose limits — for example, 8K on Tomcat and 64K on Jetty. For this reason, STOMP clients (such as the JavaScript webstomp-client and others) split larger STOMP messages at 16K boundaries and send them as multiple WebSocket messages, which requires the server to buffer and re-assemble.

Spring’s STOMP-over-WebSocket support does this ,so applications can configure the maximum size for STOMP messages irrespective of WebSocket server-specific message sizes. Keep in mind that the WebSocket message size is automatically adjusted, if necessary, to ensure they can carry 16K WebSocket messages at a minimum.

The following example shows one possible configuration:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
		registration.setMessageSizeLimit(128 * 1024);
	}

	// ...

}

The following example shows the XML configuration equivalent of the preceding example:

<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
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:message-broker>
		<websocket:transport message-size="131072" />
		<!-- ... -->
	</websocket:message-broker>

</beans>

An important point about scaling involves using multiple application instances. Currently, you cannot do that with the simple broker. However, when you use a full-featured broker (such as RabbitMQ), each application instance connects to the broker, and messages broadcast from one application instance can be broadcast through the broker to WebSocket clients connected through any other application instances.

Monitoring

When you use @EnableWebSocketMessageBroker or <websocket:message-broker>, key infrastructure components automatically gather statisticss and counters that provide important insight into the internal state of the application. The configuration also declares a bean of type WebSocketMessageBrokerStats that gathers all available information in one place and by default logs it at the INFO level once every 30 minutes. This bean can be exported to JMX through Spring’s MBeanExporter for viewing at runtime (for example, through JDK’s jconsole). The following list summarizes the available information:

Client WebSocket Sessions
Current

Indicates how many client sessions there are currently, with the count further broken down by WebSocket versus HTTP streaming and polling SockJS sessions.

Total

Indicates how many total sessions have been established.

Abnormally Closed
Connect Failures

Sessions that got established but were closed after not having received any messages within 60 seconds. This is usually an indication of proxy or network issues.

Send Limit Exceeded

Sessions closed after exceeding the configured send timeout or the send buffer limits, which can occur with slow clients (see previous section).

Transport Errors

Sessions closed after a transport error, such as failure to read or write to a WebSocket connection or HTTP request or response.

STOMP Frames

The total number of CONNECT, CONNECTED, and DISCONNECT frames processed, indicating how many clients connected on the STOMP level. Note that the DISCONNECT count may be lower when sessions get closed abnormally or when clients close without sending a DISCONNECT frame.

STOMP Broker Relay
TCP Connections

Indicates how many TCP connections on behalf of client WebSocket sessions are established to the broker. This should be equal to the number of client WebSocket sessions + 1 additional shared “system” connection for sending messages from within the application.

STOMP Frames

The total number of CONNECT, CONNECTED, and DISCONNECT frames forwarded to or received from the broker on behalf of clients. Note that a DISCONNECT frame is sent to the broker regardless of how the client WebSocket session was closed. Therefore, a lower DISCONNECT frame count is an indication that the broker is pro-actively closing connections (maybe because of a heartbeat that did not arrive in time, an invalid input frame, or other issue).

Client Inbound Channel

Statistics from the thread pool that backs the clientInboundChannel that provide insight into the health of incoming message processing. Tasks queueing up here is an indication that the application may be too slow to handle messages. If there I/O bound tasks (for example, slow database queries, HTTP requests to third party REST API, and so on), consider increasing the thread pool size.

Client Outbound Channel

Statistics from the thread pool that backs the clientOutboundChannel that provides insight into the health of broadcasting messages to clients. Tasks queueing up here is an indication clients are too slow to consume messages. One way to address this is to increase the thread pool size to accommodate the expected number of concurrent slow clients. Another option is to reduce the send timeout and send buffer size limits (see the previous section).

SockJS Task Scheduler

Statistics from the thread pool of the SockJS task scheduler that is used to send heartbeats. Note that, when heartbeats are negotiated on the STOMP level, the SockJS heartbeats are disabled.

Testing

There are two main approaches to testing applications when you use Spring’s STOMP-over-WebSocket support. The first is to write server-side tests to verify 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 are 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 cannot test that.

Ideally, controllers under test should be invoked as they are at runtime, much like the approach to testing controllers that handle HTTP requests by using the Spring MVC Test framework — that is, without running a Servlet container but relying on the Spring Framework to invoke the annotated controllers. As with Spring MVC Test, you have two possible alternatives here, either use a “context-based” or use a “standalone” setup:

  • Load the actual Spring configuration with the help of the Spring TestContext framework, inject clientInboundChannel as a test field, and use it to send messages to be handled by controller methods.

  • Manually set up the minimum Spring framework infrastructure required to invoke controllers (namely the 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 need to run a WebSocket server in embedded mode and connect to it as a WebSocket client that sends WebSocket messages containing STOMP frames. The tests for the stock portfolio sample application also demonstrate this approach by using Tomcat as the embedded WebSocket server and a simple STOMP client for test purposes.