Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto-configure WebSocket JSON converter to use context’s ObjectMapper
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
1 parent
2440e05
commit 204cb6f
Showing
3 changed files
with
311 additions
and
1 deletion.
There are no files selected for viewing
79 changes: 79 additions & 0 deletions
79
...org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 230 additions & 0 deletions
230
...pringframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
} | ||
|
||
} |
204cb6f
There was a problem hiding this comment.
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:
Instead of this:
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.204cb6f
There was a problem hiding this comment.
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.