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:
Rossen Stoyanchev
2015-05-06 18:31:26 -04:00
parent 52153bd454
commit 281588d7bb
46 changed files with 2627 additions and 484 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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);