Split files
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
[[websocket-stomp-application-context-events]]
|
||||
= 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.
|
||||
|
||||
NOTE: 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
[[websocket-stomp-authentication-token-based]]
|
||||
= Token Authentication
|
||||
|
||||
https://github.com/spring-projects/spring-security-oauth[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 https://tools.ietf.org/html/rfc6455#section-10.5[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
|
||||
https://github.com/sockjs/sockjs-client/issues/196[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).
|
||||
|
||||
NOTE: 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:
|
||||
|
||||
. Use the STOMP client to pass authentication headers at connect time.
|
||||
. 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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)`.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
[[websocket-stomp-authentication]]
|
||||
= 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.
|
||||
|
||||
The STOMP protocol does have `login` and `passcode` headers on the `CONNECT` frame.
|
||||
Those were originally designed for and are needed for STOMP over TCP. However, for STOMP
|
||||
over WebSocket, by default, Spring ignores authentication headers at the STOMP protocol
|
||||
level, and assumes that the user is already authenticated at the HTTP transport level.
|
||||
The expectation is that the WebSocket or SockJS session contain the authenticated user.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[[websocket-stomp-authorization]]
|
||||
= Authorization
|
||||
|
||||
Spring Security provides
|
||||
{docs-spring-security}/servlet/integrations/websocket.html#websocket-authorization[WebSocket sub-protocol authorization]
|
||||
that uses a `ChannelInterceptor` to authorize messages based on the user header in them.
|
||||
Also, Spring Session provides
|
||||
https://docs.spring.io/spring-session/reference/web-socket.html[WebSocket integration]
|
||||
that ensures the user's HTTP session does not expire while the WebSocket session is still active.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[[websocket-stomp-benefits]]
|
||||
= 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 <<websocket-stomp-client, 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
[[websocket-stomp-client]]
|
||||
= 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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).
|
||||
|
||||
`WebSocketStompClient` sends a heartbeat only in case of inactivity, i.e. when no
|
||||
other messages are sent. This can present a challenge when using an external broker
|
||||
since messages with a non-broker destination represent activity but aren't actually
|
||||
forwarded to the broker. In that case you can configure a `TaskScheduler`
|
||||
when initializing the <<websocket-stomp-handle-broker-relay>> which ensures a
|
||||
heartbeat is forwarded to the broker also when only messages with a non-broker
|
||||
destination are sent.
|
||||
|
||||
NOTE: 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`.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
[[websocket-stomp-configuration-performance]]
|
||||
= 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.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
`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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<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 https://github.com/JSteunou/webstomp-client[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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
[[websocket-stomp-destination-separator]]
|
||||
= 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
[[websocket-stomp-enable]]
|
||||
= 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<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>
|
||||
----
|
||||
|
||||
NOTE: 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
|
||||
https://github.com/sockjs/sockjs-client[`sockjs-client`]. For STOMP, many applications have
|
||||
used the https://github.com/jmesnil/stomp-websocket[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
|
||||
https://github.com/JSteunou/webstomp-client[JSteunou/webstomp-client] is the most
|
||||
actively maintained and evolving successor of that library. The following example code
|
||||
is based on it:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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:
|
||||
|
||||
* https://spring.io/guides/gs/messaging-stomp-websocket/[Using WebSocket to build an
|
||||
interactive web application] -- a getting started guide.
|
||||
* https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample
|
||||
application.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
[[websocket-stomp-handle-annotations]]
|
||||
= 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:
|
||||
|
||||
* <<websocket-stomp-message-mapping>>
|
||||
* <<websocket-stomp-subscribe-mapping>>
|
||||
* <<websocket-stomp-exception-handler>>
|
||||
|
||||
|
||||
[[websocket-stomp-message-mapping]]
|
||||
== `@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, pass:q[`/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]]
|
||||
=== Supported Method Arguments
|
||||
|
||||
The following table describes the method arguments:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| 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 `@jakarta.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]]
|
||||
=== 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>>.
|
||||
|
||||
|
||||
[[websocket-stomp-subscribe-mapping]]
|
||||
== `@SubscribeMapping`
|
||||
|
||||
`@SubscribeMapping` is similar to `@MessageMapping` but narrows the mapping to
|
||||
subscription messages only. It supports the same
|
||||
<<websocket-stomp-message-mapping, 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
|
||||
<<websocket-stomp-client, STOMP client>>, you could do the following to add a receipt:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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(receiptHeaders -> {
|
||||
// Subscription ready...
|
||||
});
|
||||
----
|
||||
|
||||
A server side option is <<websocket-stomp-interceptors, to register>> an
|
||||
`ExecutorChannelInterceptor` on the `brokerChannel` and implement the `afterMessageHandled`
|
||||
method that is invoked after messages, including subscriptions, have been handled.
|
||||
|
||||
|
||||
[[websocket-stomp-exception-handler]]
|
||||
== `@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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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
|
||||
<<websocket-stomp-message-mapping, `@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
|
||||
<<web.adoc#mvc-ann-controller-advice, similar support>> available in Spring MVC.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
[[websocket-stomp-handle-broker-relay-configure]]
|
||||
= 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`.
|
||||
|
||||
NOTE: 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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).
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
[[websocket-stomp-handle-broker-relay]]
|
||||
= 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
|
||||
https://www.rabbitmq.com/stomp.html[RabbitMQ],
|
||||
https://activemq.apache.org/stomp.html[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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<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
|
||||
{api-spring-framework}/messaging/MessageHandler.html[`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.
|
||||
|
||||
NOTE: 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
[[websocket-stomp-handle-send]]
|
||||
= 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
[[websocket-stomp-handle-simple-broker]]
|
||||
= 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.
|
||||
|
||||
NOTE: 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
|
||||
https://stomp.github.io/stomp-specification-1.2.html#Heart-beating[STOMP heartbeats].
|
||||
To configure a scheduler, you can declare your own `TaskScheduler` bean and set it through
|
||||
the `MessageBrokerRegistry`. Alternatively, you can use the one that is automatically
|
||||
declared in the built-in WebSocket configuration, however, you'll' need `@Lazy` to avoid
|
||||
a cycle between the built-in WebSocket configuration and your
|
||||
`WebSocketMessageBrokerConfigurer`. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private TaskScheduler messageBrokerTaskScheduler;
|
||||
|
||||
@Autowired
|
||||
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
|
||||
this.messageBrokerTaskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/")
|
||||
.setHeartbeatValue(new long[] {10000, 20000})
|
||||
.setTaskScheduler(this.messageBrokerTaskScheduler);
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
[[websocket-stomp-interceptors]]
|
||||
= Interception
|
||||
|
||||
<<websocket-stomp-application-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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
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 `SessionDisconnectEvent` 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
[[websocket-stomp-message-flow]]
|
||||
= 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 https://spring.io/spring-integration[Spring Integration] and was
|
||||
later extracted and incorporated into the Spring Framework for broader use across many
|
||||
https://spring.io/projects[Spring projects] and application scenarios.
|
||||
The following list briefly describes a few of the available messaging abstractions:
|
||||
|
||||
* {api-spring-framework}/messaging/Message.html[Message]:
|
||||
Simple representation for a message, including headers and payload.
|
||||
* {api-spring-framework}/messaging/MessageHandler.html[MessageHandler]:
|
||||
Contract for handling a message.
|
||||
* {api-spring-framework}/messaging/MessageChannel.html[MessageChannel]:
|
||||
Contract for sending a message that enables loose coupling between producers and consumers.
|
||||
* {api-spring-framework}/messaging/SubscribableChannel.html[SubscribableChannel]:
|
||||
`MessageChannel` with `MessageHandler` subscribers.
|
||||
* {api-spring-framework}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel]:
|
||||
`SubscribableChannel` that uses an `Executor` for delivering messages.
|
||||
|
||||
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:
|
||||
|
||||
image::message-flow-simple-broker.png[]
|
||||
|
||||
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:
|
||||
|
||||
image::message-flow-broker-relay.png[]
|
||||
|
||||
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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
. The client connects to `http://localhost:8080/portfolio` and, once a WebSocket connection
|
||||
is established, STOMP frames begin to flow on it.
|
||||
. 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.
|
||||
. The client sends a SEND 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`.
|
||||
. 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.
|
||||
. 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
[[websocket-stomp-ordered-messages]]
|
||||
= 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
[[websocket-stomp-overview]]
|
||||
= Overview
|
||||
|
||||
https://stomp.github.io/stomp-specification-1.2.html#Abstract[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` 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
|
||||
https://stomp.github.io/stomp-specification-1.2.html[specification] in full.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
[[websocket-stomp-websocket-scope]]
|
||||
= 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[[websocket-stomp-server-config]]
|
||||
= 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`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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)));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
[[websocket-stomp-stats]]
|
||||
= Monitoring
|
||||
|
||||
When you use `@EnableWebSocketMessageBroker` or `<websocket:message-broker>`, key
|
||||
infrastructure components automatically gather statistics 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.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
[[websocket-stomp-testing]]
|
||||
= 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
|
||||
https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web[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 https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web[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.
|
||||
@@ -0,0 +1,117 @@
|
||||
[[websocket-stomp-user-destination]]
|
||||
= 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.
|
||||
`UserDestinationMessageHandler` handles this destination and transforms it 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.
|
||||
|
||||
TIP: When working with user destinations, it is important to configure broker and
|
||||
application destination prefixes as shown in <<websocket-stomp-enable>>, or otherwise the
|
||||
broker would handle "/user" prefixed messages that should only be handled by
|
||||
`UserDestinationMessageHandler`.
|
||||
|
||||
On the sending side, messages can be sent to a destination such as
|
||||
pass:q[`/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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: 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:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@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());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: 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
|
||||
https://activemq.apache.org/delete-inactive-destinations.html[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.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user