diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java index 59a5b6c002..b64abf2fce 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java @@ -29,7 +29,9 @@ import javax.jms.Message; import javax.jms.Session; import javax.jms.TextMessage; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -42,6 +44,12 @@ import org.springframework.util.ClassUtils; * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}. * Converts from a {@link TextMessage} or {@link BytesMessage} to an object. * + *

It customizes Jackson's default properties with the following ones: + *

+ * *

Tested against Jackson 2.2; compatible with Jackson 2.0 and higher. * * @author Mark Pollack @@ -57,7 +65,7 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl public static final String DEFAULT_ENCODING = "UTF-8"; - private ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper; private MessageType targetType = MessageType.BYTES; @@ -74,6 +82,12 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl private ClassLoader beanClassLoader; + public MappingJackson2MessageConverter() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + /** * Specify the {@link ObjectMapper} to use instead of using the default. */ diff --git a/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java b/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java index b1e1c747ef..af9e2c7599 100644 --- a/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java @@ -115,6 +115,32 @@ public class MappingJackson2MessageConverterTests { verify(textMessageMock).setStringProperty("__typeid__", HashMap.class.getName()); } + @Test + public void fromTextMessage() throws Exception { + TextMessage textMessageMock = mock(TextMessage.class); + MyBean unmarshalled = new MyBean("bar"); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + MyBean result = (MyBean)converter.fromMessage(textMessageMock); + assertEquals("Invalid result", result, unmarshalled); + } + + @Test + public void fromTextMessageWithUnknownProperty() throws Exception { + TextMessage textMessageMock = mock(TextMessage.class); + MyBean unmarshalled = new MyBean("bar"); + + String text = "{\"foo\":\"bar\", \"unknownProperty\":\"value\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + MyBean result = (MyBean)converter.fromMessage(textMessageMock); + assertEquals("Invalid result", result, unmarshalled); + } + @Test public void fromTextMessageAsObject() throws Exception { TextMessage textMessageMock = mock(TextMessage.class); @@ -141,4 +167,47 @@ public class MappingJackson2MessageConverterTests { assertEquals("Invalid result", result, unmarshalled); } + public static class MyBean { + + public MyBean() { + } + + public MyBean(String foo) { + this.foo = foo; + } + + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MyBean bean = (MyBean) o; + + if (foo != null ? !foo.equals(bean.foo) : bean.foo != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return foo != null ? foo.hashCode() : 0; + } + } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java index bd719d5fad..b43cb576ae 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java @@ -26,7 +26,9 @@ import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -39,6 +41,12 @@ import org.springframework.util.MimeType; /** * A Jackson 2 based {@link MessageConverter} implementation. * + *

It customizes Jackson's default properties with the following ones: + *

+ * *

Compatible with Jackson 2.1 and higher. * * @author Rossen Stoyanchev @@ -52,16 +60,18 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter { ClassUtils.hasMethod(ObjectMapper.class, "canDeserialize", JavaType.class, AtomicReference.class); - private ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper; private Boolean prettyPrint; public MappingJackson2MessageConverter() { super(new MimeType("application", "json", Charset.forName("UTF-8"))); + this.objectMapper = new ObjectMapper(); + this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } - /** * Set the {@code ObjectMapper} for this converter. * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index 259b8ca024..91632a2af0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -289,16 +289,20 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC converters.add(new StringMessageConverter()); converters.add(new ByteArrayMessageConverter()); if (jackson2Present) { - DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); - resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); - MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); - converter.setContentTypeResolver(resolver); - converters.add(converter); + converters.add(createJacksonConverter()); } } return new CompositeMessageConverter(converters); } + protected MappingJackson2MessageConverter createJacksonConverter() { + DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); + resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + converter.setContentTypeResolver(resolver); + return converter; + } + /** * Override this method to add custom message converters. * @param messageConverters the list to add converters to, initially empty diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java index e8393d4120..1b3ede7534 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java @@ -87,11 +87,12 @@ public class MappingJackson2MessageConverterTests { this.converter.fromMessage(message, MyBean.class); } - @Test(expected = MessageConversionException.class) + @Test public void fromMessageValidJsonWithUnknownProperty() throws IOException { String payload = "{\"string\":\"string\",\"unknownProperty\":\"value\"}"; Message message = MessageBuilder.withPayload(payload.getBytes(UTF_8)).build(); - this.converter.fromMessage(message, MyBean.class); + MyBean myBean = (MyBean)this.converter.fromMessage(message, MyBean.class); + assertEquals("string", myBean.getString()); } @Test diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index 45c872d46c..6ae0155d02 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -434,12 +434,7 @@ public class Jackson2ObjectMapperBuilder { objectMapper.registerModule(module); } - if (!this.features.containsKey(MapperFeature.DEFAULT_VIEW_INCLUSION)) { - configureFeature(objectMapper, MapperFeature.DEFAULT_VIEW_INCLUSION, false); - } - if (!this.features.containsKey(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) { - configureFeature(objectMapper, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } + customizeDefaultFeatures(objectMapper); for (Object feature : this.features.keySet()) { configureFeature(objectMapper, feature, this.features.get(feature)); } @@ -475,6 +470,17 @@ public class Jackson2ObjectMapperBuilder { } } + // Any change to this method should be also applied to spring-jms and spring-messaging + // MappingJackson2MessageConverter default constructors + private void customizeDefaultFeatures(ObjectMapper objectMapper) { + if (!this.features.containsKey(MapperFeature.DEFAULT_VIEW_INCLUSION)) { + configureFeature(objectMapper, MapperFeature.DEFAULT_VIEW_INCLUSION, false); + } + if (!this.features.containsKey(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) { + configureFeature(objectMapper, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + } + @SuppressWarnings("unchecked") private void addSerializers(SimpleModule module) { for (Class type : this.serializers.keySet()) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java index 7d7e06f27b..9784018336 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.messaging.support.ImmutableMessageChannelInterceptor; import org.w3c.dom.Element; @@ -385,6 +386,8 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser { RootBeanDefinition resolverDef = new RootBeanDefinition(DefaultContentTypeResolver.class); resolverDef.getPropertyValues().add("defaultMimeType", MimeTypeUtils.APPLICATION_JSON); jacksonConverterDef.getPropertyValues().add("contentTypeResolver", resolverDef); + // Use Jackson builder in order to have JSR-310 and Joda-Time modules registered automatically + jacksonConverterDef.getPropertyValues().add("objectMapper", Jackson2ObjectMapperBuilder.json().build()); converters.add(jacksonConverterDef); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java index 809d8f6434..cc4eff6b79 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java @@ -18,6 +18,8 @@ package org.springframework.web.socket.config.annotation; import org.springframework.beans.factory.config.CustomScopeConfigurer; import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.SimpSessionScope; import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler; import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration; @@ -132,4 +134,12 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac return stats; } + @Override + protected MappingJackson2MessageConverter createJacksonConverter() { + MappingJackson2MessageConverter messageConverter = super.createJacksonConverter(); + // Use Jackson builder in order to have JSR-310 and Joda-Time modules registered automatically + messageConverter.setObjectMapper(Jackson2ObjectMapperBuilder.json().build()); + return messageConverter; + } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/Jackson2SockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/Jackson2SockJsMessageCodec.java index 089e7ccc06..e879b26f8e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/Jackson2SockJsMessageCodec.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/Jackson2SockJsMessageCodec.java @@ -20,13 +20,25 @@ import java.io.IOException; import java.io.InputStream; import com.fasterxml.jackson.core.io.JsonStringEncoder; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.util.Assert; /** * A Jackson 2.x codec for encoding and decoding SockJS messages. * + *

It customizes Jackson's default properties with the following ones: + *

+ * + *

Note that Jackson's JSR-310 and Joda-Time support modules will be registered automatically + * when available (and when Java 8 and Joda-Time themselves are available, respectively). + * * @author Rossen Stoyanchev * @since 4.0 */ @@ -36,7 +48,7 @@ public class Jackson2SockJsMessageCodec extends AbstractSockJsMessageCodec { public Jackson2SockJsMessageCodec() { - this.objectMapper = new ObjectMapper(); + this.objectMapper = Jackson2ObjectMapperBuilder.json().build(); } public Jackson2SockJsMessageCodec(ObjectMapper objectMapper) {