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
This commit is contained in:
parent
2440e05cc4
commit
204cb6f195
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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=\
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue