Add support for custom message converters

The Java and XML config for STOMP WebSocket applications now supports
configuring message converters.

Issue: SPR-11184
This commit is contained in:
Rossen Stoyanchev
2014-01-07 16:16:29 -05:00
parent 1fa4d9169f
commit 4342497305
14 changed files with 406 additions and 169 deletions

View File

@@ -153,7 +153,8 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
RuntimeBeanReference brokerChannel = getMessageChannel(beanName, channelElem, parserCxt, source);
registerMessageBroker(element, clientInChannel, clientOutChannel, brokerChannel, parserCxt, source);
RuntimeBeanReference messageConverter = registerBrokerMessageConverter(parserCxt, source);
RuntimeBeanReference messageConverter = registerBrokerMessageConverter(element, parserCxt, source);
RuntimeBeanReference messagingTemplate = registerBrokerMessagingTemplate(element, brokerChannel,
messageConverter, parserCxt, source);
@@ -339,24 +340,39 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
}
private RuntimeBeanReference registerBrokerMessageConverter(ParserContext parserCxt, Object source) {
private RuntimeBeanReference registerBrokerMessageConverter(Element element,
ParserContext parserCxt, Object source) {
Element convertersElement = DomUtils.getChildElementByTagName(element, "message-converters");
ManagedList<? super Object> convertersDef = new ManagedList<Object>();
if (convertersElement != null) {
convertersDef.setSource(source);
for (Element beanElement : DomUtils.getChildElementsByTagName(convertersElement, "bean", "ref")) {
Object object = parserCxt.getDelegate().parsePropertySubElement(beanElement, null);
convertersDef.add(object);
}
}
if (convertersElement == null || Boolean.valueOf(convertersElement.getAttribute("register-defaults"))) {
convertersDef.setSource(source);
if (jackson2Present) {
convertersDef.add(new RootBeanDefinition(MappingJackson2MessageConverter.class));
}
convertersDef.add(new RootBeanDefinition(StringMessageConverter.class));
convertersDef.add(new RootBeanDefinition(ByteArrayMessageConverter.class));
}
RootBeanDefinition contentTypeResolverDef = new RootBeanDefinition(DefaultContentTypeResolver.class);
ManagedList<RootBeanDefinition> convertersDef = new ManagedList<RootBeanDefinition>();
if (jackson2Present) {
convertersDef.add(new RootBeanDefinition(MappingJackson2MessageConverter.class));
contentTypeResolverDef.getPropertyValues().add("defaultMimeType", MimeTypeUtils.APPLICATION_JSON);
}
convertersDef.add(new RootBeanDefinition(StringMessageConverter.class));
convertersDef.add(new RootBeanDefinition(ByteArrayMessageConverter.class));
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, convertersDef);
cavs.addIndexedArgumentValue(1, contentTypeResolverDef);
RootBeanDefinition brokerMessage = new RootBeanDefinition(CompositeMessageConverter.class, cavs, null);
return new RuntimeBeanReference(registerBeanDef(brokerMessage,parserCxt, source));
return new RuntimeBeanReference(registerBeanDef(brokerMessage, parserCxt, source));
}
private RuntimeBeanReference registerBrokerMessagingTemplate(

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2002-2013 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.config.annotation;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import java.util.List;
/**
* A convenient abstract base class for {@link WebSocketMessageBrokerConfigurer}
* implementations providing empty method implementations for optional methods.
*
* @author Rossen Stoyanchev
* @since 4.0.1
*/
public abstract class AbstractWebSocketMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
return true;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
}
}

View File

@@ -21,6 +21,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.util.CollectionUtils;
@@ -71,6 +72,17 @@ public class DelegatingWebSocketMessageBrokerConfiguration extends WebSocketMess
}
}
@Override
protected boolean configureMessageConverters(List<MessageConverter> messageConverters) {
boolean registerDefaults = true;
for (WebSocketMessageBrokerConfigurer c : this.configurers) {
if (!c.configureMessageConverters(messageConverters)) {
registerDefaults = false;
}
}
return registerDefaults;
}
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
for (WebSocketMessageBrokerConfigurer c : this.configurers) {

View File

@@ -34,12 +34,13 @@ import org.springframework.context.annotation.Import;
* </pre>
* <p>
* Customize the imported configuration by implementing the
* {@link WebSocketMessageBrokerConfigurer} interface:
* {@link WebSocketMessageBrokerConfigurer} interface or more likely the convenient
* base class {@link AbstractWebSocketMessageBrokerConfigurer}:
*
* <pre class="code">
* &#064;Configuration
* &#064;EnableWebSocketMessageBroker
* public class MyConfiguration implements implements WebSocketMessageBrokerConfigurer {
* public class MyConfiguration implements extends AbstractWebSocketMessageBrokerConfigurer {
*
* &#064;Override
* public void registerStompEndpoints(StompEndpointRegistry registry) {

View File

@@ -17,9 +17,12 @@
package org.springframework.web.socket.config.annotation;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import java.util.List;
/**
* Defines methods for configuring message handling with simple messaging
* protocols (e.g. STOMP) from WebSocket clients. Typically used to customize
@@ -52,6 +55,20 @@ public interface WebSocketMessageBrokerConfigurer {
*/
void configureClientOutboundChannel(ChannelRegistration registration);
/**
* Configure the message converters to use when extracting the payload of
* messages in annotated methods and when sending messages (e.g. through the
* "broker" SimpMessagingTemplate).
* <p>
* The provided list, initially empty, can be used to add message converters
* while the boolean return value is used to determine if default message should
* be added as well.
*
* @param messageConverters initially an empty list of converters
* @return whether to also add default converter or not
*/
boolean configureMessageConverters(List<MessageConverter> messageConverters);
/**
* Configure message broker options.
*/

View File

@@ -481,6 +481,43 @@
<xsd:element name="simple-broker" type="simple-broker" />
<xsd:element name="stomp-broker-relay" type="stomp-broker-relay" />
</xsd:choice>
<xsd:element name="message-converters" minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure the message converters to use when extracting the payload of messages in annotated methods
and when sending messages (e.g. through the "broker" SimpMessagingTemplate.
MessageConverter registrations provided here will take precedence over MessageConverter types registered by default.
Also see the register-defaults attribute if you want to turn off default registrations entirely.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="beans:bean">
<xsd:annotation>
<xsd:documentation><![CDATA[
A MessageConverter bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to an HttpMessageConverter bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="register-defaults" type="xsd:boolean" default="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Whether or not default MessageConverter registrations should be added in addition to the ones provided within this element.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="client-inbound-channel" type="channel" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation><![CDATA[

View File

@@ -29,6 +29,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
@@ -246,6 +247,29 @@ public class MessageBrokerBeanDefinitionParserTests {
testExecutor("brokerChannel", 102, 202, 602);
}
@Test
public void messageConverters() {
loadBeanDefinitions("websocket-config-broker-converters.xml");
CompositeMessageConverter compositeConverter = this.appContext.getBean(CompositeMessageConverter.class);
assertNotNull(compositeConverter);
assertEquals(4, compositeConverter.getConverters().size());
assertEquals(StringMessageConverter.class, compositeConverter.getConverters().iterator().next().getClass());
}
@Test
public void messageConvertersDefaultsOff() {
loadBeanDefinitions("websocket-config-broker-converters-defaults-off.xml");
CompositeMessageConverter compositeConverter = this.appContext.getBean(CompositeMessageConverter.class);
assertNotNull(compositeConverter);
assertEquals(1, compositeConverter.getConverters().size());
assertEquals(StringMessageConverter.class, compositeConverter.getConverters().iterator().next().getClass());
}
private void testChannel(String channelName, List<Class<? extends MessageHandler>> subscriberTypes,
int interceptorCount) {

View File

@@ -118,7 +118,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@Configuration
static class TestSimpleMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
static class TestSimpleMessageBrokerConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Bean
public TestController subscriptionController() {
@@ -130,19 +130,6 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
registry.addEndpoint("/simpleBroker");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// SimpleBroker used by default
}
}
@Configuration

View File

@@ -47,6 +47,7 @@ import org.springframework.web.socket.JettyWebSocketTestServer;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.TomcatWebSocketTestServer;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.client.jetty.JettyWebSocketClient;
@@ -205,7 +206,7 @@ public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketInteg
@ComponentScan(basePackageClasses=SimpAnnotationMethodIntegrationTests.class,
useDefaultFilters=false,
includeFilters=@ComponentScan.Filter(IntegrationTestController.class))
static class TestMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
static class TestMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private HandshakeHandler handshakeHandler; // can't rely on classpath for server detection
@@ -215,14 +216,6 @@ public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketInteg
registry.addEndpoint("/ws").setHandshakeHandler(this.handshakeHandler);
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
}
@Override
public void configureMessageBroker(MessageBrokerRegistry configurer) {
configurer.setApplicationDestinationPrefixes("/app");

View File

@@ -0,0 +1,19 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
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-4.0.xsd">
<websocket:message-broker>
<websocket:stomp-endpoint path="/foo" />
<websocket:simple-broker />
<websocket:message-converters register-defaults="false">
<bean class="org.springframework.messaging.converter.StringMessageConverter"/>
</websocket:message-converters>
</websocket:message-broker>
</beans>

View File

@@ -0,0 +1,19 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
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-4.0.xsd">
<websocket:message-broker>
<websocket:stomp-endpoint path="/foo" />
<websocket:simple-broker />
<websocket:message-converters>
<bean class="org.springframework.messaging.converter.StringMessageConverter"/>
</websocket:message-converters>
</websocket:message-broker>
</beans>