From 85921808b3749d976579b09a4e461ec74a7e68ea Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 29 Dec 2013 21:49:53 +0100 Subject: [PATCH] MappingJackson2(Http)MessageConverter logs warnings after canRead/canWrite checks This change involves a general upgrade to Jackson 2.3 in our build. Issue: SPR-11261 --- build.gradle | 16 +++--- .../MappingJackson2MessageConverter.java | 47 ++++++++++++++--- .../MappingJackson2HttpMessageConverter.java | 51 +++++++++++++++---- .../Jackson2ObjectMapperFactoryBeanTests.java | 21 ++++---- 4 files changed, 96 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index 78c24f8c482..55c22db2a10 100644 --- a/build.gradle +++ b/build.gradle @@ -93,7 +93,8 @@ configure(allprojects) { project -> "http://ehcache.org/apidocs/", "http://quartz-scheduler.org/api/2.1.7/", "http://jackson.codehaus.org/1.9.4/javadoc/", - "http://fasterxml.github.com/jackson-core/javadoc/2.2.0/", + "http://fasterxml.github.com/jackson-core/javadoc/2.3.0/", + "http://fasterxml.github.com/jackson-databind/javadoc/2.3.0/", "http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs" ] as String[] } @@ -381,7 +382,7 @@ project("spring-messaging") { compile(project(":spring-beans")) compile(project(":spring-core")) compile(project(":spring-context")) - optional("com.fasterxml.jackson.core:jackson-databind:2.2.2") + optional("com.fasterxml.jackson.core:jackson-databind:2.3.0") optional("org.projectreactor:reactor-core:1.0.0.RELEASE") optional("org.projectreactor:reactor-tcp:1.0.0.RELEASE") optional("org.eclipse.jetty.websocket:websocket-server:${jettyVersion}") { @@ -474,7 +475,7 @@ project("spring-jms") { optional("org.apache.geronimo.specs:geronimo-jta_1.1_spec:1.1") optional("javax.resource:connector-api:1.5") optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12") - optional("com.fasterxml.jackson.core:jackson-databind:2.2.2") + optional("com.fasterxml.jackson.core:jackson-databind:2.3.0") } } @@ -550,7 +551,7 @@ project("spring-web") { optional("org.apache.httpcomponents:httpclient:4.3.1") optional("org.apache.httpcomponents:httpasyncclient:4.0") optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12") - optional("com.fasterxml.jackson.core:jackson-databind:2.2.2") + optional("com.fasterxml.jackson.core:jackson-databind:2.3.0") optional("taglibs:standard:1.1.2") optional("org.eclipse.jetty:jetty-servlet:${jettyVersion}") { exclude group: "javax.servlet", module: "javax.servlet-api" @@ -593,8 +594,7 @@ project("spring-websocket") { exclude group: "javax.servlet", module: "javax.servlet" } optional("org.eclipse.jetty.websocket:websocket-client:${jettyVersion}") - optional("com.fasterxml.jackson.core:jackson-databind:2.2.2") - optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12") + optional("com.fasterxml.jackson.core:jackson-databind:2.3.0") testCompile("org.apache.tomcat.embed:tomcat-embed-core:8.0.0-RC5") testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}") testCompile("log4j:log4j:1.2.17") @@ -670,7 +670,7 @@ project("spring-webmvc") { optional("velocity-tools:velocity-tools-view:1.4") optional("org.freemarker:freemarker:2.3.19") optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12") - optional("com.fasterxml.jackson.core:jackson-databind:2.2.2") + optional("com.fasterxml.jackson.core:jackson-databind:2.3.0") provided("javax.servlet:jstl:1.2") provided("javax.servlet:javax.servlet-api:3.0.1") provided("javax.servlet.jsp:jsp-api:2.1") @@ -789,7 +789,7 @@ project("spring-test") { testCompile("org.hsqldb:hsqldb:${hsqldbVersion}") testCompile("org.hibernate:hibernate-validator:4.3.0.Final") testCompile("org.codehaus.jackson:jackson-mapper-asl:1.9.12") - testCompile("com.fasterxml.jackson.core:jackson-databind:2.2.2") + testCompile("com.fasterxml.jackson.core:jackson-databind:2.3.0") testCompile("com.thoughtworks.xstream:xstream:1.4.4") testCompile("rome:rome:1.0") testCompile("javax.activation:activation:1.1") 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 564febbf075..a6dc6ad8294 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 @@ -21,10 +21,7 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; - -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.util.MimeType; +import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; @@ -33,14 +30,27 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.ClassUtils; +import org.springframework.util.MimeType; + /** * A Jackson 2 based {@link MessageConverter} implementation. * + *

Tested against Jackson 2.2 and 2.3; compatible with Jackson 2.0 and higher. + * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 4.0 */ public class MappingJackson2MessageConverter extends AbstractMessageConverter { + // Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference + private static final boolean jackson23Available = + ClassUtils.hasMethod(ObjectMapper.class, "canDeserialize", JavaType.class, AtomicReference.class); + + private ObjectMapper objectMapper = new ObjectMapper(); private Boolean prettyPrint; @@ -76,13 +86,35 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter { if (targetClass == null) { return false; } - JavaType type = this.objectMapper.constructType(targetClass); - return (this.objectMapper.canDeserialize(type) && supportsMimeType(message.getHeaders())); + JavaType javaType = this.objectMapper.constructType(targetClass); + if (!jackson23Available || !logger.isWarnEnabled()) { + return (this.objectMapper.canDeserialize(javaType) && supportsMimeType(message.getHeaders())); + } + AtomicReference causeRef = new AtomicReference(); + if (this.objectMapper.canDeserialize(javaType, causeRef) && supportsMimeType(message.getHeaders())) { + return true; + } + Throwable cause = causeRef.get(); + if (cause != null) { + logger.warn("Failed to evaluate deserialization for type: " + javaType); + } + return false; } @Override protected boolean canConvertTo(Object payload, MessageHeaders headers) { - return (this.objectMapper.canSerialize(payload.getClass()) && supportsMimeType(headers)); + if (!jackson23Available || !logger.isWarnEnabled()) { + return (this.objectMapper.canSerialize(payload.getClass()) && supportsMimeType(headers)); + } + AtomicReference causeRef = new AtomicReference(); + if (this.objectMapper.canSerialize(payload.getClass(), causeRef) && supportsMimeType(headers)) { + return true; + } + Throwable cause = causeRef.get(); + if (cause != null) { + logger.warn("Failed to evaluate serialization for type: " + payload.getClass()); + } + return false; } @Override @@ -143,7 +175,6 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter { /** * Determine the JSON encoding to use for the given content type. - * * @param contentType the MIME type from the MessageHeaders, if any * @return the JSON encoding to use (never {@code null}) */ diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java index 17c1aa3f3ce..1fd65398420 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java @@ -19,6 +19,15 @@ package org.springframework.http.converter.json; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; @@ -28,14 +37,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.Assert; - -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.util.ClassUtils; /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that @@ -46,11 +48,12 @@ import com.fasterxml.jackson.databind.SerializationFeature; *

By default, this converter supports {@code application/json}. This can be overridden by setting the * {@link #setSupportedMediaTypes supportedMediaTypes} property. * - *

Tested against Jackson 2.2; compatible with Jackson 2.0 and higher. + *

Tested against Jackson 2.2 and 2.3; compatible with Jackson 2.0 and higher. * * @author Arjen Poutsma * @author Keith Donald * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1.2 */ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter @@ -58,6 +61,10 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + // Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference + private static final boolean jackson23Available = + ClassUtils.hasMethod(ObjectMapper.class, "canDeserialize", JavaType.class, AtomicReference.class); + private ObjectMapper objectMapper = new ObjectMapper(); @@ -147,12 +154,34 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv @Override public boolean canRead(Type type, Class contextClass, MediaType mediaType) { JavaType javaType = getJavaType(type, contextClass); - return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); + if (!jackson23Available || !logger.isWarnEnabled()) { + return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); + } + AtomicReference causeRef = new AtomicReference(); + if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) { + return true; + } + Throwable cause = causeRef.get(); + if (cause != null) { + logger.warn("Failed to evaluate deserialization for type: " + javaType); + } + return false; } @Override public boolean canWrite(Class clazz, MediaType mediaType) { - return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType)); + if (!jackson23Available || !logger.isWarnEnabled()) { + return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType)); + } + AtomicReference causeRef = new AtomicReference(); + if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) { + return true; + } + Throwable cause = causeRef.get(); + if (cause != null) { + logger.warn("Failed to evaluate serialization for type: " + clazz); + } + return false; } @Override diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java index b872c4902de..ebd606362c7 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java @@ -23,10 +23,6 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.springframework.beans.FatalBeanException; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -45,9 +41,13 @@ import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.BasicSerializerFactory; import com.fasterxml.jackson.databind.ser.Serializers; +import com.fasterxml.jackson.databind.ser.std.ClassSerializer; import com.fasterxml.jackson.databind.ser.std.NumberSerializers.NumberSerializer; -import com.fasterxml.jackson.databind.ser.std.StdJdkSerializers.ClassSerializer; import com.fasterxml.jackson.databind.type.SimpleType; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.FatalBeanException; import static org.junit.Assert.*; @@ -80,7 +80,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { @Test(expected = FatalBeanException.class) public void testUnknownFeature() { - this.factory.setFeaturesToEnable(new Object[] { Boolean.TRUE }); + this.factory.setFeaturesToEnable(Boolean.TRUE); this.factory.afterPropertiesSet(); } @@ -178,11 +178,11 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertEquals(ObjectMapper.class, this.factory.getObjectType()); } - private static final SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { + private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig(); } - private static final DeserializerFactoryConfig getDeserializerFactoryConfig(ObjectMapper objectMapper) { + private static DeserializerFactoryConfig getDeserializerFactoryConfig(ObjectMapper objectMapper) { return ((BasicDeserializerFactory) objectMapper.getDeserializationContext().getFactory()).getFactoryConfig(); } @@ -221,16 +221,13 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertFalse(getDeserializerFactoryConfig(objectMapper).hasDeserializers()); this.factory.setSerializationInclusion(JsonInclude.Include.NON_NULL); - this.factory.afterPropertiesSet(); assertTrue(objectMapper == this.factory.getObject()); - assertTrue(getSerializerFactoryConfig(objectMapper).hasSerializers()); assertTrue(getDeserializerFactoryConfig(objectMapper).hasDeserializers()); Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); - assertTrue(serializers.findSerializer(null, SimpleType.construct(Class.class), null) == serializer1); assertTrue(serializers.findSerializer(null, SimpleType.construct(Boolean.class), null) == serializer2); assertNull(serializers.findSerializer(null, SimpleType.construct(Number.class), null)); @@ -247,7 +244,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_FIELDS)); assertFalse(objectMapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)); assertFalse(objectMapper.getFactory().isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES)); - assertTrue(objectMapper.getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.NON_NULL); } + }