Add SimpUserRegistry with multi-server support
This change introduces SimpUserRegistry exposing an API to access information about connected users, their sessions, and subscriptions with STOMP/WebSocket messaging. Provides are methods to access users as well as a method to find subscriptions given a Matcher strategy. The DefaultSimpUserRegistry implementation is also a SmartApplicationListener which listesn for ApplicationContext events when users connect, disconnect, subscribe, and unsubscribe to destinations. The MultiServerUserRegistry implementation is a composite that aggregates user information from the local SimpUserRegistry as well as snapshots of user on remote application servers. UserRegistryMessageHandler is used with MultiServerUserRegistry. It broadcats user registry information through the broker and listens for similar broadcasts from other servers. This must be enabled explicitly when configuring the STOMP broker relay. The existing UserSessionRegistry which was primiarly used internally to resolve a user name to session id's has been deprecated and is no longer used. If an application configures a custom UserSessionRegistr still, it will be adapted accordingly to SimpUserRegistry but the effect is rather limited (comparable to pre-existing functionality) and will not work in multi-server scenarios. Issue: SPR-12029
This commit is contained in:
@@ -48,8 +48,9 @@ import org.springframework.messaging.simp.SimpSessionScope;
|
||||
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
|
||||
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
|
||||
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
|
||||
import org.springframework.messaging.simp.user.DefaultUserSessionRegistry;
|
||||
import org.springframework.messaging.simp.user.MultiServerUserRegistry;
|
||||
import org.springframework.messaging.simp.user.UserDestinationMessageHandler;
|
||||
import org.springframework.messaging.simp.user.UserRegistryMessageHandler;
|
||||
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
@@ -61,6 +62,7 @@ import org.springframework.util.xml.DomUtils;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
|
||||
import org.springframework.web.socket.messaging.DefaultSimpUserRegistry;
|
||||
import org.springframework.web.socket.messaging.StompSubProtocolHandler;
|
||||
import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler;
|
||||
import org.springframework.web.socket.messaging.WebSocketAnnotationMethodMessageHandler;
|
||||
@@ -98,6 +100,8 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
public static final String WEB_SOCKET_HANDLER_BEAN_NAME = "subProtocolWebSocketHandler";
|
||||
|
||||
public static final String SCHEDULER_BEAN_NAME = "messageBrokerScheduler";
|
||||
|
||||
public static final String SOCKJS_SCHEDULER_BEAN_NAME = "messageBrokerSockJsScheduler";
|
||||
|
||||
private static final int DEFAULT_MAPPING_ORDER = 1;
|
||||
@@ -108,10 +112,82 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
@Override
|
||||
public BeanDefinition parse(Element element, ParserContext context) {
|
||||
|
||||
Object source = context.extractSource(element);
|
||||
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
|
||||
context.pushContainingComponent(compDefinition);
|
||||
|
||||
Element channelElem = DomUtils.getChildElementByTagName(element, "client-inbound-channel");
|
||||
RuntimeBeanReference inChannel = getMessageChannel("clientInboundChannel", channelElem, context, source);
|
||||
|
||||
channelElem = DomUtils.getChildElementByTagName(element, "client-outbound-channel");
|
||||
RuntimeBeanReference outChannel = getMessageChannel("clientOutboundChannel", channelElem, context, source);
|
||||
|
||||
channelElem = DomUtils.getChildElementByTagName(element, "broker-channel");
|
||||
RuntimeBeanReference brokerChannel = getMessageChannel("brokerChannel", channelElem, context, source);
|
||||
|
||||
RuntimeBeanReference userRegistry = registerUserRegistry(element, context, source);
|
||||
Object userDestHandler = registerUserDestHandler(element, userRegistry, inChannel, brokerChannel, context, source);
|
||||
|
||||
RuntimeBeanReference converter = registerMessageConverter(element, context, source);
|
||||
RuntimeBeanReference template = registerMessagingTemplate(element, brokerChannel, converter, context, source);
|
||||
registerAnnotationMethodMessageHandler(element, inChannel, outChannel,converter, template, context, source);
|
||||
|
||||
RootBeanDefinition broker = registerMessageBroker(element, inChannel, outChannel, brokerChannel,
|
||||
userDestHandler, template, userRegistry, context, source);
|
||||
|
||||
// WebSocket and sub-protocol handling
|
||||
|
||||
ManagedMap<String, Object> urlMap = registerHandlerMapping(element, context, source);
|
||||
RuntimeBeanReference stompHandler = registerStompHandler(element, inChannel, outChannel, context, source);
|
||||
for (Element endpointElem : DomUtils.getChildElementsByTagName(element, "stomp-endpoint")) {
|
||||
RuntimeBeanReference requestHandler = registerRequestHandler(endpointElem, stompHandler, context, source);
|
||||
String pathAttribute = endpointElem.getAttribute("path");
|
||||
Assert.state(StringUtils.hasText(pathAttribute), "Invalid <stomp-endpoint> (no path mapping)");
|
||||
List<String> paths = Arrays.asList(StringUtils.tokenizeToStringArray(pathAttribute, ","));
|
||||
for (String path : paths) {
|
||||
path = path.trim();
|
||||
Assert.state(StringUtils.hasText(path), "Invalid <stomp-endpoint> path attribute: " + pathAttribute);
|
||||
if (DomUtils.getChildElementByTagName(endpointElem, "sockjs") != null) {
|
||||
path = path.endsWith("/") ? path + "**" : path + "/**";
|
||||
}
|
||||
urlMap.put(path, requestHandler);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> scopeMap = Collections.<String, Object>singletonMap("websocket", new SimpSessionScope());
|
||||
RootBeanDefinition scopeConfigurer = new RootBeanDefinition(CustomScopeConfigurer.class);
|
||||
scopeConfigurer.getPropertyValues().add("scopes", scopeMap);
|
||||
registerBeanDefByName("webSocketScopeConfigurer", scopeConfigurer, context, source);
|
||||
|
||||
registerWebSocketMessageBrokerStats(broker, inChannel, outChannel, context, source);
|
||||
|
||||
context.popAndRegisterContainingComponent();
|
||||
return null;
|
||||
}
|
||||
|
||||
private RuntimeBeanReference registerUserRegistry(Element element, ParserContext context, Object source) {
|
||||
|
||||
Element relayElement = DomUtils.getChildElementByTagName(element, "stomp-broker-relay");
|
||||
boolean multiServer = (relayElement != null && relayElement.hasAttribute("user-registry-broadcast"));
|
||||
|
||||
if (multiServer) {
|
||||
RootBeanDefinition localRegistryBeanDef = new RootBeanDefinition(DefaultSimpUserRegistry.class);
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(MultiServerUserRegistry.class);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, localRegistryBeanDef);
|
||||
String beanName = registerBeanDef(beanDef, context, source);
|
||||
return new RuntimeBeanReference(beanName);
|
||||
}
|
||||
else {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultSimpUserRegistry.class);
|
||||
String beanName = registerBeanDef(beanDef, context, source);
|
||||
return new RuntimeBeanReference(beanName);
|
||||
}
|
||||
}
|
||||
|
||||
private ManagedMap<String, Object> registerHandlerMapping(Element element,
|
||||
ParserContext context, Object source) {
|
||||
|
||||
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
|
||||
|
||||
String orderAttribute = element.getAttribute("order");
|
||||
@@ -128,58 +204,7 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
handlerMappingDef.getPropertyValues().add("urlMap", urlMap);
|
||||
|
||||
registerBeanDef(handlerMappingDef, context, source);
|
||||
|
||||
Element channelElem = DomUtils.getChildElementByTagName(element, "client-inbound-channel");
|
||||
RuntimeBeanReference inChannel = getMessageChannel("clientInboundChannel", channelElem, context, source);
|
||||
|
||||
channelElem = DomUtils.getChildElementByTagName(element, "client-outbound-channel");
|
||||
RuntimeBeanReference outChannel = getMessageChannel("clientOutboundChannel", channelElem, context, source);
|
||||
|
||||
RootBeanDefinition registryBeanDef = new RootBeanDefinition(DefaultUserSessionRegistry.class);
|
||||
String registryBeanName = registerBeanDef(registryBeanDef, context, source);
|
||||
RuntimeBeanReference sessionRegistry = new RuntimeBeanReference(registryBeanName);
|
||||
|
||||
RuntimeBeanReference subProtoHandler = registerSubProtoHandler(element, inChannel, outChannel,
|
||||
sessionRegistry, context, source);
|
||||
|
||||
for (Element endpointElem : DomUtils.getChildElementsByTagName(element, "stomp-endpoint")) {
|
||||
RuntimeBeanReference requestHandler = registerRequestHandler(endpointElem, subProtoHandler, context, source);
|
||||
String pathAttribute = endpointElem.getAttribute("path");
|
||||
Assert.state(StringUtils.hasText(pathAttribute), "Invalid <stomp-endpoint> (no path mapping)");
|
||||
List<String> paths = Arrays.asList(StringUtils.tokenizeToStringArray(pathAttribute, ","));
|
||||
for (String path : paths) {
|
||||
path = path.trim();
|
||||
Assert.state(StringUtils.hasText(path), "Invalid <stomp-endpoint> path attribute: " + pathAttribute);
|
||||
if (DomUtils.getChildElementByTagName(endpointElem, "sockjs") != null) {
|
||||
path = path.endsWith("/") ? path + "**" : path + "/**";
|
||||
}
|
||||
urlMap.put(path, requestHandler);
|
||||
}
|
||||
}
|
||||
|
||||
channelElem = DomUtils.getChildElementByTagName(element, "broker-channel");
|
||||
RuntimeBeanReference brokerChannel = getMessageChannel("brokerChannel", channelElem, context, source);
|
||||
|
||||
RuntimeBeanReference resolver = registerUserDestResolver(element, sessionRegistry, context, source);
|
||||
RuntimeBeanReference userDestHandler = registerUserDestHandler(element, inChannel,
|
||||
brokerChannel, resolver, context, source);
|
||||
|
||||
RootBeanDefinition broker = registerMessageBroker(element, userDestHandler, inChannel,
|
||||
outChannel, brokerChannel, context, source);
|
||||
|
||||
RuntimeBeanReference converter = registerMessageConverter(element, context, source);
|
||||
RuntimeBeanReference template = registerMessagingTemplate(element, brokerChannel, converter, context, source);
|
||||
registerAnnotationMethodMessageHandler(element, inChannel, outChannel,converter, template, context, source);
|
||||
|
||||
Map<String, Object> scopeMap = Collections.<String, Object>singletonMap("websocket", new SimpSessionScope());
|
||||
RootBeanDefinition scopeConfigurer = new RootBeanDefinition(CustomScopeConfigurer.class);
|
||||
scopeConfigurer.getPropertyValues().add("scopes", scopeMap);
|
||||
registerBeanDefByName("webSocketScopeConfigurer", scopeConfigurer, context, source);
|
||||
|
||||
registerWebSocketMessageBrokerStats(broker, inChannel, outChannel, context, source);
|
||||
|
||||
context.popAndRegisterContainingComponent();
|
||||
return null;
|
||||
return urlMap;
|
||||
}
|
||||
|
||||
private RuntimeBeanReference getMessageChannel(String name, Element element, ParserContext context, Object source) {
|
||||
@@ -240,11 +265,10 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
return executorDef;
|
||||
}
|
||||
|
||||
private RuntimeBeanReference registerSubProtoHandler(Element element, RuntimeBeanReference inChannel,
|
||||
RuntimeBeanReference outChannel, RuntimeBeanReference registry, ParserContext context, Object source) {
|
||||
private RuntimeBeanReference registerStompHandler(Element element, RuntimeBeanReference inChannel,
|
||||
RuntimeBeanReference outChannel, ParserContext context, Object source) {
|
||||
|
||||
RootBeanDefinition stompHandlerDef = new RootBeanDefinition(StompSubProtocolHandler.class);
|
||||
stompHandlerDef.getPropertyValues().add("userSessionRegistry", registry);
|
||||
registerBeanDef(stompHandlerDef, context, source);
|
||||
|
||||
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
|
||||
@@ -285,13 +309,16 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
RootBeanDefinition beanDef;
|
||||
|
||||
RuntimeBeanReference sockJsService = WebSocketNamespaceUtils.registerSockJsService(
|
||||
element, SOCKJS_SCHEDULER_BEAN_NAME, context, source);
|
||||
element, SCHEDULER_BEAN_NAME, context, source);
|
||||
|
||||
if (sockJsService != null) {
|
||||
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
|
||||
cavs.addIndexedArgumentValue(0, sockJsService);
|
||||
cavs.addIndexedArgumentValue(1, subProtoHandler);
|
||||
beanDef = new RootBeanDefinition(SockJsHttpRequestHandler.class, cavs, null);
|
||||
|
||||
// Register alias for backwards compatibility with 4.1
|
||||
context.getRegistry().registerAlias(SCHEDULER_BEAN_NAME, SOCKJS_SCHEDULER_BEAN_NAME);
|
||||
}
|
||||
else {
|
||||
RuntimeBeanReference handshakeHandler = WebSocketNamespaceUtils.registerHandshakeHandler(element, context, source);
|
||||
@@ -312,9 +339,9 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
|
||||
private RootBeanDefinition registerMessageBroker(Element brokerElement,
|
||||
RuntimeBeanReference userDestHandler, RuntimeBeanReference inChannel,
|
||||
RuntimeBeanReference outChannel, RuntimeBeanReference brokerChannel,
|
||||
ParserContext context, Object source) {
|
||||
RuntimeBeanReference inChannel, RuntimeBeanReference outChannel, RuntimeBeanReference brokerChannel,
|
||||
Object userDestHandler, RuntimeBeanReference brokerTemplate,
|
||||
RuntimeBeanReference userRegistry, ParserContext context, Object source) {
|
||||
|
||||
Element simpleBrokerElem = DomUtils.getChildElementByTagName(brokerElement, "simple-broker");
|
||||
Element brokerRelayElem = DomUtils.getChildElementByTagName(brokerElement, "stomp-broker-relay");
|
||||
@@ -374,11 +401,18 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
if (brokerRelayElem.hasAttribute("virtual-host")) {
|
||||
values.add("virtualHost", brokerRelayElem.getAttribute("virtual-host"));
|
||||
}
|
||||
if (brokerElement.hasAttribute("user-destination-broadcast")) {
|
||||
String destination = brokerElement.getAttribute("user-destination-broadcast");
|
||||
ManagedMap<String, Object> map = new ManagedMap<String, Object>();
|
||||
map.setSource(source);
|
||||
ManagedMap<String, Object> map = new ManagedMap<String, Object>();
|
||||
map.setSource(source);
|
||||
if (brokerRelayElem.hasAttribute("user-destination-broadcast")) {
|
||||
String destination = brokerRelayElem.getAttribute("user-destination-broadcast");
|
||||
map.put(destination, userDestHandler);
|
||||
}
|
||||
if (brokerRelayElem.hasAttribute("user-registry-broadcast")) {
|
||||
String destination = brokerRelayElem.getAttribute("user-registry-broadcast");
|
||||
map.put(destination, registerUserRegistryMessageHandler(userRegistry,
|
||||
brokerTemplate, destination, context, source));
|
||||
}
|
||||
if (!map.isEmpty()) {
|
||||
values.add("systemSubscriptions", map);
|
||||
}
|
||||
Class<?> handlerType = StompBrokerRelayMessageHandler.class;
|
||||
@@ -392,6 +426,22 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
return brokerDef;
|
||||
}
|
||||
|
||||
private RuntimeBeanReference registerUserRegistryMessageHandler(
|
||||
RuntimeBeanReference userRegistry, RuntimeBeanReference brokerTemplate,
|
||||
String destination, ParserContext context, Object source) {
|
||||
|
||||
Object scheduler = WebSocketNamespaceUtils.registerScheduler(SCHEDULER_BEAN_NAME, context, source);
|
||||
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(UserRegistryMessageHandler.class);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, userRegistry);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(1, brokerTemplate);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(2, destination);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(3, scheduler);
|
||||
|
||||
String beanName = registerBeanDef(beanDef, context, source);
|
||||
return new RuntimeBeanReference(beanName);
|
||||
}
|
||||
|
||||
private RuntimeBeanReference registerMessageConverter(Element element, ParserContext context, Object source) {
|
||||
Element convertersElement = DomUtils.getChildElementByTagName(element, "message-converters");
|
||||
ManagedList<? super Object> converters = new ManagedList<Object>();
|
||||
@@ -484,11 +534,10 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
|
||||
private RuntimeBeanReference registerUserDestResolver(Element brokerElem,
|
||||
RuntimeBeanReference userSessionRegistry, ParserContext context, Object source) {
|
||||
RuntimeBeanReference userRegistry, ParserContext context, Object source) {
|
||||
|
||||
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
|
||||
cavs.addIndexedArgumentValue(0, userSessionRegistry);
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultUserDestinationResolver.class, cavs, null);
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultUserDestinationResolver.class);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, userRegistry);
|
||||
if (brokerElem.hasAttribute("user-destination-prefix")) {
|
||||
beanDef.getPropertyValues().add("userDestinationPrefix", brokerElem.getAttribute("user-destination-prefix"));
|
||||
}
|
||||
@@ -496,19 +545,24 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
|
||||
private RuntimeBeanReference registerUserDestHandler(Element brokerElem,
|
||||
RuntimeBeanReference inChannel, RuntimeBeanReference brokerChannel,
|
||||
RuntimeBeanReference userDestinationResolver, ParserContext context, Object source) {
|
||||
RuntimeBeanReference userRegistry, RuntimeBeanReference inChannel,
|
||||
RuntimeBeanReference brokerChannel, ParserContext context, Object source) {
|
||||
|
||||
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
|
||||
cavs.addIndexedArgumentValue(0, inChannel);
|
||||
cavs.addIndexedArgumentValue(1, brokerChannel);
|
||||
cavs.addIndexedArgumentValue(2, userDestinationResolver);
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(UserDestinationMessageHandler.class, cavs, null);
|
||||
if (brokerElem.hasAttribute("user-destination-broadcast")) {
|
||||
String destination = brokerElem.getAttribute("user-destination-broadcast");
|
||||
beanDef.getPropertyValues().add("userDestinationBroadcast", destination);
|
||||
Object userDestResolver = registerUserDestResolver(brokerElem, userRegistry, context, source);
|
||||
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(UserDestinationMessageHandler.class);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, inChannel);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(1, brokerChannel);
|
||||
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(2, userDestResolver);
|
||||
|
||||
Element relayElement = DomUtils.getChildElementByTagName(brokerElem, "stomp-broker-relay");
|
||||
if (relayElement != null && relayElement.hasAttribute("user-destination-broadcast")) {
|
||||
String destination = relayElement.getAttribute("user-destination-broadcast");
|
||||
beanDef.getPropertyValues().add("broadcastDestination", destination);
|
||||
}
|
||||
return new RuntimeBeanReference(registerBeanDef(beanDef, context, source));
|
||||
|
||||
String beanName = registerBeanDef(beanDef, context, source);
|
||||
return new RuntimeBeanReference(beanName);
|
||||
}
|
||||
|
||||
private void registerWebSocketMessageBrokerStats(RootBeanDefinition broker, RuntimeBeanReference inChannel,
|
||||
@@ -530,7 +584,7 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
if (context.getRegistry().containsBeanDefinition(name)) {
|
||||
beanDef.getPropertyValues().add("outboundChannelExecutor", context.getRegistry().getBeanDefinition(name));
|
||||
}
|
||||
name = SOCKJS_SCHEDULER_BEAN_NAME;
|
||||
name = SCHEDULER_BEAN_NAME;
|
||||
if (context.getRegistry().containsBeanDefinition(name)) {
|
||||
beanDef.getPropertyValues().add("sockJsTaskScheduler", context.getRegistry().getBeanDefinition(name));
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class WebSocketNamespaceUtils {
|
||||
return handlerRef;
|
||||
}
|
||||
|
||||
public static RuntimeBeanReference registerSockJsService(Element element, String sockJsSchedulerName,
|
||||
public static RuntimeBeanReference registerSockJsService(Element element, String schedulerName,
|
||||
ParserContext context, Object source) {
|
||||
|
||||
Element sockJsElement = DomUtils.getChildElementByTagName(element, "sockjs");
|
||||
@@ -79,7 +79,7 @@ class WebSocketNamespaceUtils {
|
||||
scheduler = new RuntimeBeanReference(customTaskSchedulerName);
|
||||
}
|
||||
else {
|
||||
scheduler = registerSockJsScheduler(sockJsSchedulerName, context, source);
|
||||
scheduler = registerScheduler(schedulerName, context, source);
|
||||
}
|
||||
sockJsServiceDef.getConstructorArgumentValues().addIndexedArgumentValue(0, scheduler);
|
||||
|
||||
@@ -156,7 +156,7 @@ class WebSocketNamespaceUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static RuntimeBeanReference registerSockJsScheduler(String schedulerName, ParserContext context, Object source) {
|
||||
public static RuntimeBeanReference registerScheduler(String schedulerName, ParserContext context, Object source) {
|
||||
if (!context.getRegistry().containsBeanDefinition(schedulerName)) {
|
||||
RootBeanDefinition taskSchedulerDef = new RootBeanDefinition(ThreadPoolTaskScheduler.class);
|
||||
taskSchedulerDef.setSource(source);
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.messaging.simp.user.UserSessionRegistry;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -63,11 +62,10 @@ public class WebMvcStompEndpointRegistry implements StompEndpointRegistry {
|
||||
|
||||
public WebMvcStompEndpointRegistry(WebSocketHandler webSocketHandler,
|
||||
WebSocketTransportRegistration transportRegistration,
|
||||
UserSessionRegistry userSessionRegistry, TaskScheduler defaultSockJsTaskScheduler) {
|
||||
TaskScheduler defaultSockJsTaskScheduler) {
|
||||
|
||||
Assert.notNull(webSocketHandler, "'webSocketHandler' is required ");
|
||||
Assert.notNull(transportRegistration, "'transportRegistration' is required");
|
||||
Assert.notNull(userSessionRegistry, "'userSessionRegistry' is required");
|
||||
|
||||
this.webSocketHandler = webSocketHandler;
|
||||
this.subProtocolWebSocketHandler = unwrapSubProtocolWebSocketHandler(webSocketHandler);
|
||||
@@ -80,7 +78,6 @@ public class WebMvcStompEndpointRegistry implements StompEndpointRegistry {
|
||||
}
|
||||
|
||||
this.stompHandler = new StompSubProtocolHandler();
|
||||
this.stompHandler.setUserSessionRegistry(userSessionRegistry);
|
||||
|
||||
if (transportRegistration.getMessageSizeLimit() != null) {
|
||||
this.stompHandler.setMessageSizeLimit(transportRegistration.getMessageSizeLimit());
|
||||
|
||||
@@ -25,11 +25,14 @@ import org.springframework.messaging.simp.annotation.support.SimpAnnotationMetho
|
||||
import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler;
|
||||
import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration;
|
||||
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
|
||||
import org.springframework.messaging.simp.user.SimpUserRegistry;
|
||||
import org.springframework.messaging.simp.user.UserSessionRegistryAdapter;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.config.WebSocketMessageBrokerStats;
|
||||
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
|
||||
import org.springframework.web.socket.messaging.DefaultSimpUserRegistry;
|
||||
import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler;
|
||||
import org.springframework.web.socket.messaging.WebSocketAnnotationMethodMessageHandler;
|
||||
|
||||
@@ -58,10 +61,10 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac
|
||||
|
||||
@Bean
|
||||
public HandlerMapping stompWebSocketHandlerMapping() {
|
||||
WebSocketHandler handler = subProtocolWebSocketHandler();
|
||||
handler = decorateWebSocketHandler(handler);
|
||||
WebMvcStompEndpointRegistry registry = new WebMvcStompEndpointRegistry(handler,
|
||||
getTransportRegistration(), userSessionRegistry(), messageBrokerSockJsTaskScheduler());
|
||||
WebSocketHandler handler = decorateWebSocketHandler(subProtocolWebSocketHandler());
|
||||
WebSocketTransportRegistration transport = getTransportRegistration();
|
||||
ThreadPoolTaskScheduler scheduler = messageBrokerTaskScheduler();
|
||||
WebMvcStompEndpointRegistry registry = new WebMvcStompEndpointRegistry(handler, transport, scheduler);
|
||||
registry.setApplicationContext(getApplicationContext());
|
||||
registerStompEndpoints(registry);
|
||||
return registry.getHandlerMapping();
|
||||
@@ -90,33 +93,21 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac
|
||||
protected void configureWebSocketTransport(WebSocketTransportRegistration registry) {
|
||||
}
|
||||
|
||||
protected abstract void registerStompEndpoints(StompEndpointRegistry registry);
|
||||
|
||||
/**
|
||||
* The default TaskScheduler to use if none is configured via
|
||||
* {@link SockJsServiceRegistration#setTaskScheduler(org.springframework.scheduling.TaskScheduler)}, i.e.
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @EnableWebSocketMessageBroker
|
||||
* public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
*
|
||||
* public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
* registry.addEndpoint("/stomp").withSockJS().setTaskScheduler(myScheduler());
|
||||
* }
|
||||
*
|
||||
* // ...
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler messageBrokerSockJsTaskScheduler() {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setThreadNamePrefix("MessageBrokerSockJS-");
|
||||
scheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
|
||||
scheduler.setRemoveOnCancelPolicy(true);
|
||||
return scheduler;
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
protected SimpUserRegistry createLocalUserRegistry() {
|
||||
org.springframework.messaging.simp.user.UserSessionRegistry sessionRegistry = userSessionRegistry();
|
||||
if (sessionRegistry == null) {
|
||||
return new DefaultSimpUserRegistry();
|
||||
}
|
||||
else {
|
||||
return (userSessionRegistry() instanceof SimpUserRegistry ?
|
||||
(SimpUserRegistry) userSessionRegistry() : new UserSessionRegistryAdapter(sessionRegistry));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void registerStompEndpoints(StompEndpointRegistry registry);
|
||||
|
||||
@Bean
|
||||
public static CustomScopeConfigurer webSocketScopeConfigurer() {
|
||||
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
|
||||
@@ -138,7 +129,7 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac
|
||||
stats.setStompBrokerRelay(brokerRelay);
|
||||
stats.setInboundChannelExecutor(clientInboundChannelExecutor());
|
||||
stats.setOutboundChannelExecutor(clientOutboundChannelExecutor());
|
||||
stats.setSockJsTaskScheduler(messageBrokerSockJsTaskScheduler());
|
||||
stats.setSockJsTaskScheduler(messageBrokerTaskScheduler());
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -32,6 +34,8 @@ public abstract class AbstractSubProtocolEvent extends ApplicationEvent {
|
||||
|
||||
private final Message<byte[]> message;
|
||||
|
||||
private final Principal user;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new AbstractSubProtocolEvent.
|
||||
@@ -42,6 +46,19 @@ public abstract class AbstractSubProtocolEvent extends ApplicationEvent {
|
||||
super(source);
|
||||
Assert.notNull(message, "Message must not be null");
|
||||
this.message = message;
|
||||
this.user = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new AbstractSubProtocolEvent.
|
||||
* @param source the component that published the event (never {@code null})
|
||||
* @param message the incoming message
|
||||
*/
|
||||
protected AbstractSubProtocolEvent(Object source, Message<byte[]> message, Principal user) {
|
||||
super(source);
|
||||
Assert.notNull(message, "Message must not be null");
|
||||
this.message = message;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +77,13 @@ public abstract class AbstractSubProtocolEvent extends ApplicationEvent {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user for the session associated with the event.
|
||||
*/
|
||||
public Principal getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[" + this.message + "]";
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.event.SmartApplicationListener;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.user.DestinationUserNameProvider;
|
||||
import org.springframework.messaging.simp.user.SimpSession;
|
||||
import org.springframework.messaging.simp.user.SimpSubscription;
|
||||
import org.springframework.messaging.simp.user.SimpSubscriptionMatcher;
|
||||
import org.springframework.messaging.simp.user.SimpUser;
|
||||
import org.springframework.messaging.simp.user.SimpUserRegistry;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default, mutable, thread-safe implementation of {@link SimpUserRegistry}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.2
|
||||
*/
|
||||
public class DefaultSimpUserRegistry implements SimpUserRegistry, SmartApplicationListener {
|
||||
|
||||
private final Map<String, DefaultSimpUser> users = new ConcurrentHashMap<String, DefaultSimpUser>();
|
||||
|
||||
private final Map<String, DefaultSimpSession> sessions = new ConcurrentHashMap<String, DefaultSimpSession>();
|
||||
|
||||
|
||||
@Override
|
||||
public SimpUser getUser(String userName) {
|
||||
return this.users.get(userName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SimpUser> getUsers() {
|
||||
return new HashSet<SimpUser>(this.users.values());
|
||||
}
|
||||
|
||||
public Set<SimpSubscription> findSubscriptions(SimpSubscriptionMatcher matcher) {
|
||||
Set<SimpSubscription> result = new HashSet<SimpSubscription>();
|
||||
for (DefaultSimpSession session : this.sessions.values()) {
|
||||
for (SimpSubscription subscription : session.subscriptions.values()) {
|
||||
if (matcher.match(subscription)) {
|
||||
result.add(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
|
||||
return AbstractSubProtocolEvent.class.isAssignableFrom(eventType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSourceType(Class<?> sourceType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
|
||||
AbstractSubProtocolEvent subProtocolEvent = (AbstractSubProtocolEvent) event;
|
||||
Message<?> message = subProtocolEvent.getMessage();
|
||||
SimpMessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);
|
||||
String sessionId = accessor.getSessionId();
|
||||
|
||||
if (event instanceof SessionSubscribeEvent) {
|
||||
DefaultSimpSession session = this.sessions.get(sessionId);
|
||||
if (session != null) {
|
||||
String id = accessor.getSubscriptionId();
|
||||
String destination = accessor.getDestination();
|
||||
session.addSubscription(id, destination);
|
||||
}
|
||||
}
|
||||
else if (event instanceof SessionConnectedEvent) {
|
||||
Principal user = subProtocolEvent.getUser();
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
String name = user.getName();
|
||||
if (user instanceof DestinationUserNameProvider) {
|
||||
name = ((DestinationUserNameProvider) user).getDestinationUserName();
|
||||
}
|
||||
synchronized (this) {
|
||||
DefaultSimpUser simpUser = this.users.get(name);
|
||||
if (simpUser == null) {
|
||||
simpUser = new DefaultSimpUser(name, sessionId);
|
||||
this.users.put(name, simpUser);
|
||||
}
|
||||
else {
|
||||
simpUser.addSession(sessionId);
|
||||
}
|
||||
this.sessions.put(sessionId, (DefaultSimpSession) simpUser.getSession(sessionId));
|
||||
}
|
||||
}
|
||||
else if (event instanceof SessionDisconnectEvent) {
|
||||
synchronized (this) {
|
||||
DefaultSimpSession session = this.sessions.remove(sessionId);
|
||||
if (session != null) {
|
||||
DefaultSimpUser user = session.getUser();
|
||||
user.removeSession(sessionId);
|
||||
if (!user.hasSessions()) {
|
||||
this.users.remove(user.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (event instanceof SessionUnsubscribeEvent) {
|
||||
DefaultSimpSession session = this.sessions.get(sessionId);
|
||||
if (session != null) {
|
||||
String subscriptionId = accessor.getSubscriptionId();
|
||||
session.removeSubscription(subscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "users=" + this.users;
|
||||
}
|
||||
|
||||
private static class DefaultSimpUser implements SimpUser {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Map<String, SimpSession> sessions =
|
||||
new ConcurrentHashMap<String, SimpSession>(1);
|
||||
|
||||
|
||||
public DefaultSimpUser(String userName, String sessionId) {
|
||||
Assert.notNull(userName);
|
||||
Assert.notNull(sessionId);
|
||||
this.name = userName;
|
||||
this.sessions.put(sessionId, new DefaultSimpSession(sessionId, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSessions() {
|
||||
return !this.sessions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpSession getSession(String sessionId) {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SimpSession> getSessions() {
|
||||
return new HashSet<SimpSession>(this.sessions.values());
|
||||
}
|
||||
|
||||
void addSession(String sessionId) {
|
||||
DefaultSimpSession session = new DefaultSimpSession(sessionId, this);
|
||||
this.sessions.put(sessionId, session);
|
||||
}
|
||||
|
||||
void removeSession(String sessionId) {
|
||||
this.sessions.remove(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || !(other instanceof SimpUser)) {
|
||||
return false;
|
||||
}
|
||||
return this.name.equals(((SimpUser) other).getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "name=" + this.name + ", sessions=" + this.sessions;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultSimpSession implements SimpSession {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final DefaultSimpUser user;
|
||||
|
||||
private final Map<String, SimpSubscription> subscriptions = new ConcurrentHashMap<String, SimpSubscription>(4);
|
||||
|
||||
|
||||
public DefaultSimpSession(String id, DefaultSimpUser user) {
|
||||
Assert.notNull(id);
|
||||
Assert.notNull(user);
|
||||
this.id = id;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultSimpUser getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SimpSubscription> getSubscriptions() {
|
||||
return new HashSet<SimpSubscription>(this.subscriptions.values());
|
||||
}
|
||||
|
||||
void addSubscription(String id, String destination) {
|
||||
this.subscriptions.put(id, new DefaultSimpSubscription(id, destination, this));
|
||||
}
|
||||
|
||||
void removeSubscription(String id) {
|
||||
this.subscriptions.remove(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.id.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || !(other instanceof SimpSubscription)) {
|
||||
return false;
|
||||
}
|
||||
return this.id.equals(((SimpSubscription) other).getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "id=" + this.id + ", subscriptions=" + this.subscriptions;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultSimpSubscription implements SimpSubscription {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final DefaultSimpSession session;
|
||||
|
||||
private final String destination;
|
||||
|
||||
|
||||
public DefaultSimpSubscription(String id, String destination, DefaultSimpSession session) {
|
||||
Assert.notNull(id);
|
||||
Assert.hasText(destination);
|
||||
Assert.notNull(session);
|
||||
this.id = id;
|
||||
this.destination = destination;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultSimpSession getSession() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDestination() {
|
||||
return this.destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * this.id.hashCode() + getSession().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || !(other instanceof SimpSubscription)) {
|
||||
return false;
|
||||
}
|
||||
SimpSubscription otherSubscription = (SimpSubscription) other;
|
||||
return (getSession().getId().equals(otherSubscription.getSession().getId()) &&
|
||||
this.id.equals(otherSubscription.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "destination=" + this.destination;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
@@ -41,4 +43,8 @@ public class SessionConnectEvent extends AbstractSubProtocolEvent {
|
||||
super(source, message);
|
||||
}
|
||||
|
||||
public SessionConnectEvent(Object source, Message<byte[]> message, Principal user) {
|
||||
super(source, message, user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
@@ -37,4 +39,8 @@ public class SessionConnectedEvent extends AbstractSubProtocolEvent {
|
||||
super(source, message);
|
||||
}
|
||||
|
||||
public SessionConnectedEvent(Object source, Message<byte[]> message, Principal user) {
|
||||
super(source, message, user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
@@ -45,14 +47,21 @@ public class SessionDisconnectEvent extends AbstractSubProtocolEvent {
|
||||
* @param sessionId the disconnect message
|
||||
* @param closeStatus the status object
|
||||
*/
|
||||
public SessionDisconnectEvent(Object source, Message<byte[]> message, String sessionId, CloseStatus closeStatus) {
|
||||
public SessionDisconnectEvent(Object source, Message<byte[]> message, String sessionId,
|
||||
CloseStatus closeStatus) {
|
||||
|
||||
this(source, message, sessionId, closeStatus, null);
|
||||
}
|
||||
|
||||
public SessionDisconnectEvent(Object source, Message<byte[]> message, String sessionId,
|
||||
CloseStatus closeStatus, Principal user) {
|
||||
|
||||
super(source, message);
|
||||
Assert.notNull(sessionId, "'sessionId' must not be null");
|
||||
this.sessionId = sessionId;
|
||||
this.status = closeStatus;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the session id.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
@@ -34,4 +36,8 @@ public class SessionSubscribeEvent extends AbstractSubProtocolEvent {
|
||||
super(source, message);
|
||||
}
|
||||
|
||||
public SessionSubscribeEvent(Object source, Message<byte[]> message, Principal user) {
|
||||
super(source, message, user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
@@ -34,4 +36,8 @@ public class SessionUnsubscribeEvent extends AbstractSubProtocolEvent {
|
||||
super(source, message);
|
||||
}
|
||||
|
||||
public SessionUnsubscribeEvent(Object source, Message<byte[]> message, Principal user) {
|
||||
super(source, message, user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ 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;
|
||||
@@ -44,8 +43,6 @@ 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.DestinationUserNameProvider;
|
||||
import org.springframework.messaging.simp.user.UserSessionRegistry;
|
||||
import org.springframework.messaging.support.AbstractMessageChannel;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
|
||||
@@ -94,8 +91,6 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
|
||||
private int messageSizeLimit = 64 * 1024;
|
||||
|
||||
private UserSessionRegistry userSessionRegistry;
|
||||
|
||||
private final StompEncoder stompEncoder = new StompEncoder();
|
||||
|
||||
private final StompDecoder stompDecoder = new StompDecoder();
|
||||
@@ -134,21 +129,6 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
return this.messageSizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a registry with which to register active user session ids.
|
||||
* @see org.springframework.messaging.simp.user.UserDestinationMessageHandler
|
||||
*/
|
||||
public void setUserSessionRegistry(UserSessionRegistry registry) {
|
||||
this.userSessionRegistry = registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured UserSessionRegistry.
|
||||
*/
|
||||
public UserSessionRegistry getUserSessionRegistry() {
|
||||
return this.userSessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link MessageHeaderInitializer} to apply to the headers of all
|
||||
* messages created from decoded STOMP frames and other messages sent to the
|
||||
@@ -234,9 +214,11 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
StompHeaderAccessor headerAccessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
Principal user = session.getPrincipal();
|
||||
|
||||
headerAccessor.setSessionId(session.getId());
|
||||
headerAccessor.setSessionAttributes(session.getAttributes());
|
||||
headerAccessor.setUser(session.getPrincipal());
|
||||
headerAccessor.setUser(user);
|
||||
headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());
|
||||
if (!detectImmutableMessageInterceptor(outputChannel)) {
|
||||
headerAccessor.setImmutable();
|
||||
@@ -257,13 +239,13 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
SimpAttributesContextHolder.setAttributesFromMessage(message);
|
||||
if (this.eventPublisher != null) {
|
||||
if (StompCommand.CONNECT.equals(headerAccessor.getCommand())) {
|
||||
publishEvent(new SessionConnectEvent(this, message));
|
||||
publishEvent(new SessionConnectEvent(this, message, user));
|
||||
}
|
||||
else if (StompCommand.SUBSCRIBE.equals(headerAccessor.getCommand())) {
|
||||
publishEvent(new SessionSubscribeEvent(this, message));
|
||||
publishEvent(new SessionSubscribeEvent(this, message, user));
|
||||
}
|
||||
else if (StompCommand.UNSUBSCRIBE.equals(headerAccessor.getCommand())) {
|
||||
publishEvent(new SessionUnsubscribeEvent(this, message));
|
||||
publishEvent(new SessionUnsubscribeEvent(this, message, user));
|
||||
}
|
||||
}
|
||||
outputChannel.send(message);
|
||||
@@ -349,7 +331,8 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
try {
|
||||
SimpAttributes simpAttributes = new SimpAttributes(session.getId(), session.getAttributes());
|
||||
SimpAttributesContextHolder.setAttributes(simpAttributes);
|
||||
publishEvent(new SessionConnectedEvent(this, (Message<byte[]>) message));
|
||||
Principal user = session.getPrincipal();
|
||||
publishEvent(new SessionConnectedEvent(this, (Message<byte[]>) message, user));
|
||||
}
|
||||
finally {
|
||||
SimpAttributesContextHolder.resetAttributes();
|
||||
@@ -466,10 +449,6 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
if (principal != null) {
|
||||
accessor = toMutableAccessor(accessor, message);
|
||||
accessor.setNativeHeader(CONNECTED_USER_HEADER, principal.getName());
|
||||
if (this.userSessionRegistry != null) {
|
||||
String userName = getSessionRegistryUserName(principal);
|
||||
this.userSessionRegistry.registerSessionId(userName, session.getId());
|
||||
}
|
||||
}
|
||||
long[] heartbeat = accessor.getHeartbeat();
|
||||
if (heartbeat[1] > 0) {
|
||||
@@ -481,14 +460,6 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
return accessor;
|
||||
}
|
||||
|
||||
private String getSessionRegistryUserName(Principal principal) {
|
||||
String userName = principal.getName();
|
||||
if (principal instanceof DestinationUserNameProvider) {
|
||||
userName = ((DestinationUserNameProvider) principal).getDestinationUserName();
|
||||
}
|
||||
return userName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveSessionId(Message<?> message) {
|
||||
return SimpMessageHeaderAccessor.getSessionId(message.getHeaders());
|
||||
@@ -505,17 +476,13 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
@Override
|
||||
public void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus, MessageChannel outputChannel) {
|
||||
this.decoders.remove(session.getId());
|
||||
Principal principal = session.getPrincipal();
|
||||
if (principal != null && this.userSessionRegistry != null) {
|
||||
String userName = getSessionRegistryUserName(principal);
|
||||
this.userSessionRegistry.unregisterSessionId(userName, session.getId());
|
||||
}
|
||||
Message<byte[]> message = createDisconnectMessage(session);
|
||||
SimpAttributes simpAttributes = SimpAttributes.fromMessage(message);
|
||||
try {
|
||||
SimpAttributesContextHolder.setAttributes(simpAttributes);
|
||||
if (this.eventPublisher != null) {
|
||||
publishEvent(new SessionDisconnectEvent(this, message, session.getId(), closeStatus));
|
||||
Principal user = session.getPrincipal();
|
||||
publishEvent(new SessionDisconnectEvent(this, message, session.getId(), closeStatus, user));
|
||||
}
|
||||
outputChannel.send(message);
|
||||
}
|
||||
|
||||
@@ -344,6 +344,27 @@
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="user-destination-broadcast" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Set a destination to broadcast messages to that remain unresolved because
|
||||
the user is not connected. In a multi-application server scenario this
|
||||
gives other application servers a chance to try.
|
||||
By default this is not set.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="user-registry-broadcast" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Set a destination to broadcast the content of the local user registry to
|
||||
and to listen for such broadcasts from other servers. In a multi-application
|
||||
server scenarios this allows each server's user registry to be aware of
|
||||
users connected to other servers.
|
||||
By default this is not set.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="simple-broker">
|
||||
@@ -853,17 +874,6 @@
|
||||
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="user-destination-broadcast" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Set a destination to broadcast messages to that remain unresolved because
|
||||
the user is not connected. In a multi-application server scenario this
|
||||
gives other application servers a chance to try.
|
||||
Note: this option applies only when the stomp-broker-relay is enabled.
|
||||
By default this is not set.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
@@ -16,14 +16,20 @@
|
||||
|
||||
package org.springframework.web.socket.config;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.CustomScopeConfigurer;
|
||||
@@ -46,9 +52,11 @@ import org.springframework.messaging.simp.annotation.support.SimpAnnotationMetho
|
||||
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
|
||||
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
|
||||
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
|
||||
import org.springframework.messaging.simp.user.MultiServerUserRegistry;
|
||||
import org.springframework.messaging.simp.user.SimpUserRegistry;
|
||||
import org.springframework.messaging.simp.user.UserDestinationMessageHandler;
|
||||
import org.springframework.messaging.simp.user.UserDestinationResolver;
|
||||
import org.springframework.messaging.simp.user.UserSessionRegistry;
|
||||
import org.springframework.messaging.simp.user.UserRegistryMessageHandler;
|
||||
import org.springframework.messaging.support.AbstractSubscribableChannel;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
|
||||
@@ -64,7 +72,9 @@ import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TestWebSocketSession;
|
||||
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
|
||||
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
|
||||
import org.springframework.web.socket.messaging.DefaultSimpUserRegistry;
|
||||
import org.springframework.web.socket.messaging.StompSubProtocolHandler;
|
||||
import org.springframework.web.socket.messaging.SubProtocolHandler;
|
||||
import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
@@ -75,9 +85,6 @@ import org.springframework.web.socket.sockjs.transport.TransportType;
|
||||
import org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService;
|
||||
import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Test fixture for MessageBrokerBeanDefinitionParser.
|
||||
* See test configuration files websocket-config-broker-*.xml.
|
||||
@@ -133,7 +140,8 @@ public class MessageBrokerBeanDefinitionParserTests {
|
||||
assertEquals(25 * 1000, subProtocolWsHandler.getSendTimeLimit());
|
||||
assertEquals(1024 * 1024, subProtocolWsHandler.getSendBufferSizeLimit());
|
||||
|
||||
StompSubProtocolHandler stompHandler = (StompSubProtocolHandler) subProtocolWsHandler.getProtocolHandlerMap().get("v12.stomp");
|
||||
Map<String, SubProtocolHandler> handlerMap = subProtocolWsHandler.getProtocolHandlerMap();
|
||||
StompSubProtocolHandler stompHandler = (StompSubProtocolHandler) handlerMap.get("v12.stomp");
|
||||
assertNotNull(stompHandler);
|
||||
assertEquals(128 * 1024, stompHandler.getMessageSizeLimit());
|
||||
|
||||
@@ -166,15 +174,15 @@ public class MessageBrokerBeanDefinitionParserTests {
|
||||
instanceOf(BarTestInterceptor.class), instanceOf(OriginHandshakeInterceptor.class)));
|
||||
assertEquals(Arrays.asList("http://mydomain3.com", "http://mydomain4.com"), defaultSockJsService.getAllowedOrigins());
|
||||
|
||||
UserSessionRegistry userSessionRegistry = this.appContext.getBean(UserSessionRegistry.class);
|
||||
assertNotNull(userSessionRegistry);
|
||||
SimpUserRegistry userRegistry = this.appContext.getBean(SimpUserRegistry.class);
|
||||
assertNotNull(userRegistry);
|
||||
assertEquals(DefaultSimpUserRegistry.class, userRegistry.getClass());
|
||||
|
||||
UserDestinationResolver userDestResolver = this.appContext.getBean(UserDestinationResolver.class);
|
||||
assertNotNull(userDestResolver);
|
||||
assertThat(userDestResolver, Matchers.instanceOf(DefaultUserDestinationResolver.class));
|
||||
DefaultUserDestinationResolver defaultUserDestResolver = (DefaultUserDestinationResolver) userDestResolver;
|
||||
assertEquals("/personal/", defaultUserDestResolver.getDestinationPrefix());
|
||||
assertSame(stompHandler.getUserSessionRegistry(), defaultUserDestResolver.getUserSessionRegistry());
|
||||
|
||||
UserDestinationMessageHandler userDestHandler = this.appContext.getBean(UserDestinationMessageHandler.class);
|
||||
assertNotNull(userDestHandler);
|
||||
@@ -192,11 +200,12 @@ public class MessageBrokerBeanDefinitionParserTests {
|
||||
testChannel("clientInboundChannel", subscriberTypes, 2);
|
||||
testExecutor("clientInboundChannel", Runtime.getRuntime().availableProcessors() * 2, Integer.MAX_VALUE, 60);
|
||||
|
||||
subscriberTypes = Arrays.<Class<? extends MessageHandler>>asList(SubProtocolWebSocketHandler.class);
|
||||
subscriberTypes = Collections.singletonList(SubProtocolWebSocketHandler.class);
|
||||
testChannel("clientOutboundChannel", subscriberTypes, 1);
|
||||
testExecutor("clientOutboundChannel", Runtime.getRuntime().availableProcessors() * 2, Integer.MAX_VALUE, 60);
|
||||
|
||||
subscriberTypes = Arrays.<Class<? extends MessageHandler>>asList(SimpleBrokerMessageHandler.class, UserDestinationMessageHandler.class);
|
||||
subscriberTypes = Arrays.<Class<? extends MessageHandler>>asList(
|
||||
SimpleBrokerMessageHandler.class, UserDestinationMessageHandler.class);
|
||||
testChannel("brokerChannel", subscriberTypes, 1);
|
||||
try {
|
||||
this.appContext.getBean("brokerChannelExecutor", ThreadPoolTaskExecutor.class);
|
||||
@@ -260,7 +269,7 @@ public class MessageBrokerBeanDefinitionParserTests {
|
||||
testChannel("clientInboundChannel", subscriberTypes, 2);
|
||||
testExecutor("clientInboundChannel", Runtime.getRuntime().availableProcessors() * 2, Integer.MAX_VALUE, 60);
|
||||
|
||||
subscriberTypes = Arrays.<Class<? extends MessageHandler>>asList(SubProtocolWebSocketHandler.class);
|
||||
subscriberTypes = Collections.singletonList(SubProtocolWebSocketHandler.class);
|
||||
testChannel("clientOutboundChannel", subscriberTypes, 1);
|
||||
testExecutor("clientOutboundChannel", Runtime.getRuntime().availableProcessors() * 2, Integer.MAX_VALUE, 60);
|
||||
|
||||
@@ -275,11 +284,20 @@ public class MessageBrokerBeanDefinitionParserTests {
|
||||
// expected
|
||||
}
|
||||
|
||||
String destination = "/topic/unresolved-user-destination";
|
||||
UserDestinationMessageHandler userDestHandler = this.appContext.getBean(UserDestinationMessageHandler.class);
|
||||
assertEquals("/topic/unresolved", userDestHandler.getUserDestinationBroadcast());
|
||||
assertEquals(destination, userDestHandler.getBroadcastDestination());
|
||||
assertNotNull(messageBroker.getSystemSubscriptions());
|
||||
assertSame(userDestHandler, messageBroker.getSystemSubscriptions().get("/topic/unresolved"));
|
||||
assertSame(userDestHandler, messageBroker.getSystemSubscriptions().get(destination));
|
||||
|
||||
destination = "/topic/simp-user-registry";
|
||||
UserRegistryMessageHandler userRegistryHandler = this.appContext.getBean(UserRegistryMessageHandler.class);
|
||||
assertEquals(destination, userRegistryHandler.getBroadcastDestination());
|
||||
assertNotNull(messageBroker.getSystemSubscriptions());
|
||||
assertSame(userRegistryHandler, messageBroker.getSystemSubscriptions().get(destination));
|
||||
|
||||
SimpUserRegistry userRegistry = this.appContext.getBean(SimpUserRegistry.class);
|
||||
assertEquals(MultiServerUserRegistry.class, userRegistry.getClass());
|
||||
|
||||
String name = "webSocketMessageBrokerStats";
|
||||
WebSocketMessageBrokerStats stats = this.appContext.getBean(name, WebSocketMessageBrokerStats.class);
|
||||
@@ -339,7 +357,7 @@ public class MessageBrokerBeanDefinitionParserTests {
|
||||
testChannel("clientInboundChannel", subscriberTypes, 3);
|
||||
testExecutor("clientInboundChannel", 100, 200, 600);
|
||||
|
||||
subscriberTypes = Arrays.<Class<? extends MessageHandler>>asList(SubProtocolWebSocketHandler.class);
|
||||
subscriberTypes = Collections.singletonList(SubProtocolWebSocketHandler.class);
|
||||
|
||||
testChannel("clientOutboundChannel", subscriberTypes, 3);
|
||||
testExecutor("clientOutboundChannel", 101, 201, 601);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.socket.config.annotation;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -23,17 +25,12 @@ import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.simp.user.DefaultUserSessionRegistry;
|
||||
import org.springframework.messaging.simp.user.UserSessionRegistry;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.socket.messaging.StompSubProtocolHandler;
|
||||
import org.springframework.web.socket.messaging.SubProtocolHandler;
|
||||
import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Test fixture for
|
||||
* {@link org.springframework.web.socket.config.annotation.WebMvcStompEndpointRegistry}.
|
||||
@@ -46,17 +43,16 @@ public class WebMvcStompEndpointRegistryTests {
|
||||
|
||||
private SubProtocolWebSocketHandler webSocketHandler;
|
||||
|
||||
private UserSessionRegistry userSessionRegistry;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
SubscribableChannel inChannel = Mockito.mock(SubscribableChannel.class);
|
||||
SubscribableChannel outChannel = Mockito.mock(SubscribableChannel.class);
|
||||
this.webSocketHandler = new SubProtocolWebSocketHandler(inChannel, outChannel);
|
||||
this.userSessionRegistry = new DefaultUserSessionRegistry();
|
||||
this.endpointRegistry = new WebMvcStompEndpointRegistry(this.webSocketHandler,
|
||||
new WebSocketTransportRegistration(), this.userSessionRegistry, Mockito.mock(TaskScheduler.class));
|
||||
|
||||
WebSocketTransportRegistration transport = new WebSocketTransportRegistration();
|
||||
TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
|
||||
this.endpointRegistry = new WebMvcStompEndpointRegistry(this.webSocketHandler, transport, scheduler);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,9 +65,6 @@ public class WebMvcStompEndpointRegistryTests {
|
||||
assertNotNull(protocolHandlers.get("v10.stomp"));
|
||||
assertNotNull(protocolHandlers.get("v11.stomp"));
|
||||
assertNotNull(protocolHandlers.get("v12.stomp"));
|
||||
|
||||
StompSubProtocolHandler stompHandler = (StompSubProtocolHandler) protocolHandlers.get("v10.stomp");
|
||||
assertSame(this.userSessionRegistry, stompHandler.getUserSessionRegistry());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -136,19 +136,16 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webSocketTransportOptions() {
|
||||
public void webSocketHandler() {
|
||||
ApplicationContext config = createConfig(TestChannelConfig.class, TestConfigurer.class);
|
||||
SubProtocolWebSocketHandler subProtocolWebSocketHandler =
|
||||
config.getBean("subProtocolWebSocketHandler", SubProtocolWebSocketHandler.class);
|
||||
SubProtocolWebSocketHandler subWsHandler = config.getBean(SubProtocolWebSocketHandler.class);
|
||||
|
||||
assertEquals(1024 * 1024, subProtocolWebSocketHandler.getSendBufferSizeLimit());
|
||||
assertEquals(25 * 1000, subProtocolWebSocketHandler.getSendTimeLimit());
|
||||
assertEquals(1024 * 1024, subWsHandler.getSendBufferSizeLimit());
|
||||
assertEquals(25 * 1000, subWsHandler.getSendTimeLimit());
|
||||
|
||||
List<SubProtocolHandler> protocolHandlers = subProtocolWebSocketHandler.getProtocolHandlers();
|
||||
for(SubProtocolHandler protocolHandler : protocolHandlers) {
|
||||
assertTrue(protocolHandler instanceof StompSubProtocolHandler);
|
||||
assertEquals(128 * 1024, ((StompSubProtocolHandler) protocolHandler).getMessageSizeLimit());
|
||||
}
|
||||
Map<String, SubProtocolHandler> handlerMap = subWsHandler.getProtocolHandlerMap();
|
||||
StompSubProtocolHandler protocolHandler = (StompSubProtocolHandler) handlerMap.get("v12.stomp");
|
||||
assertEquals(128 * 1024, protocolHandler.getMessageSizeLimit());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.messaging;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.user.SimpSubscription;
|
||||
import org.springframework.messaging.simp.user.SimpSubscriptionMatcher;
|
||||
import org.springframework.messaging.simp.user.SimpUser;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
|
||||
/**
|
||||
* Test fixture for
|
||||
* {@link DefaultSimpUserRegistry}
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class DefaultSimpUserRegistryTests {
|
||||
|
||||
@Test
|
||||
public void addOneSessionId() {
|
||||
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<byte[]> message = createMessage(SimpMessageType.CONNECT_ACK, "123");
|
||||
SessionConnectedEvent event = new SessionConnectedEvent(this, message, user);
|
||||
|
||||
DefaultSimpUserRegistry registry = new DefaultSimpUserRegistry();
|
||||
registry.onApplicationEvent(event);
|
||||
|
||||
SimpUser simpUser = registry.getUser("joe");
|
||||
assertNotNull(simpUser);
|
||||
|
||||
assertEquals(1, simpUser.getSessions().size());
|
||||
assertNotNull(simpUser.getSession("123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addMultipleSessionIds() {
|
||||
|
||||
DefaultSimpUserRegistry registry = new DefaultSimpUserRegistry();
|
||||
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<byte[]> message = createMessage(SimpMessageType.CONNECT_ACK, "123");
|
||||
SessionConnectedEvent event = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(event);
|
||||
|
||||
message = createMessage(SimpMessageType.CONNECT_ACK, "456");
|
||||
event = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(event);
|
||||
|
||||
message = createMessage(SimpMessageType.CONNECT_ACK, "789");
|
||||
event = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(event);
|
||||
|
||||
SimpUser simpUser = registry.getUser("joe");
|
||||
assertNotNull(simpUser);
|
||||
|
||||
assertEquals(3, simpUser.getSessions().size());
|
||||
assertNotNull(simpUser.getSession("123"));
|
||||
assertNotNull(simpUser.getSession("456"));
|
||||
assertNotNull(simpUser.getSession("789"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeSessionIds() {
|
||||
|
||||
DefaultSimpUserRegistry registry = new DefaultSimpUserRegistry();
|
||||
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<byte[]> message = createMessage(SimpMessageType.CONNECT_ACK, "123");
|
||||
SessionConnectedEvent connectedEvent = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(connectedEvent);
|
||||
|
||||
message = createMessage(SimpMessageType.CONNECT_ACK, "456");
|
||||
connectedEvent = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(connectedEvent);
|
||||
|
||||
message = createMessage(SimpMessageType.CONNECT_ACK, "789");
|
||||
connectedEvent = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(connectedEvent);
|
||||
|
||||
SimpUser simpUser = registry.getUser("joe");
|
||||
assertNotNull(simpUser);
|
||||
assertEquals(3, simpUser.getSessions().size());
|
||||
|
||||
|
||||
CloseStatus status = CloseStatus.GOING_AWAY;
|
||||
message = createMessage(SimpMessageType.DISCONNECT, "456");
|
||||
SessionDisconnectEvent disconnectEvent = new SessionDisconnectEvent(this, message, "456", status, user);
|
||||
registry.onApplicationEvent(disconnectEvent);
|
||||
|
||||
message = createMessage(SimpMessageType.DISCONNECT, "789");
|
||||
disconnectEvent = new SessionDisconnectEvent(this, message, "789", status, user);
|
||||
registry.onApplicationEvent(disconnectEvent);
|
||||
|
||||
assertEquals(1, simpUser.getSessions().size());
|
||||
assertNotNull(simpUser.getSession("123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findSubscriptions() throws Exception {
|
||||
|
||||
DefaultSimpUserRegistry registry = new DefaultSimpUserRegistry();
|
||||
|
||||
TestPrincipal user = new TestPrincipal("joe");
|
||||
Message<byte[]> message = createMessage(SimpMessageType.CONNECT_ACK, "123");
|
||||
SessionConnectedEvent event = new SessionConnectedEvent(this, message, user);
|
||||
registry.onApplicationEvent(event);
|
||||
|
||||
message = createMessage(SimpMessageType.SUBSCRIBE, "123", "sub1", "/match");
|
||||
SessionSubscribeEvent subscribeEvent = new SessionSubscribeEvent(this, message, user);
|
||||
registry.onApplicationEvent(subscribeEvent);
|
||||
|
||||
message = createMessage(SimpMessageType.SUBSCRIBE, "123", "sub2", "/match");
|
||||
subscribeEvent = new SessionSubscribeEvent(this, message, user);
|
||||
registry.onApplicationEvent(subscribeEvent);
|
||||
|
||||
message = createMessage(SimpMessageType.SUBSCRIBE, "123", "sub3", "/not-a-match");
|
||||
subscribeEvent = new SessionSubscribeEvent(this, message, user);
|
||||
registry.onApplicationEvent(subscribeEvent);
|
||||
|
||||
Set<SimpSubscription> matches = registry.findSubscriptions(new SimpSubscriptionMatcher() {
|
||||
@Override
|
||||
public boolean match(SimpSubscription subscription) {
|
||||
return subscription.getDestination().equals("/match");
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals(2, matches.size());
|
||||
|
||||
Iterator<SimpSubscription> iterator = matches.iterator();
|
||||
Set<String> sessionIds = new HashSet<>(2);
|
||||
sessionIds.add(iterator.next().getId());
|
||||
sessionIds.add(iterator.next().getId());
|
||||
assertEquals(new HashSet<>(Arrays.asList("sub1", "sub2")), sessionIds);
|
||||
}
|
||||
|
||||
private Message<byte[]> createMessage(SimpMessageType type, String sessionId) {
|
||||
return createMessage(type, sessionId, null, null);
|
||||
}
|
||||
|
||||
private Message<byte[]> createMessage(SimpMessageType type, String sessionId, String subscriptionId,
|
||||
String destination) {
|
||||
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(type);
|
||||
accessor.setSessionId(sessionId);
|
||||
if (destination != null) {
|
||||
accessor.setDestination(destination);
|
||||
}
|
||||
if (subscriptionId != null) {
|
||||
accessor.setSubscriptionId(subscriptionId);
|
||||
}
|
||||
return MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders());
|
||||
}
|
||||
|
||||
|
||||
private static class TestPrincipal implements Principal {
|
||||
|
||||
private String name;
|
||||
|
||||
public TestPrincipal(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,9 +47,7 @@ import org.springframework.messaging.simp.TestPrincipal;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompEncoder;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.simp.user.DefaultUserSessionRegistry;
|
||||
import org.springframework.messaging.simp.user.DestinationUserNameProvider;
|
||||
import org.springframework.messaging.simp.user.UserSessionRegistry;
|
||||
import org.springframework.messaging.support.ChannelInterceptorAdapter;
|
||||
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
|
||||
@@ -96,9 +94,6 @@ public class StompSubProtocolHandlerTests {
|
||||
@Test
|
||||
public void handleMessageToClientWithConnectedFrame() {
|
||||
|
||||
UserSessionRegistry registry = new DefaultUserSessionRegistry();
|
||||
this.protocolHandler.setUserSessionRegistry(registry);
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
@@ -106,8 +101,6 @@ public class StompSubProtocolHandlerTests {
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
WebSocketMessage<?> textMessage = this.session.getSentMessages().get(0);
|
||||
assertEquals("CONNECTED\n" + "user-name:joe\n" + "\n" + "\u0000", textMessage.getPayload());
|
||||
|
||||
assertEquals(Collections.singleton("s1"), registry.getSessionIds("joe"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,9 +108,6 @@ public class StompSubProtocolHandlerTests {
|
||||
|
||||
this.session.setPrincipal(new UniqueUser("joe"));
|
||||
|
||||
UserSessionRegistry registry = new DefaultUserSessionRegistry();
|
||||
this.protocolHandler.setUserSessionRegistry(registry);
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
|
||||
this.protocolHandler.handleMessageToClient(this.session, message);
|
||||
@@ -125,9 +115,6 @@ public class StompSubProtocolHandlerTests {
|
||||
assertEquals(1, this.session.getSentMessages().size());
|
||||
WebSocketMessage<?> textMessage = this.session.getSentMessages().get(0);
|
||||
assertEquals("CONNECTED\n" + "user-name:joe\n" + "\n" + "\u0000", textMessage.getPayload());
|
||||
|
||||
assertEquals(Collections.<String>emptySet(), registry.getSessionIds("joe"));
|
||||
assertEquals(Collections.singleton("s1"), registry.getSessionIds("Me myself and I"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -348,8 +335,6 @@ public class StompSubProtocolHandlerTests {
|
||||
|
||||
TestPublisher publisher = new TestPublisher();
|
||||
|
||||
UserSessionRegistry registry = new DefaultUserSessionRegistry();
|
||||
this.protocolHandler.setUserSessionRegistry(registry);
|
||||
this.protocolHandler.setApplicationEventPublisher(publisher);
|
||||
this.protocolHandler.afterSessionStarted(this.session, this.channel);
|
||||
|
||||
@@ -387,8 +372,6 @@ public class StompSubProtocolHandlerTests {
|
||||
|
||||
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
|
||||
|
||||
UserSessionRegistry registry = new DefaultUserSessionRegistry();
|
||||
this.protocolHandler.setUserSessionRegistry(registry);
|
||||
this.protocolHandler.setApplicationEventPublisher(publisher);
|
||||
this.protocolHandler.afterSessionStarted(this.session, this.channel);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
||||
|
||||
<websocket:message-broker order="2" user-destination-broadcast="/topic/unresolved">
|
||||
<websocket:message-broker order="2">
|
||||
<websocket:stomp-endpoint path="/foo">
|
||||
<websocket:sockjs/>
|
||||
</websocket:stomp-endpoint>
|
||||
@@ -12,7 +12,9 @@
|
||||
client-login="clientlogin" client-passcode="clientpass"
|
||||
system-login="syslogin" system-passcode="syspass"
|
||||
heartbeat-send-interval="5000" heartbeat-receive-interval="5000"
|
||||
virtual-host="spring.io"/>
|
||||
virtual-host="spring.io"
|
||||
user-destination-broadcast="/topic/unresolved-user-destination"
|
||||
user-registry-broadcast="/topic/simp-user-registry"/>
|
||||
</websocket:message-broker>
|
||||
|
||||
<bean id="myHandler" class="org.springframework.web.socket.config.TestWebSocketHandler"/>
|
||||
|
||||
Reference in New Issue
Block a user