Support heartbeat in SimpleBrokerMessageHandler
Issue: SPR-10954
This commit is contained in:
parent
595cdf05e9
commit
de9675bf5a
|
|
@ -51,8 +51,6 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
|
|||
|
||||
// SiMP header names
|
||||
|
||||
public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage";
|
||||
|
||||
public static final String DESTINATION_HEADER = "simpDestination";
|
||||
|
||||
public static final String MESSAGE_TYPE_HEADER = "simpMessageType";
|
||||
|
|
@ -65,6 +63,11 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
|
|||
|
||||
public static final String USER_HEADER = "simpUser";
|
||||
|
||||
public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage";
|
||||
|
||||
public static final String HEART_BEAT_HEADER = "simpHeartbeat";
|
||||
|
||||
|
||||
/**
|
||||
* For internal use.
|
||||
* <p>The original destination used by a client when subscribing. Such a
|
||||
|
|
@ -262,4 +265,8 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
|
|||
return (Principal) headers.get(USER_HEADER);
|
||||
}
|
||||
|
||||
public static long[] getHeartbeat(Map<String, Object> headers) {
|
||||
return (long[]) headers.get(HEART_BEAT_HEADER);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ public enum SimpMessageType {
|
|||
|
||||
CONNECT_ACK,
|
||||
|
||||
HEARTBEAT,
|
||||
|
||||
MESSAGE,
|
||||
|
||||
SUBSCRIBE,
|
||||
|
||||
UNSUBSCRIBE,
|
||||
|
||||
HEARTBEAT,
|
||||
|
||||
DISCONNECT,
|
||||
|
||||
DISCONNECT_ACK,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@
|
|||
|
||||
package org.springframework.messaging.simp.broker;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
|
@ -27,6 +31,7 @@ import org.springframework.messaging.simp.SimpMessageType;
|
|||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.messaging.support.MessageHeaderInitializer;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
|
@ -43,10 +48,18 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
|
||||
private static final byte[] EMPTY_PAYLOAD = new byte[0];
|
||||
|
||||
private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
|
||||
|
||||
private SubscriptionRegistry subscriptionRegistry;
|
||||
|
||||
private PathMatcher pathMatcher;
|
||||
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
private long[] heartbeatValue;
|
||||
|
||||
private ScheduledFuture<?> heartbeatFuture;
|
||||
|
||||
private MessageHeaderInitializer headerInitializer;
|
||||
|
||||
|
||||
|
|
@ -100,6 +113,49 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
initPathMatcherToUse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link org.springframework.scheduling.TaskScheduler} to
|
||||
* use for providing heartbeat support. Setting this property also sets the
|
||||
* {@link #setHeartbeatValue heartbeatValue} to "10000, 10000".
|
||||
* <p>By default this is not set.
|
||||
* @since 4.2
|
||||
*/
|
||||
public void setTaskScheduler(TaskScheduler taskScheduler) {
|
||||
Assert.notNull(taskScheduler);
|
||||
this.taskScheduler = taskScheduler;
|
||||
if (this.heartbeatValue == null) {
|
||||
this.heartbeatValue = new long[] {10000, 10000};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured TaskScheduler.
|
||||
*/
|
||||
public TaskScheduler getTaskScheduler() {
|
||||
return this.taskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the value for the heart-beat settings. The first number
|
||||
* represents how often the server will write or send a heartbeat.
|
||||
* The second is how often the client should write. 0 means no heartbeats.
|
||||
* <p>By default this is set to "0, 0" unless the {@link #setTaskScheduler
|
||||
* taskScheduler} in which case the default becomes "10000,10000"
|
||||
* (in milliseconds).
|
||||
* @since 4.2
|
||||
*/
|
||||
public void setHeartbeatValue(long[] heartbeat) {
|
||||
Assert.notNull(heartbeat);
|
||||
this.heartbeatValue = heartbeat;
|
||||
}
|
||||
|
||||
/**
|
||||
* The configured value for the heart-beat settings.
|
||||
*/
|
||||
public long[] getHeartbeatValue() {
|
||||
return this.heartbeatValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link MessageHeaderInitializer} to apply to the headers
|
||||
* of all messages sent to the client outbound channel.
|
||||
|
|
@ -120,11 +176,37 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
@Override
|
||||
public void startInternal() {
|
||||
publishBrokerAvailableEvent();
|
||||
if (getTaskScheduler() != null) {
|
||||
long interval = initHeartbeatTaskDelay();
|
||||
if (interval > 0) {
|
||||
this.heartbeatFuture = this.taskScheduler.scheduleWithFixedDelay(new HeartbeatTask(), interval);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Assert.isTrue(getHeartbeatValue() == null ||
|
||||
(getHeartbeatValue()[0] == 0 && getHeartbeatValue()[1] == 0),
|
||||
"Heartbeat values configured but no TaskScheduler is provided.");
|
||||
}
|
||||
}
|
||||
|
||||
private long initHeartbeatTaskDelay() {
|
||||
if (getHeartbeatValue() == null) {
|
||||
return 0;
|
||||
}
|
||||
else if (getHeartbeatValue()[0] > 0 && getHeartbeatValue()[1] > 0) {
|
||||
return Math.min(getHeartbeatValue()[0], getHeartbeatValue()[1]);
|
||||
}
|
||||
else {
|
||||
return (getHeartbeatValue()[0] > 0 ? getHeartbeatValue()[0] : getHeartbeatValue()[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopInternal() {
|
||||
publishBrokerUnavailableEvent();
|
||||
if (this.heartbeatFuture != null) {
|
||||
this.heartbeatFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -133,6 +215,9 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(headers);
|
||||
String destination = SimpMessageHeaderAccessor.getDestination(headers);
|
||||
String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
|
||||
Principal user = SimpMessageHeaderAccessor.getUser(headers);
|
||||
|
||||
updateSessionReadTime(sessionId);
|
||||
|
||||
if (!checkDestinationPrefix(destination)) {
|
||||
return;
|
||||
|
|
@ -150,23 +235,21 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
}
|
||||
else if (SimpMessageType.CONNECT.equals(messageType)) {
|
||||
logMessage(message);
|
||||
long[] clientHeartbeat = SimpMessageHeaderAccessor.getHeartbeat(headers);
|
||||
long[] serverHeartbeat = getHeartbeatValue();
|
||||
this.sessions.put(sessionId, new SessionInfo(sessionId, user, clientHeartbeat, serverHeartbeat));
|
||||
SimpMessageHeaderAccessor connectAck = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
|
||||
initHeaders(connectAck);
|
||||
connectAck.setSessionId(sessionId);
|
||||
connectAck.setUser(SimpMessageHeaderAccessor.getUser(headers));
|
||||
connectAck.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, message);
|
||||
connectAck.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, serverHeartbeat);
|
||||
Message<byte[]> messageOut = MessageBuilder.createMessage(EMPTY_PAYLOAD, connectAck.getMessageHeaders());
|
||||
getClientOutboundChannel().send(messageOut);
|
||||
}
|
||||
else if (SimpMessageType.DISCONNECT.equals(messageType)) {
|
||||
logMessage(message);
|
||||
this.subscriptionRegistry.unregisterAllSubscriptions(sessionId);
|
||||
SimpMessageHeaderAccessor disconnectAck = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT_ACK);
|
||||
initHeaders(disconnectAck);
|
||||
disconnectAck.setSessionId(sessionId);
|
||||
disconnectAck.setUser(SimpMessageHeaderAccessor.getUser(headers));
|
||||
Message<byte[]> messageOut = MessageBuilder.createMessage(EMPTY_PAYLOAD, disconnectAck.getMessageHeaders());
|
||||
getClientOutboundChannel().send(messageOut);
|
||||
handleDisconnect(sessionId, user);
|
||||
}
|
||||
else if (SimpMessageType.SUBSCRIBE.equals(messageType)) {
|
||||
logMessage(message);
|
||||
|
|
@ -178,6 +261,15 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateSessionReadTime(String sessionId) {
|
||||
if (sessionId != null) {
|
||||
SessionInfo info = this.sessions.get(sessionId);
|
||||
if (info != null) {
|
||||
info.setLastReadTime(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logMessage(Message<?> message) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
SimpMessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);
|
||||
|
|
@ -192,11 +284,23 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleDisconnect(String sessionId, Principal user) {
|
||||
this.sessions.remove(sessionId);
|
||||
this.subscriptionRegistry.unregisterAllSubscriptions(sessionId);
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT_ACK);
|
||||
accessor.setSessionId(sessionId);
|
||||
accessor.setUser(user);
|
||||
initHeaders(accessor);
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
|
||||
getClientOutboundChannel().send(message);
|
||||
}
|
||||
|
||||
protected void sendMessageToSubscribers(String destination, Message<?> message) {
|
||||
MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
|
||||
if (!subscriptions.isEmpty() && logger.isDebugEnabled()) {
|
||||
logger.debug("Broadcasting to " + subscriptions.size() + " sessions.");
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
for (String sessionId : subscriptions.keySet()) {
|
||||
for (String subscriptionId : subscriptions.get(sessionId)) {
|
||||
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
|
|
@ -212,6 +316,12 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
catch (Throwable ex) {
|
||||
logger.error("Failed to send " + message, ex);
|
||||
}
|
||||
finally {
|
||||
SessionInfo info = this.sessions.get(sessionId);
|
||||
if (info != null) {
|
||||
info.setLastWriteTime(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,4 +331,93 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
|
|||
return "SimpleBroker[" + this.subscriptionRegistry + "]";
|
||||
}
|
||||
|
||||
|
||||
private static class SessionInfo {
|
||||
|
||||
/* STOMP spec: receiver SHOULD take into account an error margin */
|
||||
private static final long HEARTBEAT_MULTIPLIER = 3;
|
||||
|
||||
|
||||
private final String sessiondId;
|
||||
|
||||
private final Principal user;
|
||||
|
||||
private final long readInterval;
|
||||
|
||||
private final long writeInterval;
|
||||
|
||||
private volatile long lastReadTime;
|
||||
|
||||
private volatile long lastWriteTime;
|
||||
|
||||
|
||||
public SessionInfo(String sessiondId, Principal user, long[] clientHeartbeat, long[] serverHeartbeat) {
|
||||
this.sessiondId = sessiondId;
|
||||
this.user = user;
|
||||
if (clientHeartbeat != null && serverHeartbeat != null) {
|
||||
this.readInterval = (clientHeartbeat[0] > 0 && serverHeartbeat[1] > 0 ?
|
||||
Math.max(clientHeartbeat[0], serverHeartbeat[1]) * HEARTBEAT_MULTIPLIER : 0);
|
||||
this.writeInterval = (clientHeartbeat[1] > 0 && serverHeartbeat[0] > 0 ?
|
||||
Math.max(clientHeartbeat[1], serverHeartbeat[0]) : 0);
|
||||
}
|
||||
else {
|
||||
this.readInterval = 0;
|
||||
this.writeInterval = 0;
|
||||
}
|
||||
this.lastReadTime = this.lastWriteTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public String getSessiondId() {
|
||||
return this.sessiondId;
|
||||
}
|
||||
|
||||
public Principal getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
public long getReadInterval() {
|
||||
return this.readInterval;
|
||||
}
|
||||
|
||||
public long getWriteInterval() {
|
||||
return this.writeInterval;
|
||||
}
|
||||
|
||||
public long getLastReadTime() {
|
||||
return this.lastReadTime;
|
||||
}
|
||||
|
||||
public void setLastReadTime(long lastReadTime) {
|
||||
this.lastReadTime = lastReadTime;
|
||||
}
|
||||
|
||||
public long getLastWriteTime() {
|
||||
return this.lastWriteTime;
|
||||
}
|
||||
|
||||
public void setLastWriteTime(long lastWriteTime) {
|
||||
this.lastWriteTime = lastWriteTime;
|
||||
}
|
||||
}
|
||||
|
||||
private class HeartbeatTask implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (SessionInfo info : sessions.values()) {
|
||||
if (info.getReadInterval() > 0 && (now - info.getLastReadTime()) > info.getReadInterval()) {
|
||||
handleDisconnect(info.getSessiondId(), info.getUser());
|
||||
}
|
||||
if (info.getWriteInterval() > 0 && (now - info.getLastWriteTime()) > info.getWriteInterval()) {
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
|
||||
accessor.setSessionId(info.getSessiondId());
|
||||
accessor.setUser(info.getUser());
|
||||
initHeaders(accessor);
|
||||
MessageHeaders headers = accessor.getMessageHeaders();
|
||||
getClientOutboundChannel().send(MessageBuilder.createMessage(EMPTY_PAYLOAD, headers));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.springframework.messaging.simp.config;
|
|||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
/**
|
||||
* Registration class for configuring a {@link SimpleBrokerMessageHandler}.
|
||||
|
|
@ -28,14 +29,54 @@ import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
|
|||
*/
|
||||
public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
|
||||
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
private long[] heartbeat;
|
||||
|
||||
|
||||
public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
|
||||
super(inChannel, outChannel, prefixes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure the {@link org.springframework.scheduling.TaskScheduler} to
|
||||
* use for providing heartbeat support. Setting this property also sets the
|
||||
* {@link #setHeartbeatValue heartbeatValue} to "10000, 10000".
|
||||
* <p>By default this is not set.
|
||||
* @since 4.2
|
||||
*/
|
||||
public SimpleBrokerRegistration setTaskScheduler(TaskScheduler taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the value for the heartbeat settings. The first number
|
||||
* represents how often the server will write or send a heartbeat.
|
||||
* The second is how often the client should write. 0 means no heartbeats.
|
||||
* <p>By default this is set to "0, 0" unless the {@link #setTaskScheduler
|
||||
* taskScheduler} in which case the default becomes "10000,10000"
|
||||
* (in milliseconds).
|
||||
* @since 4.2
|
||||
*/
|
||||
public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
|
||||
this.heartbeat = heartbeat;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
|
||||
return new SimpleBrokerMessageHandler(getClientInboundChannel(),
|
||||
SimpleBrokerMessageHandler handler = new SimpleBrokerMessageHandler(getClientInboundChannel(),
|
||||
getClientOutboundChannel(), brokerChannel, getDestinationPrefixes());
|
||||
if (this.taskScheduler != null) {
|
||||
handler.setTaskScheduler(this.taskScheduler);
|
||||
}
|
||||
if (this.heartbeat != null) {
|
||||
handler.setHeartbeatValue(this.heartbeat);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
|
|||
}
|
||||
}
|
||||
else if (StompCommand.CONNECTED.equals(command)) {
|
||||
initHeartbeats(stompHeaders);
|
||||
initHeartbeatTasks(stompHeaders);
|
||||
this.sessionFuture.set(this);
|
||||
this.sessionHandler.afterConnected(this, stompHeaders);
|
||||
}
|
||||
|
|
@ -420,20 +420,18 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
|
|||
handler.handleFrame(stompHeaders, object);
|
||||
}
|
||||
|
||||
private void initHeartbeats(StompHeaders connectedHeaders) {
|
||||
long clientRead = this.connectHeaders.getHeartbeat()[0];
|
||||
long serverWrite = connectedHeaders.getHeartbeat()[1];
|
||||
|
||||
if (clientRead > 0 && serverWrite > 0) {
|
||||
long interval = Math.max(clientRead, serverWrite);
|
||||
private void initHeartbeatTasks(StompHeaders connectedHeaders) {
|
||||
long[] connect = this.connectHeaders.getHeartbeat();
|
||||
long[] connected = connectedHeaders.getHeartbeat();
|
||||
if (connect == null || connected == null) {
|
||||
return;
|
||||
}
|
||||
if (connect[0] > 0 && connected[1] > 0) {
|
||||
long interval = Math.max(connect[0], connected[1]);
|
||||
this.connection.onWriteInactivity(new WriteInactivityTask(), interval);
|
||||
}
|
||||
|
||||
long clientWrite = this.connectHeaders.getHeartbeat()[1];
|
||||
long serverRead = connectedHeaders.getHeartbeat()[0];
|
||||
|
||||
if (clientWrite > 0 && serverRead > 0) {
|
||||
final long interval = Math.max(clientWrite, serverRead) * HEARTBEAT_MULTIPLIER;
|
||||
if (connect[1] > 0 && connected[0] > 0) {
|
||||
final long interval = Math.max(connect[1], connected[0]) * HEARTBEAT_MULTIPLIER;
|
||||
this.connection.onReadInactivity(new ReadInactivityTask(), interval);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@
|
|||
|
||||
package org.springframework.messaging.simp.broker;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
|
@ -29,13 +32,16 @@ import org.mockito.ArgumentCaptor;
|
|||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.TestPrincipal;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
/**
|
||||
* Unit tests for SimpleBrokerMessageHandler.
|
||||
|
|
@ -43,6 +49,7 @@ import org.springframework.messaging.support.MessageBuilder;
|
|||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class SimpleBrokerMessageHandlerTests {
|
||||
|
||||
private SimpleBrokerMessageHandler messageHandler;
|
||||
|
|
@ -56,6 +63,9 @@ public class SimpleBrokerMessageHandlerTests {
|
|||
@Mock
|
||||
private SubscribableChannel brokerChannel;
|
||||
|
||||
@Mock
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<Message<?>> messageCaptor;
|
||||
|
||||
|
|
@ -133,11 +143,11 @@ public class SimpleBrokerMessageHandlerTests {
|
|||
@Test
|
||||
public void connect() {
|
||||
|
||||
String sess1 = "sess1";
|
||||
|
||||
this.messageHandler.start();
|
||||
|
||||
Message<String> connectMessage = createConnectMessage(sess1);
|
||||
String id = "sess1";
|
||||
Message<String> connectMessage = createConnectMessage(id, new TestPrincipal("joe"), null);
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.handleMessage(connectMessage);
|
||||
|
||||
verify(this.clientOutboundChannel, times(1)).send(this.messageCaptor.capture());
|
||||
|
|
@ -145,10 +155,150 @@ public class SimpleBrokerMessageHandlerTests {
|
|||
|
||||
SimpMessageHeaderAccessor connectAckHeaders = SimpMessageHeaderAccessor.wrap(connectAckMessage);
|
||||
assertEquals(connectMessage, connectAckHeaders.getHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER));
|
||||
assertEquals(sess1, connectAckHeaders.getSessionId());
|
||||
assertEquals(id, connectAckHeaders.getSessionId());
|
||||
assertEquals("joe", connectAckHeaders.getUser().getName());
|
||||
assertArrayEquals(new long[] {10000, 10000},
|
||||
SimpMessageHeaderAccessor.getHeartbeat(connectAckHeaders.getMessageHeaders()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heartbeatValueWithAndWithoutTaskScheduler() throws Exception {
|
||||
|
||||
assertNull(this.messageHandler.getHeartbeatValue());
|
||||
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
|
||||
assertNotNull(this.messageHandler.getHeartbeatValue());
|
||||
assertArrayEquals(new long[] {10000, 10000}, this.messageHandler.getHeartbeatValue());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void startWithHeartbeatValueWithoutTaskScheduler() throws Exception {
|
||||
this.messageHandler.setHeartbeatValue(new long[] {10000, 10000});
|
||||
this.messageHandler.start();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void startAndStopWithHeartbeatValue() throws Exception {
|
||||
|
||||
ScheduledFuture future = mock(ScheduledFuture.class);
|
||||
when(this.taskScheduler.scheduleWithFixedDelay(any(Runnable.class), eq(15000L))).thenReturn(future);
|
||||
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.setHeartbeatValue(new long[] {15000, 16000});
|
||||
this.messageHandler.start();
|
||||
|
||||
verify(this.taskScheduler).scheduleWithFixedDelay(any(Runnable.class), eq(15000L));
|
||||
verifyNoMoreInteractions(this.taskScheduler, future);
|
||||
|
||||
this.messageHandler.stop();
|
||||
|
||||
verify(future).cancel(true);
|
||||
verifyNoMoreInteractions(future);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void startWithOneZeroHeartbeatValue() throws Exception {
|
||||
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.setHeartbeatValue(new long[] {0, 10000});
|
||||
this.messageHandler.start();
|
||||
|
||||
verify(this.taskScheduler).scheduleWithFixedDelay(any(Runnable.class), eq(10000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInactivity() throws Exception {
|
||||
|
||||
this.messageHandler.setHeartbeatValue(new long[] {0, 1});
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.start();
|
||||
|
||||
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(this.taskScheduler).scheduleWithFixedDelay(taskCaptor.capture(), eq(1L));
|
||||
Runnable heartbeatTask = taskCaptor.getValue();
|
||||
assertNotNull(heartbeatTask);
|
||||
|
||||
String id = "sess1";
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<String> connectMessage = createConnectMessage(id, user, new long[] {1, 0});
|
||||
this.messageHandler.handleMessage(connectMessage);
|
||||
|
||||
Thread.sleep(10);
|
||||
heartbeatTask.run();
|
||||
|
||||
verify(this.clientOutboundChannel, atLeast(2)).send(this.messageCaptor.capture());
|
||||
List<Message<?>> messages = this.messageCaptor.getAllValues();
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
MessageHeaders headers = messages.get(0).getHeaders();
|
||||
assertEquals(SimpMessageType.CONNECT_ACK, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
|
||||
headers = messages.get(1).getHeaders();
|
||||
assertEquals(SimpMessageType.DISCONNECT_ACK, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
|
||||
assertEquals(id, headers.get(SimpMessageHeaderAccessor.SESSION_ID_HEADER));
|
||||
assertEquals(user, headers.get(SimpMessageHeaderAccessor.USER_HEADER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInactivity() throws Exception {
|
||||
|
||||
this.messageHandler.setHeartbeatValue(new long[] {1, 0});
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.start();
|
||||
|
||||
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(this.taskScheduler).scheduleWithFixedDelay(taskCaptor.capture(), eq(1L));
|
||||
Runnable heartbeatTask = taskCaptor.getValue();
|
||||
assertNotNull(heartbeatTask);
|
||||
|
||||
String id = "sess1";
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<String> connectMessage = createConnectMessage(id, user, new long[] {0, 1});
|
||||
this.messageHandler.handleMessage(connectMessage);
|
||||
|
||||
Thread.sleep(10);
|
||||
heartbeatTask.run();
|
||||
|
||||
verify(this.clientOutboundChannel, times(2)).send(this.messageCaptor.capture());
|
||||
List<Message<?>> messages = this.messageCaptor.getAllValues();
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
MessageHeaders headers = messages.get(0).getHeaders();
|
||||
assertEquals(SimpMessageType.CONNECT_ACK, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
|
||||
headers = messages.get(1).getHeaders();
|
||||
assertEquals(SimpMessageType.HEARTBEAT, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
|
||||
assertEquals(id, headers.get(SimpMessageHeaderAccessor.SESSION_ID_HEADER));
|
||||
assertEquals(user, headers.get(SimpMessageHeaderAccessor.USER_HEADER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readWriteIntervalCalculation() throws Exception {
|
||||
|
||||
this.messageHandler.setHeartbeatValue(new long[] {1, 1});
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.start();
|
||||
|
||||
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(this.taskScheduler).scheduleWithFixedDelay(taskCaptor.capture(), eq(1L));
|
||||
Runnable heartbeatTask = taskCaptor.getValue();
|
||||
assertNotNull(heartbeatTask);
|
||||
|
||||
String id = "sess1";
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<String> connectMessage = createConnectMessage(id, user, new long[] {10000, 10000});
|
||||
this.messageHandler.handleMessage(connectMessage);
|
||||
|
||||
Thread.sleep(10);
|
||||
heartbeatTask.run();
|
||||
|
||||
verify(this.clientOutboundChannel, times(1)).send(this.messageCaptor.capture());
|
||||
List<Message<?>> messages = this.messageCaptor.getAllValues();
|
||||
assertEquals(1, messages.size());
|
||||
assertEquals(SimpMessageType.CONNECT_ACK,
|
||||
messages.get(0).getHeaders().get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
|
||||
}
|
||||
|
||||
private Message<String> createSubscriptionMessage(String sessionId, String subcriptionId, String destination) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
|
||||
|
|
@ -158,17 +308,18 @@ public class SimpleBrokerMessageHandlerTests {
|
|||
return MessageBuilder.createMessage("", headers.getMessageHeaders());
|
||||
}
|
||||
|
||||
private Message<String> createConnectMessage(String sessionId) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
|
||||
headers.setSessionId(sessionId);
|
||||
headers.setUser(new TestPrincipal("joe"));
|
||||
return MessageBuilder.createMessage("", headers.getMessageHeaders());
|
||||
private Message<String> createConnectMessage(String sessionId, Principal user, long[] heartbeat) {
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
|
||||
accessor.setSessionId(sessionId);
|
||||
accessor.setUser(user);
|
||||
accessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, heartbeat);
|
||||
return MessageBuilder.createMessage("", accessor.getMessageHeaders());
|
||||
}
|
||||
|
||||
private Message<String> createMessage(String destination, String payload) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
headers.setDestination(destination);
|
||||
return MessageBuilder.createMessage("", headers.getMessageHeaders());
|
||||
return MessageBuilder.createMessage(payload, headers.getMessageHeaders());
|
||||
}
|
||||
|
||||
private boolean messageCaptured(String sessionId, String subcriptionId, String destination) {
|
||||
|
|
|
|||
|
|
@ -320,6 +320,14 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
|||
String pathMatcherRef = messageBrokerElement.getAttribute("path-matcher");
|
||||
brokerDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef));
|
||||
}
|
||||
if (simpleBrokerElem.hasAttribute("scheduler")) {
|
||||
String scheduler = simpleBrokerElem.getAttribute("scheduler");
|
||||
brokerDef.getPropertyValues().add("taskScheduler", new RuntimeBeanReference(scheduler));
|
||||
}
|
||||
if (simpleBrokerElem.hasAttribute("heartbeat")) {
|
||||
String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
|
||||
brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
|
||||
}
|
||||
}
|
||||
else if (brokerRelayElem != null) {
|
||||
String prefix = brokerRelayElem.getAttribute("prefix");
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.simp.SimpAttributes;
|
||||
import org.springframework.messaging.simp.SimpAttributesContextHolder;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
|
|
@ -233,17 +234,18 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
|||
StompHeaderAccessor headerAccessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));
|
||||
}
|
||||
|
||||
headerAccessor.setSessionId(session.getId());
|
||||
headerAccessor.setSessionAttributes(session.getAttributes());
|
||||
headerAccessor.setUser(session.getPrincipal());
|
||||
headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());
|
||||
if (!detectImmutableMessageInterceptor(outputChannel)) {
|
||||
headerAccessor.setImmutable();
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));
|
||||
}
|
||||
|
||||
if (StompCommand.CONNECT.equals(headerAccessor.getCommand())) {
|
||||
this.stats.incrementConnectCount();
|
||||
}
|
||||
|
|
@ -401,13 +403,17 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
|||
}
|
||||
else if (accessor instanceof SimpMessageHeaderAccessor) {
|
||||
stompAccessor = StompHeaderAccessor.wrap(message);
|
||||
if (SimpMessageType.CONNECT_ACK.equals(stompAccessor.getMessageType())) {
|
||||
SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(message.getHeaders());
|
||||
if (SimpMessageType.CONNECT_ACK.equals(messageType)) {
|
||||
stompAccessor = convertConnectAcktoStompConnected(stompAccessor);
|
||||
}
|
||||
else if (SimpMessageType.DISCONNECT_ACK.equals(stompAccessor.getMessageType())) {
|
||||
else if (SimpMessageType.DISCONNECT_ACK.equals(messageType)) {
|
||||
stompAccessor = StompHeaderAccessor.create(StompCommand.ERROR);
|
||||
stompAccessor.setMessage("Session closed.");
|
||||
}
|
||||
else if (SimpMessageType.HEARTBEAT.equals(messageType)) {
|
||||
stompAccessor = StompHeaderAccessor.createForHeartbeat();
|
||||
}
|
||||
else if (stompAccessor.getCommand() == null || StompCommand.SEND.equals(stompAccessor.getCommand())) {
|
||||
stompAccessor.updateStompCommandAsServerMessage();
|
||||
}
|
||||
|
|
@ -429,23 +435,21 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
|||
Message<?> message = (Message<?>) connectAckHeaders.getHeader(name);
|
||||
Assert.notNull(message, "Original STOMP CONNECT not found in " + connectAckHeaders);
|
||||
StompHeaderAccessor connectHeaders = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
String version;
|
||||
StompHeaderAccessor connectedHeaders = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
Set<String> acceptVersions = connectHeaders.getAcceptVersion();
|
||||
if (acceptVersions.contains("1.2")) {
|
||||
version = "1.2";
|
||||
connectedHeaders.setVersion("1.2");
|
||||
}
|
||||
else if (acceptVersions.contains("1.1")) {
|
||||
version = "1.1";
|
||||
connectedHeaders.setVersion("1.1");
|
||||
}
|
||||
else if (acceptVersions.isEmpty()) {
|
||||
version = null;
|
||||
}
|
||||
else {
|
||||
else if (!acceptVersions.isEmpty()) {
|
||||
throw new IllegalArgumentException("Unsupported STOMP version '" + acceptVersions + "'");
|
||||
}
|
||||
StompHeaderAccessor connectedHeaders = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
connectedHeaders.setVersion(version);
|
||||
connectedHeaders.setHeartbeat(0, 0); // not supported
|
||||
long[] heartbeat = (long[]) connectAckHeaders.getHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER);
|
||||
if (heartbeat != null) {
|
||||
connectedHeaders.setHeartbeat(heartbeat[0], heartbeat[1]);
|
||||
}
|
||||
return connectedHeaders;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
http\://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd=org/springframework/web/socket/config/spring-websocket-4.0.xsd
|
||||
http\://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd=org/springframework/web/socket/config/spring-websocket-4.1.xsd
|
||||
http\://www.springframework.org/schema/websocket/spring-websocket.xsd=org/springframework/web/socket/config/spring-websocket-4.1.xsd
|
||||
http\://www.springframework.org/schema/websocket/spring-websocket.xsd=org/springframework/web/socket/config/spring-websocket-4.2.xsd
|
||||
|
|
|
|||
|
|
@ -0,0 +1,896 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
~ Copyright 2002-2014 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.
|
||||
-->
|
||||
|
||||
<xsd:schema xmlns="http://www.springframework.org/schema/websocket"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:tool="http://www.springframework.org/schema/tool"
|
||||
targetNamespace="http://www.springframework.org/schema/websocket"
|
||||
elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
|
||||
<xsd:import namespace="http://www.springframework.org/schema/beans" schemaLocation="http://www.springframework.org/schema/beans/spring-beans.xsd"/>
|
||||
<xsd:import namespace="http://www.springframework.org/schema/tool" schemaLocation="http://www.springframework.org/schema/tool/spring-tool.xsd" />
|
||||
|
||||
<xsd:complexType name="mapping">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
An entry in the registered HandlerMapping that matches a path with a handler.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:attribute name="path" type="xsd:string" use="required">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A path that maps a particular request to a handler.
|
||||
Exact path mapping URIs (such as "/myPath") are supported as well as Ant-stype path patterns (such as /myPath/**).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="handler" type="xsd:string" use="required">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.WebSocketHandler"><![CDATA[
|
||||
The bean name of a WebSocketHandler to use for requests that match the path configuration.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="handshake-handler">
|
||||
<xsd:attribute name="ref" type="xsd:string" use="required">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.server.HandshakeHandler"><![CDATA[
|
||||
The bean name of a HandshakeHandler to use for processing WebSocket handshake requests.
|
||||
If none specified, a DefaultHandshakeHandler will be configured by default.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="handshake-interceptors">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.server.HandshakeInterceptor"><![CDATA[
|
||||
A list of HandshakeInterceptor beans definition and references.
|
||||
A HandshakeInterceptor can be used to inspect the handshake request and response as well as to pass attributes to the target WebSocketHandler.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:sequence>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.server.HandshakeInterceptor"><![CDATA[
|
||||
A HandshakeInterceptor bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.server.HandshakeInterceptor"><![CDATA[
|
||||
A reference to a HandshakeInterceptor bean.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="sockjs-service">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService"><![CDATA[
|
||||
Configures a DefaultSockJsService for processing HTTP requests from SockJS clients.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="transport-handlers" minOccurs="0" maxOccurs="1">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.sockjs.transport.TransportHandler"><![CDATA[
|
||||
List of TransportHandler beans to be configured for the current handlers element.
|
||||
One can choose not to register the default TransportHandlers and/or override those using
|
||||
custom TransportHandlers.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A TransportHandler bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A reference to a TransportHandler bean.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="register-defaults" type="xsd:boolean" default="true">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Whether or not default TransportHandlers registrations should be added in addition to the ones provided within this element.
|
||||
Default registrations include XhrPollingTransportHandler, XhrReceivingTransportHandler,
|
||||
JsonpPollingTransportHandler, JsonpReceivingTransportHandler, XhrStreamingTransportHandler,
|
||||
EventSourceTransportHandler, HtmlFileTransportHandler, and WebSocketTransportHandler.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
A unique name for the service, mainly for logging purposes.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="client-library-url" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
Transports with no native cross-domain communication (e.g. "eventsource",
|
||||
"htmlfile") must get a simple page from the "foreign" domain in an invisible
|
||||
iframe so that code in the iframe can run from a domain local to the SockJS
|
||||
server. Since the iframe needs to load the SockJS javascript client library,
|
||||
this property allows specifying where to load it from.
|
||||
|
||||
By default this is set to point to
|
||||
"https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js". However it can
|
||||
also be set to point to a URL served by the application.
|
||||
|
||||
Note that it's possible to specify a relative URL in which case the URL
|
||||
must be relative to the iframe URL. For example assuming a SockJS endpoint
|
||||
mapped to "/sockjs", and resulting iframe URL "/sockjs/iframe.html", then the
|
||||
The relative URL must start with "../../" to traverse up to the location
|
||||
above the SockJS mapping. In case of a prefix-based Servlet mapping one more
|
||||
traversal may be needed.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="stream-bytes-limit" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
Minimum number of bytes that can be send over a single HTTP streaming request before it will be closed.
|
||||
Defaults to 128K (i.e. 128 1024).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="session-cookie-needed" type="xsd:boolean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
The "cookie_needed" value in the response from the SockJs "/info" endpoint.
|
||||
This property indicates whether the use of a JSESSIONID cookie is required for the application to function correctly,
|
||||
e.g. for load balancing or in Java Servlet containers for the use of an HTTP session.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="heartbeat-time" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
The amount of time in milliseconds when the server has not sent any messages and after which the server
|
||||
should send a heartbeat frame to the client in order to keep the connection from breaking.
|
||||
The default value is 25,000 (25 seconds).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="disconnect-delay" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
The amount of time in milliseconds before a client is considered disconnected after not having
|
||||
a receiving connection, i.e. an active connection over which the server can send data to the client.
|
||||
The default value is 5000.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="message-cache-size" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
The number of server-to-client messages that a session can cache while waiting for
|
||||
the next HTTP polling request from the client.
|
||||
The default size is 100.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="websocket-enabled" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
Some load balancers don't support websockets. Set this option to "false" to disable the WebSocket transport on the server side.
|
||||
The default value is "true".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="scheduler" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
The bean name of a TaskScheduler; a new ThreadPoolTaskScheduler instance will be created if no value is provided.
|
||||
This scheduler instance will be used for scheduling heart-beat messages.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="message-codec" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
The bean name of a SockJsMessageCodec to use for encoding and decoding SockJS messages.
|
||||
By default Jackson2SockJsMessageCodec is used requiring the Jackson library to be present on the classpath.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="suppress-cors" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
|
||||
This option can be used to disable automatic addition of CORS headers for SockJS requests.
|
||||
The default value is "false".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="stomp-broker-relay">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
Configures a MessageHandler that handles messages by forwarding them to a STOMP broker.
|
||||
This MessageHandler also opens a default "system" TCP connection to the message
|
||||
broker that is used for sending messages that originate from the server application (as
|
||||
opposed to from a client).
|
||||
The "login", "password", "heartbeat-send-interval" and "heartbeat-receive-interval" attributes
|
||||
are provided to configure this "system" connection.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:attribute name="prefix" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
Comma-separated list of destination prefixes supported by the broker being configured.
|
||||
Destinations that do not match the given prefix(es) are ignored.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="relay-host" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The STOMP message broker host.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="relay-port" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The STOMP message broker port.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="client-login" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The login to use when creating connections to the STOMP broker on behalf of connected clients.
|
||||
By default this is set to "guest".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="client-passcode" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The passcode to use when creating connections to the STOMP broker on behalf of connected clients.
|
||||
By default this is set to "guest".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="system-login" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The login for the shared "system" connection used to send messages to
|
||||
the STOMP broker from within the application, i.e. messages not associated
|
||||
with a specific client session (e.g. REST/HTTP request handling method).
|
||||
By default this is set to "guest".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="system-passcode" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The passcode for the shared "system" connection used to send messages to
|
||||
the STOMP broker from within the application, i.e. messages not associated
|
||||
with a specific client session (e.g. REST/HTTP request handling method).
|
||||
By default this is set to "guest".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="heartbeat-send-interval" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The interval, in milliseconds, at which the "system" connection will send heartbeats to the STOMP broker.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="heartbeat-receive-interval" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The interval, in milliseconds, at which the "system" connection expects to receive heartbeats from the STOMP broker.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="auto-startup" type="xsd:boolean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
Whether or not the StompBrokerRelay should be automatically started as part of its SmartLifecycle,
|
||||
i.e. at the time of an application context refresh.
|
||||
Default value is "true".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="virtual-host" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
|
||||
The value of the "host" header to use in STOMP CONNECT frames sent to the STOMP broker.
|
||||
This may be useful for example in a cloud environment where the actual host to which
|
||||
the TCP connection is established is different from the host providing the cloud-based STOMP service.
|
||||
By default this property is not set.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="simple-broker">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler"><![CDATA[
|
||||
Configures a SimpleBrokerMessageHandler that handles messages as a simple message broker implementation.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:attribute name="prefix" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
|
||||
Comma-separated list of destination prefixes supported by the broker being configured.
|
||||
Destinations that do not match the given prefix(es) are ignored.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="heartbeat" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
|
||||
Configure the value for the heartbeat settings. The first number represents how often the server will
|
||||
write or send a heartbeat. The second is how often the client should write. 0 means no heartbeats.
|
||||
By default this is set to "0, 0" unless the scheduler attribute is also set in which case the
|
||||
default becomes "10000,10000" (in milliseconds).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="scheduler" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
|
||||
The name of a task TaskScheduler to use for heartbeat support. Setting this property also
|
||||
automatically sets the heartbeat attribute to "10000, 10000".
|
||||
By default this attribute is not set.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="channel">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="executor" type="channel-executor" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="interceptors" type="channel-interceptors" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="channel-executor">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
|
||||
Configuration for the ThreadPoolTaskExecutor that sends messages for the message channel.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:attribute name="core-pool-size" type="xsd:string" use="optional">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
|
||||
Set the core pool size of the ThreadPoolExecutor.
|
||||
NOTE: the core pool size is effectively the max pool size when an unbounded queue-capacity is configured (the default).
|
||||
This is essentially the "Unbounded queues" strategy as explained in java.util.concurrent.ThreadPoolExecutor.
|
||||
When this strategy is used, the max pool size is effectively ignored.
|
||||
By default this is set to twice the value of Runtime.availableProcessors().
|
||||
In an an application where tasks do not block frequently,
|
||||
the number should be closer to or equal to the number of available CPUs/cores.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="max-pool-size" type="xsd:string" use="optional">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
|
||||
Set the max pool size of the ThreadPoolExecutor.
|
||||
NOTE: when an unbounded queue-capacity is configured (the default), the max pool size is effectively ignored.
|
||||
See the "Unbounded queues" strategy in java.util.concurrent.ThreadPoolExecutor for more details.
|
||||
By default this is set to Integer.MAX_VALUE.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="keep-alive-seconds" type="xsd:string" use="optional">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
|
||||
Set the time limit for which threads may remain idle before being terminated.
|
||||
If there are more than the core number of threads currently in the pool, after waiting this amount of time without
|
||||
processing a task, excess threads will be terminated. This overrides any value set in the constructor.
|
||||
By default this is set to 60.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="queue-capacity" type="xsd:string" use="optional">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
|
||||
Set the queue capacity for the ThreadPoolExecutor.
|
||||
NOTE: when an unbounded queue-capacity is configured (the default) the core pool size is effectively the max pool size.
|
||||
This is essentially the "Unbounded queues" strategy as explained in java.util.concurrent.ThreadPoolExecutor.
|
||||
When this strategy is used, the max pool size is effectively ignored.
|
||||
By default this is set to Integer.MAX_VALUE.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="channel-interceptors">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="java:org.springframework.messaging.support.ChannelInterceptor"><![CDATA[
|
||||
List of ChannelInterceptor beans to be used with this channel.
|
||||
Empty by default.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:sequence>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A ChannelInterceptor bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A reference to a ChannelInterceptor bean.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<!-- Elements definitions -->
|
||||
|
||||
<xsd:element name="handlers">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configures WebSocket support by registering a SimpleUrlHandlerMapping and mapping
|
||||
paths to registered WebSocketHandlers.
|
||||
|
||||
If a sockjs service is configured within this element, then a
|
||||
SockJsHttpRequestHandler will handle
|
||||
requests mapped to the given path.
|
||||
|
||||
Otherwise a WebSocketHttpRequestHandler
|
||||
will be registered for that purpose.
|
||||
|
||||
See EnableWebSocket Javadoc for
|
||||
information on code-based alternatives to enabling WebSocket support.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="mapping" type="mapping" minOccurs="1" maxOccurs="unbounded"/>
|
||||
<xsd:element name="handshake-handler" type="handshake-handler" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="handshake-interceptors" type="handshake-interceptors" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="sockjs" type="sockjs-service" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="order" type="xsd:token">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Order value for this SimpleUrlHandlerMapping.
|
||||
Default value is 1.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="allowed-origins" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure allowed {@code Origin} header values. Multiple origins may be specified
|
||||
as a comma-separated list.
|
||||
|
||||
This check is mostly designed for browser clients. There is noting preventing other
|
||||
types of client to modify the Origin header value.
|
||||
|
||||
When SockJS is enabled and allowed origins are restricted, transport types that do not
|
||||
use {@code Origin} headers for cross origin requests (jsonp-polling, iframe-xhr-polling,
|
||||
iframe-eventsource and iframe-htmlfile) are disabled. As a consequence, IE6/IE7 won't be
|
||||
supported anymore and IE8/IE9 will only be supported without cookies.
|
||||
|
||||
By default, all origins are allowed.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
<xsd:element name="message-broker">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configures broker-backed messaging over WebSocket using a higher-level messaging sub-protocol.
|
||||
Registers a SimpleUrlHandlerMapping and maps paths to registered Controllers.
|
||||
|
||||
A StompSubProtocolHandler is registered to handle various versions of the STOMP protocol.
|
||||
|
||||
See EnableWebSocketMessageBroker javadoc for information on code-based alternatives to enabling broker-backed messaging.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="transport" minOccurs="0" maxOccurs="1">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure options related to the processing of messages received from and sent to WebSocket clients.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="decorator-factories" maxOccurs="1" minOccurs="0">
|
||||
<xsd:complexType>
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory"><![CDATA[
|
||||
Configure one or more factories to decorate the handler used to process WebSocket
|
||||
messages. This may be useful for some advanced use cases, for example to allow
|
||||
Spring Security to forcibly close the WebSocket session when the corresponding
|
||||
HTTP session expires.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory"><![CDATA[
|
||||
A WebSocketHandlerDecoratorFactory bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation source="org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory"><![CDATA[
|
||||
A reference to a WebSocketHandlerDecoratorFactory bean.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="message-size" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure the maximum size for an incoming sub-protocol message.
|
||||
For example a STOMP message may be received as multiple WebSocket messages
|
||||
or multiple HTTP POST requests when SockJS fallback options are in use.
|
||||
|
||||
In theory a WebSocket message can be almost unlimited in size.
|
||||
In practice WebSocket servers impose limits on incoming message size.
|
||||
STOMP clients for example tend to split large messages around 16K
|
||||
boundaries. Therefore a server must be able to buffer partial content
|
||||
and decode when enough data is received. Use this property to configure
|
||||
the max size of the buffer to use.
|
||||
|
||||
The default value is 64K (i.e. 64 * 1024).
|
||||
|
||||
NOTE that the current version 1.2 of the STOMP spec
|
||||
does not specifically discuss how to send STOMP messages over WebSocket.
|
||||
Version 2 of the spec will but in the mean time existing client libraries
|
||||
have already established a practice that servers must handle.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="send-timeout" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure a time limit (in milliseconds) for the maximum amount of a time
|
||||
allowed when sending messages to a WebSocket session or writing to an
|
||||
HTTP response when SockJS fallback option are in use.
|
||||
|
||||
In general WebSocket servers expect that messages to a single WebSocket
|
||||
session are sent from a single thread at a time. This is automatically
|
||||
guaranteed when using {@code @EnableWebSocketMessageBroker} configuration.
|
||||
If message sending is slow, or at least slower than rate of messages sending,
|
||||
subsequent messages are buffered until either the {@code sendTimeLimit}
|
||||
or the {@code sendBufferSizeLimit} are reached at which point the session
|
||||
state is cleared and an attempt is made to close the session.
|
||||
|
||||
NOTE that the session time limit is checked only
|
||||
on attempts to send additional messages. So if only a single message is
|
||||
sent and it hangs, the session will not time out until another message is
|
||||
sent or the underlying physical socket times out. So this is not a
|
||||
replacement for WebSocket server or HTTP connection timeout but is rather
|
||||
intended to control the extent of buffering of unsent messages.
|
||||
|
||||
NOTE that closing the session may not succeed in
|
||||
actually closing the physical socket and may also hang. This is true
|
||||
especially when using blocking IO such as the BIO connector in Tomcat
|
||||
that is used by default on Tomcat 7. Therefore it is recommended to ensure
|
||||
the server is using non-blocking IO such as Tomcat's NIO connector that
|
||||
is used by default on Tomcat 8. If you must use blocking IO consider
|
||||
customizing OS-level TCP settings, for example
|
||||
{@code /proc/sys/net/ipv4/tcp_retries2} on Linux.
|
||||
|
||||
The default value is 10 seconds (i.e. 10 * 10000).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="send-buffer-size" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure the maximum amount of data to buffer when sending messages
|
||||
to a WebSocket session, or an HTTP response when SockJS fallback
|
||||
option are in use.
|
||||
|
||||
In general WebSocket servers expect that messages to a single WebSocket
|
||||
session are sent from a single thread at a time. This is automatically
|
||||
guaranteed when using {@code @EnableWebSocketMessageBroker} configuration.
|
||||
If message sending is slow, or at least slower than rate of messages sending,
|
||||
subsequent messages are buffered until either the {@code sendTimeLimit}
|
||||
or the {@code sendBufferSizeLimit} are reached at which point the session
|
||||
state is cleared and an attempt is made to close the session.
|
||||
|
||||
NOTE that closing the session may not succeed in
|
||||
actually closing the physical socket and may also hang. This is true
|
||||
especially when using blocking IO such as the BIO connector in Tomcat
|
||||
configured by default on Tomcat 7. Therefore it is recommended to ensure
|
||||
the server is using non-blocking IO such as Tomcat's NIO connector used
|
||||
by default on Tomcat 8. If you must use blocking IO consider customizing
|
||||
OS-level TCP settings, for example {@code /proc/sys/net/ipv4/tcp_retries2}
|
||||
on Linux.
|
||||
|
||||
The default value is 512K (i.e. 512 * 1024).
|
||||
|
||||
@param sendBufferSizeLimit the maximum number of bytes to buffer when
|
||||
sending messages; if the value is less than or equal to 0 then buffering
|
||||
is effectively disabled.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="stomp-endpoint" minOccurs="1" maxOccurs="unbounded">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Registers STOMP over WebSocket endpoints.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="handshake-handler" type="handshake-handler" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="handshake-interceptors" type="handshake-interceptors" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="sockjs" type="sockjs-service" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="path" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A path that maps a particular message destination to a handler method.
|
||||
Exact path mapping URIs (such as "/myPath") are supported as well as Ant-stype path patterns (such as /myPath/**).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="allowed-origins" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure allowed {@code Origin} header values. Multiple origins may be specified
|
||||
as a comma-separated list.
|
||||
|
||||
This check is mostly designed for browser clients. There is noting preventing other
|
||||
types of client to modify the Origin header value.
|
||||
|
||||
When SockJS is enabled and allowed origins are restricted, transport types that do not
|
||||
use {@code Origin} headers for cross origin requests (jsonp-polling, iframe-xhr-polling,
|
||||
iframe-eventsource and iframe-htmlfile) are disabled. As a consequence, IE6/IE7 won't be
|
||||
supported anymore and IE8/IE9 will only be supported without cookies.
|
||||
|
||||
By default, all origins are allowed.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:choice>
|
||||
<xsd:element name="simple-broker" type="simple-broker"/>
|
||||
<xsd:element name="stomp-broker-relay" type="stomp-broker-relay"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="argument-resolvers" minOccurs="0">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configures HandlerMethodArgumentResolver types to support custom controller method argument types.
|
||||
Using this option does not override the built-in support for resolving handler method arguments.
|
||||
To customize the built-in support for argument resolution configure WebSocketAnnotationMethodMessageHandler directly.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:choice minOccurs="1" maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean" minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The HandlerMethodArgumentResolver (or WebArgumentResolver for backwards compatibility) bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref" minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A reference to a HandlerMethodArgumentResolver bean definition.
|
||||
]]></xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="java:org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver" />
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="return-value-handlers" minOccurs="0">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configures HandlerMethodReturnValueHandler types to support custom controller method return value handling.
|
||||
Using this option does not override the built-in support for handling return values.
|
||||
To customize the built-in support for handling return values configure WebSocketAnnotationMethodMessageHandler directly.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:choice minOccurs="1" maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean" minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The HandlerMethodReturnValueHandler bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref" minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A reference to a HandlerMethodReturnValueHandler bean definition.
|
||||
]]></xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="java:org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler" />
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="message-converters" minOccurs="0">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Configure the message converters to use when extracting the payload of messages in annotated methods
|
||||
and when sending messages (e.g. through the "broker" SimpMessagingTemplate.
|
||||
MessageConverter registrations provided here will take precedence over MessageConverter types registered by default.
|
||||
Also see the register-defaults attribute if you want to turn off default registrations entirely.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element ref="beans:bean">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A MessageConverter bean definition.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element ref="beans:ref">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A reference to an HttpMessageConverter bean.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="register-defaults" type="xsd:boolean" default="true">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Whether or not default MessageConverter registrations should be added in addition to the ones provided within this element.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="client-inbound-channel" type="channel" minOccurs="0" maxOccurs="1">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The channel for receiving messages from clients (e.g. WebSocket clients).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element name="client-outbound-channel" type="channel" minOccurs="0" maxOccurs="1">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The channel for sending messages to clients (e.g. WebSocket clients).
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element name="broker-channel" type="channel" minOccurs="0" maxOccurs="1">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The channel for sending messages with translated user destinations.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="application-destination-prefix" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Comma-separated list of prefixes to match to the destinations of handled messages.
|
||||
Messages whose destination does not start with one of the configured prefixes are ignored.
|
||||
|
||||
Prefix is removed from the destination part and then messages are delegated to
|
||||
@SubscribeMapping and @MessageMapping}annotated methods.
|
||||
|
||||
Prefixes without a trailing slash will have one appended automatically.
|
||||
By default the list of prefixes is empty in which case all destinations match.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="user-destination-prefix" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The prefix used to identify user destinations.
|
||||
Any destinations that do not start with the given prefix are not be resolved.
|
||||
The default value is "/user/".
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="path-matcher" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
A reference to the PathMatcher to use to match the destinations of incoming
|
||||
messages to @MessageMapping and @SubscribeMapping methods.
|
||||
|
||||
By default AntPathMatcher is configured.
|
||||
However applications may provide an AntPathMatcher instance
|
||||
customized to use "." (commonly used in messaging) instead of "/" as path
|
||||
separator or provide a completely different PathMatcher implementation.
|
||||
|
||||
Note that the configured PathMatcher is only used for matching the
|
||||
portion of the destination after the configured prefix. For example given
|
||||
application destination prefix "/app" and destination "/app/price.stock.**",
|
||||
the message might be mapped to a controller with "price" and "stock.**"
|
||||
as its type and method-level mappings respectively.
|
||||
|
||||
When the simple broker is enabled, the PathMatcher configured here is
|
||||
also used to match message destinations when brokering messages.
|
||||
]]></xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
<tool:expected-type type="java:org.springframework.util.PathMatcher" />
|
||||
</tool:annotation>
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="order" type="xsd:token">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Order value for this SimpleUrlHandlerMapping.
|
||||
Default value is 1.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* 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.
|
||||
|
|
@ -18,6 +18,7 @@ package org.springframework.web.socket.config;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
|
|
@ -180,8 +181,10 @@ public class MessageBrokerBeanDefinitionParserTests {
|
|||
|
||||
SimpleBrokerMessageHandler brokerMessageHandler = this.appContext.getBean(SimpleBrokerMessageHandler.class);
|
||||
assertNotNull(brokerMessageHandler);
|
||||
assertEquals(Arrays.asList("/topic", "/queue"),
|
||||
new ArrayList<String>(brokerMessageHandler.getDestinationPrefixes()));
|
||||
Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
|
||||
assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<String>(prefixes));
|
||||
assertNotNull(brokerMessageHandler.getTaskScheduler());
|
||||
assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
|
||||
|
||||
List<Class<? extends MessageHandler>> subscriberTypes =
|
||||
Arrays.<Class<? extends MessageHandler>>asList(SimpAnnotationMethodMessageHandler.class,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.web.socket.config.annotation;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -37,6 +38,7 @@ import org.springframework.messaging.handler.annotation.SendTo;
|
|||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.annotation.SubscribeMapping;
|
||||
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.simp.user.UserDestinationMessageHandler;
|
||||
|
|
@ -44,6 +46,7 @@ import org.springframework.messaging.support.AbstractSubscribableChannel;
|
|||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
|
@ -149,14 +152,18 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void messageBrokerSockJsTaskScheduler() {
|
||||
public void taskScheduler() {
|
||||
ApplicationContext config = createConfig(TestChannelConfig.class, TestConfigurer.class);
|
||||
ThreadPoolTaskScheduler taskScheduler =
|
||||
config.getBean("messageBrokerSockJsTaskScheduler", ThreadPoolTaskScheduler.class);
|
||||
|
||||
String name = "messageBrokerSockJsTaskScheduler";
|
||||
ThreadPoolTaskScheduler taskScheduler = config.getBean(name, ThreadPoolTaskScheduler.class);
|
||||
ScheduledThreadPoolExecutor executor = taskScheduler.getScheduledThreadPoolExecutor();
|
||||
assertEquals(Runtime.getRuntime().availableProcessors(), executor.getCorePoolSize());
|
||||
assertTrue(executor.getRemoveOnCancelPolicy());
|
||||
|
||||
SimpleBrokerMessageHandler handler = config.getBean(SimpleBrokerMessageHandler.class);
|
||||
assertNotNull(handler.getTaskScheduler());
|
||||
assertArrayEquals(new long[] {15000, 15000}, handler.getHeartbeatValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -200,6 +207,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
|||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Controller
|
||||
static class TestController {
|
||||
|
||||
|
|
@ -215,6 +223,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Configuration
|
||||
static class TestConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
|
|
@ -234,6 +243,13 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
|||
registration.setSendTimeLimit(25 * 1000);
|
||||
registration.setSendBufferSizeLimit(1024 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker()
|
||||
.setTaskScheduler(mock(TaskScheduler.class))
|
||||
.setHeartbeatValue(new long[] {15000, 15000});
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
|
|
|
|||
|
|
@ -16,21 +16,12 @@
|
|||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -42,6 +33,7 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
|
|
@ -53,7 +45,6 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
|||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.TestPrincipal;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
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.user.DefaultUserSessionRegistry;
|
||||
|
|
@ -103,7 +94,7 @@ public class StompSubProtocolHandlerTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientConnected() {
|
||||
public void handleMessageToClientWithConnectedFrame() {
|
||||
|
||||
UserSessionRegistry registry = new DefaultUserSessionRegistry();
|
||||
this.protocolHandler.setUserSessionRegistry(registry);
|
||||
|
|
@ -120,7 +111,7 @@ public class StompSubProtocolHandlerTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientConnectedUniqueUserName() {
|
||||
public void handleMessageToClientWithDestinationUserNameProvider() {
|
||||
|
||||
this.session.setPrincipal(new UniqueUser("joe"));
|
||||
|
||||
|
|
@ -140,47 +131,197 @@ public class StompSubProtocolHandlerTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientConnectedWithHeartbeats() {
|
||||
public void handleMessageToClientWithSimpConnectAck() {
|
||||
|
||||
SockJsSession sockJsSession = Mockito.mock(SockJsSession.class);
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECT);
|
||||
accessor.setHeartbeat(10000, 10000);
|
||||
accessor.setAcceptVersion("1.0,1.1");
|
||||
Message<?> connectMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
headers.setHeartbeat(0,10);
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(sockJsSession, message);
|
||||
SimpMessageHeaderAccessor ackAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
|
||||
ackAccessor.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, connectMessage);
|
||||
ackAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, new long[] {15000, 15000});
|
||||
Message<byte[]> ackMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, ackAccessor.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, ackMessage);
|
||||
|
||||
verify(sockJsSession).disableHeartbeat();
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
TextMessage actual = (TextMessage) this.session.getSentMessages().get(0);
|
||||
assertEquals("CONNECTED\n" + "version:1.1\n" + "heart-beat:15000,15000\n" +
|
||||
"user-name:joe\n" + "\n" + "\u0000", actual.getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientConnectAck() {
|
||||
public void handleMessageToClientWithSimpHeartbeat() {
|
||||
|
||||
StompHeaderAccessor connectHeaders = StompHeaderAccessor.create(StompCommand.CONNECT);
|
||||
connectHeaders.setHeartbeat(10000, 10000);
|
||||
connectHeaders.setAcceptVersion("1.0,1.1");
|
||||
Message<?> connectMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, connectHeaders.getMessageHeaders());
|
||||
|
||||
SimpMessageHeaderAccessor connectAckHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
|
||||
connectAckHeaders.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, connectMessage);
|
||||
Message<byte[]> connectAckMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, connectAckHeaders.getMessageHeaders());
|
||||
|
||||
this.protocolHandler.handleMessageToClient(this.session, connectAckMessage);
|
||||
|
||||
verifyNoMoreInteractions(this.channel);
|
||||
|
||||
// Check CONNECTED reply
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
|
||||
accessor.setSessionId("s1");
|
||||
accessor.setUser(new TestPrincipal("joe"));
|
||||
Message<byte[]> ackMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, ackMessage);
|
||||
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
TextMessage textMessage = (TextMessage) this.session.getSentMessages().get(0);
|
||||
TextMessage actual = (TextMessage) this.session.getSentMessages().get(0);
|
||||
assertEquals("\n", actual.getPayload());
|
||||
}
|
||||
|
||||
List<Message<byte[]>> messages = new StompDecoder().decode(ByteBuffer.wrap(textMessage.getPayload().getBytes()));
|
||||
assertEquals(1, messages.size());
|
||||
StompHeaderAccessor replyHeaders = StompHeaderAccessor.wrap(messages.get(0));
|
||||
@Test
|
||||
public void handleMessageToClientWithHeartbeatSuppressingSockJsHeartbeat() throws IOException {
|
||||
|
||||
assertEquals(StompCommand.CONNECTED, replyHeaders.getCommand());
|
||||
assertEquals("1.1", replyHeaders.getVersion());
|
||||
assertArrayEquals(new long[] {0, 0}, replyHeaders.getHeartbeat());
|
||||
assertEquals("joe", replyHeaders.getNativeHeader("user-name").get(0));
|
||||
SockJsSession sockJsSession = Mockito.mock(SockJsSession.class);
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
accessor.setHeartbeat(0, 10);
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(sockJsSession, message);
|
||||
|
||||
verify(sockJsSession).getPrincipal();
|
||||
verify(sockJsSession).disableHeartbeat();
|
||||
verify(sockJsSession).sendMessage(any(WebSocketMessage.class));
|
||||
verifyNoMoreInteractions(sockJsSession);
|
||||
|
||||
sockJsSession = Mockito.mock(SockJsSession.class);
|
||||
accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
accessor.setHeartbeat(0, 0);
|
||||
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(sockJsSession, message);
|
||||
|
||||
verify(sockJsSession).getPrincipal();
|
||||
verify(sockJsSession).sendMessage(any(WebSocketMessage.class));
|
||||
verifyNoMoreInteractions(sockJsSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientWithUserDestination() {
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
|
||||
headers.setMessageId("mess0");
|
||||
headers.setSubscriptionId("sub0");
|
||||
headers.setDestination("/queue/foo-user123");
|
||||
headers.setNativeHeader(StompHeaderAccessor.ORIGINAL_DESTINATION, "/user/queue/foo");
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
WebSocketMessage<?> textMessage = this.session.getSentMessages().get(0);
|
||||
assertTrue(((String) textMessage.getPayload()).contains("destination:/user/queue/foo\n"));
|
||||
assertFalse(((String) textMessage.getPayload()).contains(SimpMessageHeaderAccessor.ORIGINAL_DESTINATION));
|
||||
}
|
||||
|
||||
// SPR-12475
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientWithBinaryWebSocketMessage() {
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
|
||||
headers.setMessageId("mess0");
|
||||
headers.setSubscriptionId("sub0");
|
||||
headers.setContentType(MimeTypeUtils.APPLICATION_OCTET_STREAM);
|
||||
headers.setDestination("/queue/foo");
|
||||
|
||||
// Non-empty payload
|
||||
|
||||
byte[] payload = new byte[1];
|
||||
Message<byte[]> message = MessageBuilder.createMessage(payload, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
WebSocketMessage<?> webSocketMessage = this.session.getSentMessages().get(0);
|
||||
assertTrue(webSocketMessage instanceof BinaryMessage);
|
||||
|
||||
// Empty payload
|
||||
|
||||
payload = EMPTY_PAYLOAD;
|
||||
message = MessageBuilder.createMessage(payload, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
|
||||
assertEquals(2, this.session.getSentMessages().size());
|
||||
webSocketMessage = this.session.getSentMessages().get(1);
|
||||
assertTrue(webSocketMessage instanceof TextMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClient() {
|
||||
|
||||
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.CONNECT).headers(
|
||||
"login:guest", "passcode:guest", "accept-version:1.1,1.0", "heart-beat:10000,10000").build();
|
||||
|
||||
this.protocolHandler.afterSessionStarted(this.session, this.channel);
|
||||
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
|
||||
|
||||
verify(this.channel).send(this.messageCaptor.capture());
|
||||
Message<?> actual = this.messageCaptor.getValue();
|
||||
assertNotNull(actual);
|
||||
|
||||
assertEquals("s1", SimpMessageHeaderAccessor.getSessionId(actual.getHeaders()));
|
||||
assertNotNull(SimpMessageHeaderAccessor.getSessionAttributes(actual.getHeaders()));
|
||||
assertNotNull(SimpMessageHeaderAccessor.getUser(actual.getHeaders()));
|
||||
assertEquals("joe", SimpMessageHeaderAccessor.getUser(actual.getHeaders()).getName());
|
||||
assertNotNull(SimpMessageHeaderAccessor.getHeartbeat(actual.getHeaders()));
|
||||
assertArrayEquals(new long[] {10000, 10000}, SimpMessageHeaderAccessor.getHeartbeat(actual.getHeaders()));
|
||||
|
||||
StompHeaderAccessor stompAccessor = StompHeaderAccessor.wrap(actual);
|
||||
assertEquals(StompCommand.CONNECT, stompAccessor.getCommand());
|
||||
assertEquals("guest", stompAccessor.getLogin());
|
||||
assertEquals("guest", stompAccessor.getPasscode());
|
||||
assertArrayEquals(new long[] {10000, 10000}, stompAccessor.getHeartbeat());
|
||||
assertEquals(new HashSet<>(Arrays.asList("1.1","1.0")), stompAccessor.getAcceptVersion());
|
||||
assertEquals(0, this.session.getSentMessages().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClientWithImmutableMessageInterceptor() {
|
||||
AtomicReference<Boolean> mutable = new AtomicReference<>();
|
||||
ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel();
|
||||
channel.addInterceptor(new ChannelInterceptorAdapter() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
mutable.set(MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class).isMutable());
|
||||
return message;
|
||||
}
|
||||
});
|
||||
channel.addInterceptor(new ImmutableMessageChannelInterceptor());
|
||||
|
||||
StompSubProtocolHandler handler = new StompSubProtocolHandler();
|
||||
handler.afterSessionStarted(this.session, channel);
|
||||
|
||||
TextMessage message = StompTextMessageBuilder.create(StompCommand.CONNECT).build();
|
||||
handler.handleMessageFromClient(this.session, message, channel);
|
||||
assertNotNull(mutable.get());
|
||||
assertTrue(mutable.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClientWithoutImmutableMessageInterceptor() {
|
||||
AtomicReference<Boolean> mutable = new AtomicReference<>();
|
||||
ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel();
|
||||
channel.addInterceptor(new ChannelInterceptorAdapter() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
mutable.set(MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class).isMutable());
|
||||
return message;
|
||||
}
|
||||
});
|
||||
|
||||
StompSubProtocolHandler handler = new StompSubProtocolHandler();
|
||||
handler.afterSessionStarted(this.session, channel);
|
||||
|
||||
TextMessage message = StompTextMessageBuilder.create(StompCommand.CONNECT).build();
|
||||
handler.handleMessageFromClient(this.session, message, channel);
|
||||
assertNotNull(mutable.get());
|
||||
assertFalse(mutable.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClientWithInvalidStompCommand() {
|
||||
|
||||
TextMessage textMessage = new TextMessage("FOO\n\n\0");
|
||||
|
||||
this.protocolHandler.afterSessionStarted(this.session, this.channel);
|
||||
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
|
||||
|
||||
verifyZeroInteractions(this.channel);
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
TextMessage actual = (TextMessage) this.session.getSentMessages().get(0);
|
||||
assertTrue(actual.getPayload().startsWith("ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -262,137 +403,6 @@ public class StompSubProtocolHandlerTests {
|
|||
assertEquals("joe", accessor.getUser().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientUserDestination() {
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
|
||||
headers.setMessageId("mess0");
|
||||
headers.setSubscriptionId("sub0");
|
||||
headers.setDestination("/queue/foo-user123");
|
||||
headers.setNativeHeader(StompHeaderAccessor.ORIGINAL_DESTINATION, "/user/queue/foo");
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
WebSocketMessage<?> textMessage = this.session.getSentMessages().get(0);
|
||||
assertTrue(((String) textMessage.getPayload()).contains("destination:/user/queue/foo\n"));
|
||||
assertFalse(((String) textMessage.getPayload()).contains(SimpMessageHeaderAccessor.ORIGINAL_DESTINATION));
|
||||
}
|
||||
|
||||
// SPR-12475
|
||||
|
||||
@Test
|
||||
public void handleMessageToClientBinaryWebSocketMessage() {
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
|
||||
headers.setMessageId("mess0");
|
||||
headers.setSubscriptionId("sub0");
|
||||
headers.setContentType(MimeTypeUtils.APPLICATION_OCTET_STREAM);
|
||||
headers.setDestination("/queue/foo");
|
||||
|
||||
// Non-empty payload
|
||||
|
||||
byte[] payload = new byte[1];
|
||||
Message<byte[]> message = MessageBuilder.createMessage(payload, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
WebSocketMessage<?> webSocketMessage = this.session.getSentMessages().get(0);
|
||||
assertTrue(webSocketMessage instanceof BinaryMessage);
|
||||
|
||||
// Empty payload
|
||||
|
||||
payload = EMPTY_PAYLOAD;
|
||||
message = MessageBuilder.createMessage(payload, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
|
||||
assertEquals(2, this.session.getSentMessages().size());
|
||||
webSocketMessage = this.session.getSentMessages().get(1);
|
||||
assertTrue(webSocketMessage instanceof TextMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClient() {
|
||||
|
||||
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.CONNECT).headers(
|
||||
"login:guest", "passcode:guest", "accept-version:1.1,1.0", "heart-beat:10000,10000").build();
|
||||
|
||||
this.protocolHandler.afterSessionStarted(this.session, this.channel);
|
||||
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
|
||||
|
||||
verify(this.channel).send(this.messageCaptor.capture());
|
||||
Message<?> actual = this.messageCaptor.getValue();
|
||||
assertNotNull(actual);
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(actual);
|
||||
assertEquals(StompCommand.CONNECT, headers.getCommand());
|
||||
assertEquals("s1", headers.getSessionId());
|
||||
assertNotNull(headers.getSessionAttributes());
|
||||
assertEquals("joe", headers.getUser().getName());
|
||||
assertEquals("guest", headers.getLogin());
|
||||
assertEquals("guest", headers.getPasscode());
|
||||
assertArrayEquals(new long[] {10000, 10000}, headers.getHeartbeat());
|
||||
assertEquals(new HashSet<>(Arrays.asList("1.1","1.0")), headers.getAcceptVersion());
|
||||
|
||||
assertEquals(0, this.session.getSentMessages().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClientWithImmutableMessageInterceptor() {
|
||||
AtomicReference<Boolean> mutable = new AtomicReference<>();
|
||||
ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel();
|
||||
channel.addInterceptor(new ChannelInterceptorAdapter() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
mutable.set(MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class).isMutable());
|
||||
return message;
|
||||
}
|
||||
});
|
||||
channel.addInterceptor(new ImmutableMessageChannelInterceptor());
|
||||
|
||||
StompSubProtocolHandler handler = new StompSubProtocolHandler();
|
||||
handler.afterSessionStarted(this.session, channel);
|
||||
|
||||
TextMessage message = StompTextMessageBuilder.create(StompCommand.CONNECT).build();
|
||||
handler.handleMessageFromClient(this.session, message, channel);
|
||||
assertNotNull(mutable.get());
|
||||
assertTrue(mutable.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleMessageFromClientWithoutImmutableMessageInterceptor() {
|
||||
AtomicReference<Boolean> mutable = new AtomicReference<>();
|
||||
ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel();
|
||||
channel.addInterceptor(new ChannelInterceptorAdapter() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
mutable.set(MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class).isMutable());
|
||||
return message;
|
||||
}
|
||||
});
|
||||
|
||||
StompSubProtocolHandler handler = new StompSubProtocolHandler();
|
||||
handler.afterSessionStarted(this.session, channel);
|
||||
|
||||
TextMessage message = StompTextMessageBuilder.create(StompCommand.CONNECT).build();
|
||||
handler.handleMessageFromClient(this.session, message, channel);
|
||||
assertNotNull(mutable.get());
|
||||
assertFalse(mutable.get());
|
||||
}
|
||||
@Test
|
||||
public void handleMessageFromClientInvalidStompCommand() {
|
||||
|
||||
TextMessage textMessage = new TextMessage("FOO\n\n\0");
|
||||
|
||||
this.protocolHandler.afterSessionStarted(this.session, this.channel);
|
||||
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
|
||||
|
||||
verifyZeroInteractions(this.channel);
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
TextMessage actual = (TextMessage) this.session.getSentMessages().get(0);
|
||||
assertTrue(actual.getPayload().startsWith("ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webSocketScope() {
|
||||
|
||||
|
|
@ -421,10 +431,10 @@ public class StompSubProtocolHandlerTests {
|
|||
TextMessage textMessage = new TextMessage(new StompEncoder().encode(message));
|
||||
|
||||
this.protocolHandler.handleMessageFromClient(this.session, textMessage, testChannel);
|
||||
assertEquals(Collections.emptyList(), session.getSentMessages());
|
||||
assertEquals(Collections.<WebSocketMessage<?>>emptyList(), session.getSentMessages());
|
||||
|
||||
this.protocolHandler.afterSessionEnded(this.session, CloseStatus.BAD_DATA, testChannel);
|
||||
assertEquals(Collections.emptyList(), session.getSentMessages());
|
||||
assertEquals(Collections.<WebSocketMessage<?>>emptyList(), this.session.getSentMessages());
|
||||
verify(runnable, times(1)).run();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -175,6 +175,14 @@ public class WebSocketStompClientIntegrationTests {
|
|||
received.add((String) payload);
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Delay send since server processes concurrently
|
||||
// Ideally order should be preserved or receipts supported (simple broker)
|
||||
Thread.sleep(500);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
logger.error(ex);
|
||||
}
|
||||
session.send(this.topic, this.payload);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
<websocket:sockjs/>
|
||||
</websocket:stomp-endpoint>
|
||||
|
||||
<websocket:simple-broker prefix="/topic, /queue"/>
|
||||
<websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
|
||||
|
||||
</websocket:message-broker>
|
||||
|
||||
|
|
@ -42,5 +42,6 @@
|
|||
|
||||
<bean id="myHandler" class="org.springframework.web.socket.config.TestHandshakeHandler"/>
|
||||
<bean id="barTestInterceptor" class="org.springframework.web.socket.config.BarTestInterceptor"/>
|
||||
<bean id="scheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"/>
|
||||
|
||||
</beans>
|
||||
|
|
|
|||
Loading…
Reference in New Issue