Support user destinations with multiple app servers

This change adds support for broadcasting messages with unresolved
user destinations so that other servers can try to resolve it.
That enables sending messages to users who may be connected to a
different server.

Issue: SPR-11620
This commit is contained in:
Rossen Stoyanchev
2015-04-14 20:50:48 -04:00
parent c384945a17
commit c29eae3307
14 changed files with 550 additions and 150 deletions

View File

@@ -159,15 +159,18 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
channelElem = DomUtils.getChildElementByTagName(element, "broker-channel");
RuntimeBeanReference brokerChannel = getMessageChannel("brokerChannel", channelElem, context, source);
RootBeanDefinition broker = registerMessageBroker(element, inChannel, outChannel, brokerChannel, 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);
RuntimeBeanReference resolver = registerUserDestinationResolver(element, sessionRegistry, context, source);
registerUserDestinationMessageHandler(inChannel, brokerChannel, resolver, context, source);
Map<String, Object> scopeMap = Collections.<String, Object>singletonMap("websocket", new SimpSessionScope());
RootBeanDefinition scopeConfigurer = new RootBeanDefinition(CustomScopeConfigurer.class);
scopeConfigurer.getPropertyValues().add("scopes", scopeMap);
@@ -308,11 +311,13 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
return new RuntimeBeanReference(registerBeanDef(beanDef, context, source));
}
private RootBeanDefinition registerMessageBroker(Element messageBrokerElement, RuntimeBeanReference inChannel,
RuntimeBeanReference outChannel, RuntimeBeanReference brokerChannel, ParserContext context, Object source) {
private RootBeanDefinition registerMessageBroker(Element brokerElement,
RuntimeBeanReference userDestHandler, RuntimeBeanReference inChannel,
RuntimeBeanReference outChannel, RuntimeBeanReference brokerChannel,
ParserContext context, Object source) {
Element simpleBrokerElem = DomUtils.getChildElementByTagName(messageBrokerElement, "simple-broker");
Element brokerRelayElem = DomUtils.getChildElementByTagName(messageBrokerElement, "stomp-broker-relay");
Element simpleBrokerElem = DomUtils.getChildElementByTagName(brokerElement, "simple-broker");
Element brokerRelayElem = DomUtils.getChildElementByTagName(brokerElement, "stomp-broker-relay");
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, inChannel);
@@ -324,8 +329,8 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
String prefix = simpleBrokerElem.getAttribute("prefix");
cavs.addIndexedArgumentValue(3, Arrays.asList(StringUtils.tokenizeToStringArray(prefix, ",")));
brokerDef = new RootBeanDefinition(SimpleBrokerMessageHandler.class, cavs, null);
if (messageBrokerElement.hasAttribute("path-matcher")) {
String pathMatcherRef = messageBrokerElement.getAttribute("path-matcher");
if (brokerElement.hasAttribute("path-matcher")) {
String pathMatcherRef = brokerElement.getAttribute("path-matcher");
brokerDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef));
}
if (simpleBrokerElem.hasAttribute("scheduler")) {
@@ -369,6 +374,13 @@ 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);
map.put(destination, userDestHandler);
values.add("systemSubscriptions", map);
}
Class<?> handlerType = StompBrokerRelayMessageHandler.class;
brokerDef = new RootBeanDefinition(handlerType, cavs, values);
}
@@ -471,7 +483,7 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
return list;
}
private RuntimeBeanReference registerUserDestinationResolver(Element brokerElem,
private RuntimeBeanReference registerUserDestResolver(Element brokerElem,
RuntimeBeanReference userSessionRegistry, ParserContext context, Object source) {
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
@@ -483,15 +495,19 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
return new RuntimeBeanReference(registerBeanDef(beanDef, context, source));
}
private RuntimeBeanReference registerUserDestinationMessageHandler(RuntimeBeanReference inChannel,
RuntimeBeanReference brokerChannel, RuntimeBeanReference userDestinationResolver,
ParserContext context, Object source) {
private RuntimeBeanReference registerUserDestHandler(Element brokerElem,
RuntimeBeanReference inChannel, RuntimeBeanReference brokerChannel,
RuntimeBeanReference userDestinationResolver, 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);
}
return new RuntimeBeanReference(registerBeanDef(beanDef, context, source));
}

View File

@@ -853,6 +853,17 @@
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>

View File

@@ -275,6 +275,12 @@ public class MessageBrokerBeanDefinitionParserTests {
// expected
}
UserDestinationMessageHandler userDestHandler = this.appContext.getBean(UserDestinationMessageHandler.class);
assertEquals("/topic/unresolved", userDestHandler.getUserDestinationBroadcast());
assertNotNull(messageBroker.getSystemSubscriptions());
assertSame(userDestHandler, messageBroker.getSystemSubscriptions().get("/topic/unresolved"));
String name = "webSocketMessageBrokerStats";
WebSocketMessageBrokerStats stats = this.appContext.getBean(name, WebSocketMessageBrokerStats.class);
String actual = stats.toString();

View File

@@ -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">
<websocket:message-broker order="2" user-destination-broadcast="/topic/unresolved">
<websocket:stomp-endpoint path="/foo">
<websocket:sockjs/>
</websocket:stomp-endpoint>