Skip to content

Commit

Permalink
Auto-configure WebSocket JSON converter to use context’s ObjectMapper
Browse files Browse the repository at this point in the history
This commit adds auto-configuration support for WebSocket-based
messaging. When the user enables WebSocket messaging (typically via
@EnableWebSocket and @EnableWebSocketMessageBroker) and an ObjectMapper
bean exists, a MappingJackson2MessageConverter that uses the
ObjectMapper will be configured. This causes any spring.jackson.*
configuration to affect WebSocket message conversion in the same way
as it affects HTTP message conversion.

Closes gh-2445
  • Loading branch information
wilkinsona committed Sep 29, 2015
1 parent 2440e05 commit 204cb6f
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 1 deletion.
@@ -0,0 +1,79 @@
/*
* Copyright 2012-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.boot.autoconfigure.websocket;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
* {@link EnableAutoConfiguration Auto-configuration} for WebSocket-based messaging.
*
* @author Andy Wilkinson
* @since 1.3.0
*/
@ConditionalOnWebApplication
@ConditionalOnClass(WebSocketMessageBrokerConfigurer.class)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
public class WebSocketMessagingAutoConfiguration {

@Configuration
@ConditionalOnBean({ DelegatingWebSocketMessageBrokerConfiguration.class,
ObjectMapper.class })
@ConditionalOnClass(ObjectMapper.class)
static class WebSocketMessageConverterConfiguration extends
AbstractWebSocketMessageBrokerConfigurer {

@Autowired
private ObjectMapper objectMapper;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// The user must register their own endpoints
}

@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(this.objectMapper);
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
converter.setContentTypeResolver(resolver);
messageConverters.add(converter);
return true;
}

}

}
Expand Up @@ -82,7 +82,8 @@ org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguratio
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration

# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
Expand Down
@@ -0,0 +1,230 @@
/*
* Copyright 2012-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.boot.autoconfigure.websocket;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.web.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.SimpleMessageConverter;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.Transport;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

/**
* Tests for {@link WebSocketMessagingAutoConfiguration}.
*
* @author Andy Wilkinson
*/
public class WebSocketMessagingAutoConfigurationTests {

private AnnotationConfigEmbeddedWebApplicationContext context = new AnnotationConfigEmbeddedWebApplicationContext();

private SockJsClient sockJsClient;

@Before
public void setup() {
List<Transport> transports = Arrays.asList(new WebSocketTransport(
new StandardWebSocketClient()), new RestTemplateXhrTransport(
new RestTemplate()));
this.sockJsClient = new SockJsClient(transports);
}

@After
public void tearDown() {
this.context.close();
this.sockJsClient.stop();
}

@Test
public void basicMessagingWithJson() throws Throwable {
EnvironmentTestUtils.addEnvironment(this.context, "server.port:0",
"spring.jackson.serialization.indent-output:true");
this.context.register(WebSocketMessagingConfiguration.class);
new ServerPortInfoApplicationContextInitializer().initialize(this.context);
this.context.refresh();
WebSocketStompClient stompClient = new WebSocketStompClient(this.sockJsClient);
final AtomicReference<Throwable> failure = new AtomicReference<Throwable>();
final AtomicReference<Object> result = new AtomicReference<Object>();
final CountDownLatch latch = new CountDownLatch(1);
StompSessionHandler handler = new StompSessionHandlerAdapter() {

@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
session.subscribe("/app/data", new StompFrameHandler() {

@Override
public void handleFrame(StompHeaders headers, Object payload) {
result.set(payload);
latch.countDown();
}

@Override
public Type getPayloadType(StompHeaders headers) {
return Object.class;
}

});
}

@Override
public void handleFrame(StompHeaders headers, Object payload) {
latch.countDown();
}

@Override
public void handleException(StompSession session, StompCommand command,
StompHeaders headers, byte[] payload, Throwable exception) {
failure.set(exception);
latch.countDown();
}

@Override
public void handleTransportError(StompSession session, Throwable exception) {
failure.set(exception);
latch.countDown();
}

};

stompClient.setMessageConverter(new SimpleMessageConverter());
stompClient.connect("ws://localhost:{port}/messaging", handler, this.context
.getEnvironment().getProperty("local.server.port"));

if (!latch.await(30, TimeUnit.SECONDS)) {
if (failure.get() != null) {
throw failure.get();
}
else {
fail("Response was not received within 30 seconds");
}
}
assertThat(new String((byte[]) result.get()),
is(equalTo(String.format("{%n \"foo\" : 5,%n \"bar\" : \"baz\"%n}"))));
}

@Configuration
@EnableWebSocket
@EnableConfigurationProperties
@EnableWebSocketMessageBroker
@ImportAutoConfiguration({ JacksonAutoConfiguration.class,
EmbeddedServletContainerAutoConfiguration.class,
ServerPropertiesAutoConfiguration.class,
WebSocketMessagingAutoConfiguration.class,
DispatcherServletAutoConfiguration.class })
static class WebSocketMessagingConfiguration extends
AbstractWebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messaging").withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
}

@Bean
public MessagingController messagingController() {
return new MessagingController();
}

@Bean
public TomcatEmbeddedServletContainerFactory tomcat() {
return new TomcatEmbeddedServletContainerFactory(0);
}

@Bean
public TomcatWebSocketContainerCustomizer tomcatCuztomiser() {
return new TomcatWebSocketContainerCustomizer();
}

}

@Controller
static class MessagingController {

@SubscribeMapping("/data")
Data getData() {
return new Data(5, "baz");
}

}

static class Data {

private int foo;

private String bar;

Data(int foo, String bar) {
this.foo = foo;
this.bar = bar;
}

public int getFoo() {
return this.foo;
}

public String getBar() {
return this.bar;
}

}

}

2 comments on commit 204cb6f

@bobbywarner
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wilkinsona All my JSON output from the web socket now looks like this with Spring Boot 1.3:

"{\"id\":1000,\"type\":\"message\"}"

Instead of this:

{"id": 1000, "type": "message"}

I've tried over-ridding the configureMessageConverters method in my web socket config class, but it does not seem to have any effect. Please let me know if you have any recommendations on how to "disable" this and revert back to the previous behavior so my JSON is rendered as the later example.

@wilkinsona
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bobbywarner Commit comments aren't really the best place to discuss this. If you are looking for recommendations, please ask a question on StackOverflow using the spring-boot tag. If you believe you have found a bug, please open a new issue. Before doing so, please double-check the output. "{\"id\":1000,\"type\":\"message\"}" isn't valid JSON and, as far as I can tell, isn't what's produced.

Please sign in to comment.