diff --git a/build.gradle b/build.gradle index b7707ffb62..4c5fe0045d 100644 --- a/build.gradle +++ b/build.gradle @@ -697,6 +697,7 @@ project("spring-web") { testCompile("org.apache.taglibs:taglibs-standard-jstlel:1.2.1") { exclude group: "org.apache.taglibs", module: "taglibs-standard-spec" } + testCompile("com.fasterxml.jackson.datatype:jackson-datatype-joda:${jackson2Version}") testRuntime("com.sun.mail:javax.mail:1.5.2") } 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 7a42c8f0bd..0169a3bb2b 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 @@ -389,11 +389,12 @@ public class Jackson2ObjectMapperBuilder { } /** - * Specify one or more modules by class (or class name in XML), - * to be registered with the {@link ObjectMapper}. - *

Modules specified here will be registered in combination with + * Specify one or more modules by class to be registered with + * the {@link ObjectMapper}. + *

Modules specified here will be registered after * Spring's autodetection of JSR-310 and Joda-Time, or Jackson's - * finding of modules (see {@link #findModulesViaServiceLoader}). + * finding of modules (see {@link #findModulesViaServiceLoader}), + * allowing to eventually override their configuration. *

Specify either this or {@link #modules}, not both. * @see com.fasterxml.jackson.databind.Module */ @@ -481,6 +482,29 @@ public class Jackson2ObjectMapperBuilder { public void configure(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); + if (this.modules != null) { + // Complete list of modules given + for (Module module : this.modules) { + // Using Jackson 2.0+ registerModule method, not Jackson 2.2+ registerModules + objectMapper.registerModule(module); + } + } + else { + // Combination of modules by class presence in the classpath and class names specified + if (this.findModulesViaServiceLoader) { + // Jackson 2.2+ + objectMapper.registerModules(ObjectMapper.findModules(this.moduleClassLoader)); + } + else { + registerWellKnownModulesIfAvailable(objectMapper); + } + if (this.modulesToInstall != null) { + for (Class module : this.modulesToInstall) { + objectMapper.registerModule(BeanUtils.instantiate(module)); + } + } + } + if (this.dateFormat != null) { objectMapper.setDateFormat(this.dateFormat); } @@ -511,29 +535,6 @@ public class Jackson2ObjectMapperBuilder { configureFeature(objectMapper, feature, this.features.get(feature)); } - if (this.modules != null) { - // Complete list of modules given - for (Module module : this.modules) { - // Using Jackson 2.0+ registerModule method, not Jackson 2.2+ registerModules - objectMapper.registerModule(module); - } - } - else { - // Combination of modules by class names specified and class presence in the classpath - if (this.modulesToInstall != null) { - for (Class module : this.modulesToInstall) { - objectMapper.registerModule(BeanUtils.instantiate(module)); - } - } - if (this.findModulesViaServiceLoader) { - // Jackson 2.2+ - objectMapper.registerModules(ObjectMapper.findModules(this.moduleClassLoader)); - } - else { - registerWellKnownModulesIfAvailable(objectMapper); - } - } - if (this.propertyNamingStrategy != null) { objectMapper.setPropertyNamingStrategy(this.propertyNamingStrategy); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java index b6d7cb57ad..4288b03b97 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java @@ -346,11 +346,12 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBeanModules specified here will be registered in combination with + *

Modules specified here will be registered after * Spring's autodetection of JSR-310 and Joda-Time, or Jackson's - * finding of modules (see {@link #setFindModulesViaServiceLoader}). + * finding of modules (see {@link #setFindModulesViaServiceLoader}), + * allowing to eventually override their configuration. *

Specify either this or {@link #setModules}, not both. * @since 4.0.1 * @see com.fasterxml.jackson.databind.Module diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index c0f1abd77e..79d28e73d8 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -16,7 +16,9 @@ package org.springframework.http.converter.json; +import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -28,6 +30,8 @@ import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; @@ -44,12 +48,17 @@ import com.fasterxml.jackson.databind.deser.Deserializers; import com.fasterxml.jackson.databind.deser.std.DateDeserializers; import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.module.SimpleSerializers; 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.NumberSerializer; import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.joda.cfg.JacksonJodaDateFormat; +import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; import org.junit.Test; import org.springframework.beans.FatalBeanException; @@ -211,6 +220,32 @@ public class Jackson2ObjectMapperBuilderTests { assertTrue(serializers.findSerializer(null, SimpleType.construct(Integer.class), null) == serializer1); } + @Test + public void defaultModules() throws JsonProcessingException, UnsupportedEncodingException { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build(); + DateTime dateTime = DateTime.parse("2011-12-03T10:15:30"); + assertEquals("1322903730000", new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); + } + + @Test // SPR-12634 + public void customizeDefaultModules() throws JsonProcessingException, UnsupportedEncodingException { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .modulesToInstall(CustomModule.class).build(); + DateTime dateTime = DateTime.parse("2011-12-03T10:15:30"); + assertEquals("\"2011-12-03\"", new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); + } + + @Test // SPR-12634 + public void customizeDefaultModulesWithSerializer() throws JsonProcessingException, UnsupportedEncodingException { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .serializerByType(DateTime.class, new DateTimeSerializer(new JacksonJodaDateFormat(DateTimeFormat.forPattern("YYYY-MM-dd").withZoneUTC()))) + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build(); + DateTime dateTime = DateTime.parse("2011-12-03T10:15:30"); + assertEquals("\"2011-12-03\"", new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); + } + + private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig(); } @@ -231,6 +266,7 @@ public class Jackson2ObjectMapperBuilderTests { public void serializerByType() { JsonSerializer serializer = new NumberSerializer(); ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .modules(new ArrayList<>()) // Disable well-known modules detection .serializerByType(Boolean.class, serializer).build(); assertTrue(getSerializerFactoryConfig(objectMapper).hasSerializers()); Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); @@ -241,6 +277,7 @@ public class Jackson2ObjectMapperBuilderTests { public void deserializerByType() throws JsonMappingException { JsonDeserializer deserializer = new DateDeserializers.DateDeserializer(); ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .modules(new ArrayList<>()) // Disable well-known modules detection .deserializerByType(Date.class, deserializer).build(); assertTrue(getDeserializerFactoryConfig(objectMapper).hasDeserializers()); Deserializers deserializers = getDeserializerFactoryConfig(objectMapper).deserializers().iterator().next(); @@ -284,6 +321,7 @@ public class Jackson2ObjectMapperBuilderTests { JsonSerializer serializer2 = new NumberSerializer(); Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json() + .modules(new ArrayList<>()) // Disable well-known modules detection .serializers(serializer1) .serializersByType(Collections., JsonSerializer>singletonMap(Boolean.class, serializer2)) .deserializersByType(deserializerMap) @@ -346,4 +384,25 @@ public class Jackson2ObjectMapperBuilderTests { assertTrue(xmlObjectMapper.getClass().isAssignableFrom(XmlMapper.class)); } + + public static class CustomModule extends Module { + + @Override + public String getModuleName() { + return this.getClass().getSimpleName(); + } + + @Override + public Version version() { + return Version.unknownVersion(); + } + + @Override + public void setupModule(SetupContext context) { + SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(DateTime.class, new DateTimeSerializer(new JacksonJodaDateFormat(DateTimeFormat.forPattern("YYYY-MM-dd").withZoneUTC()))); + context.addSerializers(serializers); + } + } + } 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 1563b55c9b..0e926d1255 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 @@ -16,7 +16,9 @@ package org.springframework.http.converter.json; +import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -28,6 +30,8 @@ import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; @@ -42,12 +46,17 @@ import com.fasterxml.jackson.databind.deser.BasicDeserializerFactory; import com.fasterxml.jackson.databind.deser.std.DateDeserializers.DateDeserializer; import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.module.SimpleSerializers; 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.NumberSerializer; import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.joda.cfg.JacksonJodaDateFormat; +import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; import org.junit.Before; import org.junit.Test; @@ -221,6 +230,40 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertTrue(serializers.findSerializer(null, SimpleType.construct(Integer.class), null) == serializer1); } + @Test + public void defaultModules() throws JsonProcessingException, UnsupportedEncodingException { + this.factory.afterPropertiesSet(); + ObjectMapper objectMapper = this.factory.getObject(); + + DateTime dateTime = DateTime.parse("2011-12-03T10:15:30"); + assertEquals("1322903730000", new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); + } + + @Test // SPR-12634 + public void customizeDefaultModules() throws JsonProcessingException, UnsupportedEncodingException { + this.factory.setFeaturesToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + this.factory.setModulesToInstall(CustomModule.class); + this.factory.afterPropertiesSet(); + ObjectMapper objectMapper = this.factory.getObject(); + + DateTime dateTime = DateTime.parse("2011-12-03T10:15:30"); + assertEquals("\"2011-12-03\"", new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); + } + + @Test // SPR-12634 + public void customizeDefaultModulesWithSerializer() throws JsonProcessingException, UnsupportedEncodingException { + Map, JsonSerializer> serializers = new HashMap<>(); + serializers.put(DateTime.class, new DateTimeSerializer(new JacksonJodaDateFormat(DateTimeFormat.forPattern("YYYY-MM-dd").withZoneUTC()))); + + this.factory.setSerializersByType(serializers); + this.factory.setFeaturesToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + this.factory.afterPropertiesSet(); + ObjectMapper objectMapper = this.factory.getObject(); + + DateTime dateTime = DateTime.parse("2011-12-03T10:15:30"); + assertEquals("\"2011-12-03\"", new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); + } + @Test public void simpleSetup() { this.factory.afterPropertiesSet(); @@ -283,6 +326,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { JsonSerializer> serializer1 = new ClassSerializer(); JsonSerializer serializer2 = new NumberSerializer(); + factory.setModules(new ArrayList<>()); // Disable well-known modules detection factory.setSerializers(serializer1); factory.setSerializersByType(Collections., JsonSerializer> singletonMap(Boolean.class, serializer2)); factory.setDeserializersByType(deserializers); @@ -350,4 +394,25 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertEquals(XmlMapper.class, this.factory.getObjectType()); } + + public static class CustomModule extends Module { + + @Override + public String getModuleName() { + return this.getClass().getSimpleName(); + } + + @Override + public Version version() { + return Version.unknownVersion(); + } + + @Override + public void setupModule(SetupContext context) { + SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(DateTime.class, new DateTimeSerializer(new JacksonJodaDateFormat(DateTimeFormat.forPattern("YYYY-MM-dd").withZoneUTC()))); + context.addSerializers(serializers); + } + } + }