Add STOMP client
WebSocketStompClient can be used with any implementation of org.springframework.web.socket.client.WebSocketClient, which includes org.springframework.web.socket.sockjs.client.SockJsClient. Reactor11TcpStompClient can be used with reactor-net and provides STOMP over TCP. It's also possible to adapt other WebSocket and TCP client libraries (see StompClientSupport for more details). For example usage see WebSocketStompClientIntegrationTests. Issue: SPR-11588
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.simp.stomp.BufferingStompDecoder;
|
||||
import org.springframework.messaging.simp.stomp.ConnectionHandlingStompSession;
|
||||
import org.springframework.messaging.simp.stomp.StompClientSupport;
|
||||
import org.springframework.messaging.simp.stomp.StompDecoder;
|
||||
import org.springframework.messaging.simp.stomp.StompEncoder;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaders;
|
||||
import org.springframework.messaging.simp.stomp.StompSession;
|
||||
import org.springframework.messaging.simp.stomp.StompSessionHandler;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.messaging.tcp.TcpConnection;
|
||||
import org.springframework.messaging.tcp.TcpConnectionHandler;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||
import org.springframework.util.concurrent.SettableListenableFuture;
|
||||
import org.springframework.web.socket.BinaryMessage;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
import org.springframework.web.socket.WebSocketMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.sockjs.transport.SockJsSession;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A STOMP over WebSocket client that connects using an implementation of
|
||||
* {@link org.springframework.web.socket.client.WebSocketClient WebSocketClient}
|
||||
* including {@link org.springframework.web.socket.sockjs.client.SockJsClient
|
||||
* SockJsClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.2
|
||||
*/
|
||||
public class WebSocketStompClient extends StompClientSupport implements SmartLifecycle {
|
||||
|
||||
private static Log logger = LogFactory.getLog(WebSocketStompClient.class);
|
||||
|
||||
|
||||
private final WebSocketClient webSocketClient;
|
||||
|
||||
private int inboundMessageSizeLimit = 64 * 1024;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
|
||||
private boolean running = false;
|
||||
|
||||
private int phase = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor. Sets {@link #setDefaultHeartbeat} to "0,0" but will
|
||||
* reset it back to the preferred "10000,10000" when a
|
||||
* {@link #setTaskScheduler} is configured.
|
||||
*
|
||||
* @param webSocketClient the WebSocket client to connect with
|
||||
*/
|
||||
public WebSocketStompClient(WebSocketClient webSocketClient) {
|
||||
Assert.notNull(webSocketClient, "'webSocketClient' is required.");
|
||||
this.webSocketClient = webSocketClient;
|
||||
setDefaultHeartbeat(new long[] {0, 0});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured WebSocketClient.
|
||||
*/
|
||||
public WebSocketClient getWebSocketClient() {
|
||||
return this.webSocketClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>Also automatically sets the {@link #setDefaultHeartbeat defaultHeartbeat}
|
||||
* property to "10000,10000" if it is currently set to "0,0".
|
||||
*/
|
||||
@Override
|
||||
public void setTaskScheduler(TaskScheduler taskScheduler) {
|
||||
if (taskScheduler != null && !isDefaultHeartbeatEnabled()) {
|
||||
setDefaultHeartbeat(new long[] {10000, 10000});
|
||||
}
|
||||
super.setTaskScheduler(taskScheduler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the maximum size allowed for inbound STOMP message.
|
||||
* Since a STOMP message can be received in multiple WebSocket messages,
|
||||
* buffering may be required and this property determines the maximum buffer
|
||||
* size per message.
|
||||
* <p>By default this is set to 64 * 1024 (64K).
|
||||
*/
|
||||
public void setInboundMessageSizeLimit(int inboundMessageSizeLimit) {
|
||||
this.inboundMessageSizeLimit = inboundMessageSizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured inbound message buffer size in bytes.
|
||||
*/
|
||||
public int getInboundMessageSizeLimit() {
|
||||
return this.inboundMessageSizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to auto-start the contained WebSocketClient when the Spring
|
||||
* context has been refreshed.
|
||||
* <p>Default is "true".
|
||||
*/
|
||||
public void setAutoStartup(boolean autoStartup) {
|
||||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value for the 'autoStartup' property. If "true", this client
|
||||
* will automatically start and stop the contained WebSocketClient.
|
||||
*/
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return this.autoStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the phase in which the WebSocket client should be started and
|
||||
* subsequently closed. The startup order proceeds from lowest to highest,
|
||||
* and the shutdown order is the reverse of that.
|
||||
* <p>By default this is Integer.MAX_VALUE meaning that the WebSocket client
|
||||
* is started as late as possible and stopped as soon as possible.
|
||||
*/
|
||||
public void setPhase(int phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured phase.
|
||||
*/
|
||||
@Override
|
||||
public int getPhase() {
|
||||
return this.phase;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (!isRunning()) {
|
||||
this.running = true;
|
||||
if (getWebSocketClient() instanceof Lifecycle) {
|
||||
((Lifecycle) getWebSocketClient()).start();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (isRunning()) {
|
||||
this.running = false;
|
||||
if (getWebSocketClient() instanceof Lifecycle) {
|
||||
((Lifecycle) getWebSocketClient()).stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Runnable callback) {
|
||||
this.stop();
|
||||
callback.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the given WebSocket URL and notify the given
|
||||
* {@link org.springframework.messaging.simp.stomp.StompSessionHandler}
|
||||
* when connected on the STOMP level after the CONNECTED frame is received.
|
||||
* @param url the url to connect to
|
||||
* @param handler the session handler
|
||||
* @param uriVars URI variables to expand into the URL
|
||||
* @return ListenableFuture for access to the session when ready for use
|
||||
*/
|
||||
public ListenableFuture<StompSession> connect(String url, StompSessionHandler handler, Object... uriVars) {
|
||||
return connect(url, null, handler, uriVars);
|
||||
}
|
||||
|
||||
/**
|
||||
* An overloaded version of
|
||||
* {@link #connect(String, StompSessionHandler, Object...)} that also
|
||||
* accepts {@link WebSocketHttpHeaders} to use for the WebSocket handshake.
|
||||
* @param url the url to connect to
|
||||
* @param handshakeHeaders the headers for the WebSocket handshake
|
||||
* @param handler the session handler
|
||||
* @param uriVariables URI variables to expand into the URL
|
||||
* @return ListenableFuture for access to the session when ready for use
|
||||
*/
|
||||
public ListenableFuture<StompSession> connect(String url, WebSocketHttpHeaders handshakeHeaders,
|
||||
StompSessionHandler handler, Object... uriVariables) {
|
||||
|
||||
return connect(url, handshakeHeaders, null, handler, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* An overloaded version of
|
||||
* {@link #connect(String, StompSessionHandler, Object...)} that also accepts
|
||||
* {@link WebSocketHttpHeaders} to use for the WebSocket handshake and
|
||||
* {@link StompHeaders} for the STOMP CONNECT frame.
|
||||
* @param url the url to connect to
|
||||
* @param handshakeHeaders headers for the WebSocket handshake
|
||||
* @param connectHeaders headers for the STOMP CONNECT frame
|
||||
* @param handler the session handler
|
||||
* @param uriVariables URI variables to expand into the URL
|
||||
* @return ListenableFuture for access to the session when ready for use
|
||||
*/
|
||||
public ListenableFuture<StompSession> connect(String url, WebSocketHttpHeaders handshakeHeaders,
|
||||
StompHeaders connectHeaders, StompSessionHandler handler, Object... uriVariables) {
|
||||
|
||||
Assert.notNull(url, "uriTemplate must not be null");
|
||||
URI uri = UriComponentsBuilder.fromUriString(url).buildAndExpand(uriVariables).encode().toUri();
|
||||
return connect(uri, handshakeHeaders, connectHeaders, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* An overloaded version of
|
||||
* {@link #connect(String, WebSocketHttpHeaders, StompSessionHandler, Object...)}
|
||||
* that accepts a fully prepared {@link java.net.URI}.
|
||||
* @param url the url to connect to
|
||||
* @param handshakeHeaders the headers for the WebSocket handshake
|
||||
* @param connectHeaders headers for the STOMP CONNECT frame
|
||||
* @param sessionHandler the STOMP session handler
|
||||
* @return ListenableFuture for access to the session when ready for use
|
||||
*/
|
||||
public ListenableFuture<StompSession> connect(URI url, WebSocketHttpHeaders handshakeHeaders,
|
||||
StompHeaders connectHeaders, StompSessionHandler sessionHandler) {
|
||||
|
||||
Assert.notNull(url, "'uri' must not be null");
|
||||
ConnectionHandlingStompSession session = createSession(connectHeaders, sessionHandler);
|
||||
WebSocketTcpConnectionHandlerAdapter adapter = new WebSocketTcpConnectionHandlerAdapter(session);
|
||||
getWebSocketClient().doHandshake(adapter, handshakeHeaders, url).addCallback(adapter);
|
||||
return session.getSessionFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StompHeaders processConnectHeaders(StompHeaders connectHeaders) {
|
||||
connectHeaders = super.processConnectHeaders(connectHeaders);
|
||||
if (connectHeaders.isHeartbeatEnabled()) {
|
||||
Assert.notNull(getTaskScheduler(), "TaskScheduler cannot be null if heartbeats are enabled.");
|
||||
}
|
||||
return connectHeaders;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adapt WebSocket to the TcpConnectionHandler and TcpConnection contracts.
|
||||
*/
|
||||
private class WebSocketTcpConnectionHandlerAdapter implements ListenableFutureCallback<WebSocketSession>,
|
||||
WebSocketHandler, TcpConnection<byte[]> {
|
||||
|
||||
private final TcpConnectionHandler<byte[]> connectionHandler;
|
||||
|
||||
private final StompWebSocketMessageCodec codec = new StompWebSocketMessageCodec(getInboundMessageSizeLimit());
|
||||
|
||||
private volatile WebSocketSession session;
|
||||
|
||||
private volatile long lastReadTime = -1;
|
||||
|
||||
private volatile long lastWriteTime = -1;
|
||||
|
||||
private final List<ScheduledFuture<?>> inactivityTasks = new ArrayList<ScheduledFuture<?>>(2);
|
||||
|
||||
|
||||
public WebSocketTcpConnectionHandlerAdapter(TcpConnectionHandler<byte[]> connectionHandler) {
|
||||
Assert.notNull(connectionHandler);
|
||||
this.connectionHandler = connectionHandler;
|
||||
}
|
||||
|
||||
|
||||
// ListenableFutureCallback implementation: handshake outcome
|
||||
|
||||
@Override
|
||||
public void onSuccess(WebSocketSession webSocketSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable ex) {
|
||||
this.connectionHandler.afterConnectFailure(ex);
|
||||
}
|
||||
|
||||
|
||||
// WebSocketHandler implementation
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
this.session = session;
|
||||
this.connectionHandler.afterConnected(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> webSocketMessage) {
|
||||
this.lastReadTime = (this.lastReadTime != -1 ? System.currentTimeMillis() : -1);
|
||||
List<Message<byte[]>> messages;
|
||||
try {
|
||||
messages = this.codec.decode(webSocketMessage);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
this.connectionHandler.handleFailure(ex);
|
||||
return;
|
||||
}
|
||||
for (Message<byte[]> message : messages) {
|
||||
this.connectionHandler.handleMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable ex) throws Exception {
|
||||
this.connectionHandler.handleFailure(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||
cancelInactivityTasks();
|
||||
this.connectionHandler.afterConnectionClosed();
|
||||
}
|
||||
|
||||
private void cancelInactivityTasks() {
|
||||
for (ScheduledFuture<?> task : this.inactivityTasks) {
|
||||
try {
|
||||
task.cancel(true);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
this.lastReadTime = -1;
|
||||
this.lastWriteTime = -1;
|
||||
this.inactivityTasks.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPartialMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// TcpConnection implementation
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Void> send(Message<byte[]> message) {
|
||||
updateLastWriteTime();
|
||||
SettableListenableFuture<Void> future = new SettableListenableFuture<Void>();
|
||||
try {
|
||||
this.session.sendMessage(this.codec.encode(message, this.session.getClass()));
|
||||
future.set(null);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
future.setException(ex);
|
||||
}
|
||||
finally {
|
||||
updateLastWriteTime();
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
private void updateLastWriteTime() {
|
||||
this.lastWriteTime = (this.lastWriteTime != -1 ? System.currentTimeMillis() : -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadInactivity(final Runnable runnable, final long duration) {
|
||||
Assert.notNull(getTaskScheduler(), "No scheduler configured.");
|
||||
this.lastReadTime = System.currentTimeMillis();
|
||||
this.inactivityTasks.add(getTaskScheduler().scheduleWithFixedDelay(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (System.currentTimeMillis() - lastReadTime > duration) {
|
||||
try {
|
||||
runnable.run();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("ReadInactivityTask failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, duration / 2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWriteInactivity(final Runnable runnable, final long duration) {
|
||||
Assert.notNull(getTaskScheduler(), "No scheduler configured.");
|
||||
this.lastWriteTime = System.currentTimeMillis();
|
||||
this.inactivityTasks.add(getTaskScheduler().scheduleWithFixedDelay(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (System.currentTimeMillis() - lastWriteTime > duration) {
|
||||
try {
|
||||
runnable.run();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("WriteInactivityTask failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, duration / 2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
this.session.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to close session: " + this.session.getId(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encode and decode STOMP WebSocket messages.
|
||||
*/
|
||||
private static class StompWebSocketMessageCodec {
|
||||
|
||||
private static final StompEncoder ENCODER = new StompEncoder();
|
||||
|
||||
private static final StompDecoder DECODER = new StompDecoder();
|
||||
|
||||
private final BufferingStompDecoder bufferingDecoder;
|
||||
|
||||
|
||||
public StompWebSocketMessageCodec(int messageSizeLimit) {
|
||||
this.bufferingDecoder = new BufferingStompDecoder(DECODER, messageSizeLimit);
|
||||
}
|
||||
|
||||
|
||||
public List<Message<byte[]>> decode(WebSocketMessage<?> webSocketMessage) {
|
||||
List<Message<byte[]>> result = Collections.<Message<byte[]>>emptyList();
|
||||
ByteBuffer byteBuffer;
|
||||
if (webSocketMessage instanceof TextMessage) {
|
||||
byteBuffer = ByteBuffer.wrap(((TextMessage) webSocketMessage).asBytes());
|
||||
}
|
||||
else if (webSocketMessage instanceof BinaryMessage) {
|
||||
byteBuffer = ((BinaryMessage) webSocketMessage).getPayload();
|
||||
}
|
||||
else {
|
||||
return result;
|
||||
}
|
||||
result = this.bufferingDecoder.decode(byteBuffer);
|
||||
if (result.isEmpty()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Incomplete STOMP frame content received, bufferSize=" +
|
||||
this.bufferingDecoder.getBufferSize() + ", bufferSizeLimit=" +
|
||||
this.bufferingDecoder.getBufferSizeLimit() + ".");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public WebSocketMessage<?> encode(Message<byte[]> message, Class<? extends WebSocketSession> sessionType) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
Assert.notNull(accessor);
|
||||
byte[] payload = message.getPayload();
|
||||
byte[] bytes = ENCODER.encode(accessor.getMessageHeaders(), payload);
|
||||
|
||||
boolean useBinary = (payload.length > 0 &&
|
||||
!(SockJsSession.class.isAssignableFrom(sessionType)) &&
|
||||
MimeTypeUtils.APPLICATION_OCTET_STREAM.isCompatibleWith(accessor.getContentType()));
|
||||
|
||||
return (useBinary ? new BinaryMessage(bytes) : new TextMessage(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestName;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.converter.StringMessageConverter;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompFrameHandler;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaders;
|
||||
import org.springframework.messaging.simp.stomp.StompSession;
|
||||
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.socket.TomcatWebSocketTestServer;
|
||||
import org.springframework.web.socket.WebSocketTestServer;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurationSupport;
|
||||
import org.springframework.web.socket.server.RequestUpgradeStrategy;
|
||||
import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
|
||||
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
|
||||
|
||||
|
||||
/**
|
||||
* Integration tests for {@link WebSocketStompClient}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebSocketStompClientIntegrationTests {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(WebSocketStompClientIntegrationTests.class);
|
||||
|
||||
@Rule
|
||||
public final TestName testName = new TestName();
|
||||
|
||||
private WebSocketStompClient stompClient;
|
||||
|
||||
private WebSocketTestServer server;
|
||||
|
||||
private AnnotationConfigWebApplicationContext wac;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
logger.debug("Setting up before '" + this.testName.getMethodName() + "'");
|
||||
|
||||
this.wac = new AnnotationConfigWebApplicationContext();
|
||||
this.wac.register(TestConfig.class);
|
||||
this.wac.refresh();
|
||||
|
||||
this.server = new TomcatWebSocketTestServer();
|
||||
this.server.setup();
|
||||
this.server.deployConfig(this.wac);
|
||||
this.server.start();
|
||||
|
||||
WebSocketClient webSocketClient = new StandardWebSocketClient();
|
||||
this.stompClient = new WebSocketStompClient(webSocketClient);
|
||||
this.stompClient.setMessageConverter(new StringMessageConverter());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
try {
|
||||
this.server.undeployConfig();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
logger.error("Failed to undeploy application config", t);
|
||||
}
|
||||
try {
|
||||
this.server.stop();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
logger.error("Failed to stop server", t);
|
||||
}
|
||||
try {
|
||||
this.wac.close();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
logger.error("Failed to close WebApplicationContext", t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void publishSubscribe() throws Exception {
|
||||
|
||||
String url = "ws://127.0.0.1:" + this.server.getPort() + "/stomp";
|
||||
|
||||
TestHandler testHandler = new TestHandler("/topic/foo", "payload");
|
||||
this.stompClient.connect(url, testHandler);
|
||||
|
||||
assertTrue(testHandler.awaitForMessageCount(1, 5000));
|
||||
assertThat(testHandler.getReceived(), containsInAnyOrder("payload"));
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class TestConfig extends WebSocketMessageBrokerConfigurationSupport {
|
||||
|
||||
@Override
|
||||
protected void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// Can't rely on classpath detection
|
||||
RequestUpgradeStrategy upgradeStrategy = new TomcatRequestUpgradeStrategy();
|
||||
registry.addEndpoint("/stomp")
|
||||
.setHandshakeHandler(new DefaultHandshakeHandler(upgradeStrategy))
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry configurer) {
|
||||
configurer.setApplicationDestinationPrefixes("/app");
|
||||
configurer.enableSimpleBroker("/topic", "/queue");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestHandler extends StompSessionHandlerAdapter {
|
||||
|
||||
private final String topic;
|
||||
|
||||
private final Object payload;
|
||||
|
||||
private final List<String> received = new ArrayList<>();
|
||||
|
||||
|
||||
public TestHandler(String topic, Object payload) {
|
||||
this.topic = topic;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
|
||||
public List<String> getReceived() {
|
||||
return this.received;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
|
||||
session.subscribe(this.topic, new StompFrameHandler() {
|
||||
@Override
|
||||
public Type getPayloadType(StompHeaders headers) {
|
||||
return String.class;
|
||||
}
|
||||
@Override
|
||||
public void handleFrame(StompHeaders headers, Object payload) {
|
||||
received.add((String) payload);
|
||||
}
|
||||
});
|
||||
session.send(this.topic, this.payload);
|
||||
}
|
||||
|
||||
public boolean awaitForMessageCount(int expected, long millisToWait) throws InterruptedException {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Awaiting for message count: " + expected);
|
||||
}
|
||||
long startTime = System.currentTimeMillis();
|
||||
while (this.received.size() < expected) {
|
||||
Thread.sleep(500);
|
||||
if ((System.currentTimeMillis() - startTime) > millisToWait) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(StompSession session, StompCommand command,
|
||||
StompHeaders headers, byte[] payload, Throwable ex) {
|
||||
|
||||
logger.error(command + " " + headers, ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFrame(StompHeaders headers, Object payload) {
|
||||
logger.error("STOMP error frame " + headers + " payload=" + payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(StompSession session, Throwable exception) {
|
||||
logger.error(exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.isNotNull;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.simp.stomp.ConnectionHandlingStompSession;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaders;
|
||||
import org.springframework.messaging.simp.stomp.StompSessionHandler;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.messaging.tcp.TcpConnection;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.concurrent.SettableListenableFuture;
|
||||
import org.springframework.web.socket.BinaryMessage;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.PongMessage;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link \WebSocketStompClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebSocketStompClientTests {
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
private TestWebSocketStompClient stompClient;
|
||||
|
||||
@Mock
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
@Mock
|
||||
private ConnectionHandlingStompSession stompSession;
|
||||
|
||||
private ArgumentCaptor<WebSocketHandler> webSocketHandlerCaptor;
|
||||
|
||||
private SettableListenableFuture<WebSocketSession> handshakeFuture;
|
||||
|
||||
@Mock
|
||||
private WebSocketSession webSocketSession;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
WebSocketClient webSocketClient = mock(WebSocketClient.class);
|
||||
this.stompClient = new TestWebSocketStompClient(webSocketClient);
|
||||
this.stompClient.setTaskScheduler(this.taskScheduler);
|
||||
this.stompClient.setStompSession(this.stompSession);
|
||||
|
||||
this.webSocketHandlerCaptor = ArgumentCaptor.forClass(WebSocketHandler.class);
|
||||
this.handshakeFuture = new SettableListenableFuture<>();
|
||||
when(webSocketClient.doHandshake(this.webSocketHandlerCaptor.capture(), any(), any(URI.class)))
|
||||
.thenReturn(this.handshakeFuture);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void webSocketHandshakeFailure() throws Exception {
|
||||
|
||||
connect();
|
||||
|
||||
IllegalStateException handshakeFailure = new IllegalStateException("simulated exception");
|
||||
this.handshakeFuture.setException(handshakeFailure);
|
||||
|
||||
verify(this.stompSession).afterConnectFailure(same(handshakeFailure));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void webSocketConnectionEstablished() throws Exception {
|
||||
connect().afterConnectionEstablished(this.webSocketSession);
|
||||
verify(this.stompSession).afterConnected(isNotNull(TcpConnection.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webSocketTransportError() throws Exception {
|
||||
|
||||
IllegalStateException exception = new IllegalStateException("simulated exception");
|
||||
connect().handleTransportError(this.webSocketSession, exception);
|
||||
|
||||
verify(this.stompSession).handleFailure(same(exception));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webSocketConnectionClosed() throws Exception {
|
||||
connect().afterConnectionClosed(this.webSocketSession, CloseStatus.NORMAL);
|
||||
verify(this.stompSession).afterConnectionClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWebSocketMessage() throws Exception {
|
||||
|
||||
String text = "SEND\na:alpha\n\nMessage payload\0";
|
||||
connect().handleMessage(this.webSocketSession, new TextMessage(text));
|
||||
|
||||
ArgumentCaptor<? extends Message<byte[]>> captor = ArgumentCaptor.forClass(Message.class);
|
||||
verify(this.stompSession).handleMessage(captor.capture());
|
||||
Message<byte[]> message = captor.getValue();
|
||||
assertNotNull(message);
|
||||
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
StompHeaders headers = StompHeaders.readOnlyStompHeaders(accessor.toNativeHeaderMap());
|
||||
assertEquals(StompCommand.SEND, accessor.getCommand());
|
||||
assertEquals("alpha", headers.getFirst("a"));
|
||||
assertEquals("Message payload", new String(message.getPayload(), UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWebSocketMessageSplitAcrossTwoMessage() throws Exception {
|
||||
|
||||
WebSocketHandler webSocketHandler = connect();
|
||||
|
||||
String part1 = "SEND\na:alpha\n\nMessage";
|
||||
webSocketHandler.handleMessage(this.webSocketSession, new TextMessage(part1));
|
||||
|
||||
verifyNoMoreInteractions(this.stompSession);
|
||||
|
||||
String part2 = " payload\0";
|
||||
webSocketHandler.handleMessage(this.webSocketSession, new TextMessage(part2));
|
||||
|
||||
ArgumentCaptor<? extends Message<byte[]>> captor = ArgumentCaptor.forClass(Message.class);
|
||||
verify(this.stompSession).handleMessage(captor.capture());
|
||||
Message<byte[]> message = captor.getValue();
|
||||
assertNotNull(message);
|
||||
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
StompHeaders headers = StompHeaders.readOnlyStompHeaders(accessor.toNativeHeaderMap());
|
||||
assertEquals(StompCommand.SEND, accessor.getCommand());
|
||||
assertEquals("alpha", headers.getFirst("a"));
|
||||
assertEquals("Message payload", new String(message.getPayload(), UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWebSocketMessageBinary() throws Exception {
|
||||
|
||||
String text = "SEND\na:alpha\n\nMessage payload\0";
|
||||
connect().handleMessage(this.webSocketSession, new BinaryMessage(text.getBytes(UTF_8)));
|
||||
|
||||
ArgumentCaptor<? extends Message<byte[]>> captor = ArgumentCaptor.forClass(Message.class);
|
||||
verify(this.stompSession).handleMessage(captor.capture());
|
||||
Message<byte[]> message = captor.getValue();
|
||||
assertNotNull(message);
|
||||
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
StompHeaders headers = StompHeaders.readOnlyStompHeaders(accessor.toNativeHeaderMap());
|
||||
assertEquals(StompCommand.SEND, accessor.getCommand());
|
||||
assertEquals("alpha", headers.getFirst("a"));
|
||||
assertEquals("Message payload", new String(message.getPayload(), UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWebSocketMessagePong() throws Exception {
|
||||
connect().handleMessage(this.webSocketSession, new PongMessage());
|
||||
verifyNoMoreInteractions(this.stompSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWebSocketMessage() throws Exception {
|
||||
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.SEND);
|
||||
accessor.setDestination("/topic/foo");
|
||||
byte[] payload = "payload".getBytes(UTF_8);
|
||||
|
||||
getTcpConnection().send(MessageBuilder.createMessage(payload, accessor.getMessageHeaders()));
|
||||
|
||||
ArgumentCaptor<TextMessage> textMessageCaptor = ArgumentCaptor.forClass(TextMessage.class);
|
||||
verify(this.webSocketSession).sendMessage(textMessageCaptor.capture());
|
||||
TextMessage textMessage = textMessageCaptor.getValue();
|
||||
assertNotNull(textMessage);
|
||||
assertEquals("SEND\ndestination:/topic/foo\ncontent-length:7\n\npayload\0", textMessage.getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWebSocketBinary() throws Exception {
|
||||
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.SEND);
|
||||
accessor.setDestination("/b");
|
||||
accessor.setContentType(MimeTypeUtils.APPLICATION_OCTET_STREAM);
|
||||
byte[] payload = "payload".getBytes(UTF_8);
|
||||
|
||||
getTcpConnection().send(MessageBuilder.createMessage(payload, accessor.getMessageHeaders()));
|
||||
|
||||
ArgumentCaptor<BinaryMessage> binaryMessageCaptor = ArgumentCaptor.forClass(BinaryMessage.class);
|
||||
verify(this.webSocketSession).sendMessage(binaryMessageCaptor.capture());
|
||||
BinaryMessage binaryMessage = binaryMessageCaptor.getValue();
|
||||
assertNotNull(binaryMessage);
|
||||
assertEquals("SEND\ndestination:/b\ncontent-type:application/octet-stream\ncontent-length:7\n\npayload\0",
|
||||
new String(binaryMessage.getPayload().array(), UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heartbeatDefaultValue() throws Exception {
|
||||
WebSocketStompClient stompClient = new WebSocketStompClient(mock(WebSocketClient.class));
|
||||
assertArrayEquals(new long[] {0, 0}, stompClient.getDefaultHeartbeat());
|
||||
|
||||
StompHeaders connectHeaders = stompClient.processConnectHeaders(null);
|
||||
assertArrayEquals(new long[] {0, 0}, connectHeaders.getHeartbeat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heartbeatDefaultValueWithScheduler() throws Exception {
|
||||
WebSocketStompClient stompClient = new WebSocketStompClient(mock(WebSocketClient.class));
|
||||
stompClient.setTaskScheduler(mock(TaskScheduler.class));
|
||||
assertArrayEquals(new long[] {10000, 10000}, stompClient.getDefaultHeartbeat());
|
||||
|
||||
|
||||
StompHeaders connectHeaders = stompClient.processConnectHeaders(null);
|
||||
assertArrayEquals(new long[] {10000, 10000}, connectHeaders.getHeartbeat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heartbeatDefaultValueSetWithoutScheduler() throws Exception {
|
||||
WebSocketStompClient stompClient = new WebSocketStompClient(mock(WebSocketClient.class));
|
||||
stompClient.setDefaultHeartbeat(new long[] {5, 5});
|
||||
try {
|
||||
stompClient.processConnectHeaders(null);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInactivityAfterDelayHasElapsed() throws Exception {
|
||||
TcpConnection<byte[]> tcpConnection = getTcpConnection();
|
||||
Runnable runnable = mock(Runnable.class);
|
||||
long delay = 2;
|
||||
tcpConnection.onReadInactivity(runnable, delay);
|
||||
testInactivityTaskScheduling(runnable, delay, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInactivityBeforeDelayHasElapsed() throws Exception {
|
||||
TcpConnection<byte[]> tcpConnection = getTcpConnection();
|
||||
Runnable runnable = mock(Runnable.class);
|
||||
long delay = 10000;
|
||||
tcpConnection.onReadInactivity(runnable, delay);
|
||||
testInactivityTaskScheduling(runnable, delay, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInactivityAfterDelayHasElapsed() throws Exception {
|
||||
TcpConnection<byte[]> tcpConnection = getTcpConnection();
|
||||
Runnable runnable = mock(Runnable.class);
|
||||
long delay = 2;
|
||||
tcpConnection.onWriteInactivity(runnable, delay);
|
||||
testInactivityTaskScheduling(runnable, delay, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInactivityBeforeDelayHasElapsed() throws Exception {
|
||||
TcpConnection<byte[]> tcpConnection = getTcpConnection();
|
||||
Runnable runnable = mock(Runnable.class);
|
||||
long delay = 1000;
|
||||
tcpConnection.onWriteInactivity(runnable, delay);
|
||||
testInactivityTaskScheduling(runnable, delay, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelInactivityTasks() throws Exception {
|
||||
|
||||
TcpConnection<byte[]> tcpConnection = getTcpConnection();
|
||||
|
||||
ScheduledFuture future = mock(ScheduledFuture.class);
|
||||
when(this.taskScheduler.scheduleWithFixedDelay(any(), eq(1L))).thenReturn(future);
|
||||
|
||||
tcpConnection.onReadInactivity(mock(Runnable.class), 2L);
|
||||
tcpConnection.onWriteInactivity(mock(Runnable.class), 2L);
|
||||
|
||||
this.webSocketHandlerCaptor.getValue().afterConnectionClosed(this.webSocketSession, CloseStatus.NORMAL);
|
||||
|
||||
verify(future, times(2)).cancel(true);
|
||||
verifyNoMoreInteractions(future);
|
||||
}
|
||||
|
||||
|
||||
private WebSocketHandler connect() {
|
||||
|
||||
this.stompClient.connect("/foo", mock(StompSessionHandler.class));
|
||||
|
||||
verify(this.stompSession).getSessionFuture();
|
||||
verifyNoMoreInteractions(this.stompSession);
|
||||
|
||||
WebSocketHandler webSocketHandler = this.webSocketHandlerCaptor.getValue();
|
||||
assertNotNull(webSocketHandler);
|
||||
return webSocketHandler;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private TcpConnection<byte[]> getTcpConnection() throws Exception {
|
||||
WebSocketHandler webSocketHandler = connect();
|
||||
webSocketHandler.afterConnectionEstablished(this.webSocketSession);
|
||||
return (TcpConnection<byte[]>) webSocketHandler;
|
||||
}
|
||||
|
||||
private void testInactivityTaskScheduling(Runnable runnable, long delay, long sleepTime)
|
||||
throws InterruptedException {
|
||||
|
||||
ArgumentCaptor<Runnable> inactivityTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(this.taskScheduler).scheduleWithFixedDelay(inactivityTaskCaptor.capture(), eq(delay/2));
|
||||
verifyNoMoreInteractions(this.taskScheduler);
|
||||
|
||||
if (sleepTime > 0) {
|
||||
Thread.sleep(sleepTime);
|
||||
}
|
||||
|
||||
Runnable inactivityTask = inactivityTaskCaptor.getValue();
|
||||
assertNotNull(inactivityTask);
|
||||
inactivityTask.run();
|
||||
|
||||
if (sleepTime > 0) {
|
||||
verify(runnable).run();
|
||||
}
|
||||
else {
|
||||
verifyNoMoreInteractions(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestWebSocketStompClient extends WebSocketStompClient {
|
||||
|
||||
private ConnectionHandlingStompSession stompSession;
|
||||
|
||||
|
||||
public TestWebSocketStompClient(WebSocketClient webSocketClient) {
|
||||
super(webSocketClient);
|
||||
}
|
||||
|
||||
public void setStompSession(ConnectionHandlingStompSession stompSession) {
|
||||
this.stompSession = stompSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConnectionHandlingStompSession createSession(StompHeaders headers, StompSessionHandler handler) {
|
||||
return this.stompSession;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user