Allow Jackson builder modules configuration to be customized

Modules (well-known or user provided) registration is now performed
first in order to allow their configuration to be customized by more
specific ones like custom serializers or deserializers.

Issue: SPR-12634
This commit is contained in:
Sebastien Deleuze 2015-01-26 11:08:08 +01:00
parent 6e10f7c8cf
commit 5fb6d6d89c
5 changed files with 157 additions and 30 deletions

View File

@ -697,6 +697,7 @@ project("spring-web") {
testCompile("org.apache.taglibs:taglibs-standard-jstlel:1.2.1") { testCompile("org.apache.taglibs:taglibs-standard-jstlel:1.2.1") {
exclude group: "org.apache.taglibs", module: "taglibs-standard-spec" 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") testRuntime("com.sun.mail:javax.mail:1.5.2")
} }

View File

@ -389,11 +389,12 @@ public class Jackson2ObjectMapperBuilder {
} }
/** /**
* Specify one or more modules by class (or class name in XML), * Specify one or more modules by class to be registered with
* to be registered with the {@link ObjectMapper}. * the {@link ObjectMapper}.
* <p>Modules specified here will be registered in combination with * <p>Modules specified here will be registered after
* Spring's autodetection of JSR-310 and Joda-Time, or Jackson's * 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.
* <p>Specify either this or {@link #modules}, not both. * <p>Specify either this or {@link #modules}, not both.
* @see com.fasterxml.jackson.databind.Module * @see com.fasterxml.jackson.databind.Module
*/ */
@ -481,6 +482,29 @@ public class Jackson2ObjectMapperBuilder {
public void configure(ObjectMapper objectMapper) { public void configure(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null"); 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<? extends Module> module : this.modulesToInstall) {
objectMapper.registerModule(BeanUtils.instantiate(module));
}
}
}
if (this.dateFormat != null) { if (this.dateFormat != null) {
objectMapper.setDateFormat(this.dateFormat); objectMapper.setDateFormat(this.dateFormat);
} }
@ -511,29 +535,6 @@ public class Jackson2ObjectMapperBuilder {
configureFeature(objectMapper, feature, this.features.get(feature)); 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<? extends Module> 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) { if (this.propertyNamingStrategy != null) {
objectMapper.setPropertyNamingStrategy(this.propertyNamingStrategy); objectMapper.setPropertyNamingStrategy(this.propertyNamingStrategy);
} }

View File

@ -346,11 +346,12 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
} }
/** /**
* Specify one or more modules by class (or class name in XML), * Specify one or more modules by class (or class name in XML)
* to be registered with the {@link ObjectMapper}. * to be registered with the {@link ObjectMapper}.
* <p>Modules specified here will be registered in combination with * <p>Modules specified here will be registered after
* Spring's autodetection of JSR-310 and Joda-Time, or Jackson's * 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.
* <p>Specify either this or {@link #setModules}, not both. * <p>Specify either this or {@link #setModules}, not both.
* @since 4.0.1 * @since 4.0.1
* @see com.fasterxml.jackson.databind.Module * @see com.fasterxml.jackson.databind.Module

View File

@ -16,7 +16,9 @@
package org.springframework.http.converter.json; package org.springframework.http.converter.json;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -28,6 +30,8 @@ import java.util.TimeZone;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser; 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.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException; 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.deser.std.DateDeserializers;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleModule; 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.BasicSerializerFactory;
import com.fasterxml.jackson.databind.ser.Serializers; import com.fasterxml.jackson.databind.ser.Serializers;
import com.fasterxml.jackson.databind.ser.std.ClassSerializer; import com.fasterxml.jackson.databind.ser.std.ClassSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer; import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.databind.type.SimpleType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper; 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.junit.Test;
import org.springframework.beans.FatalBeanException; import org.springframework.beans.FatalBeanException;
@ -211,6 +220,32 @@ public class Jackson2ObjectMapperBuilderTests {
assertTrue(serializers.findSerializer(null, SimpleType.construct(Integer.class), null) == serializer1); 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) { private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) {
return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig(); return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig();
} }
@ -231,6 +266,7 @@ public class Jackson2ObjectMapperBuilderTests {
public void serializerByType() { public void serializerByType() {
JsonSerializer<Number> serializer = new NumberSerializer(); JsonSerializer<Number> serializer = new NumberSerializer();
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.modules(new ArrayList<>()) // Disable well-known modules detection
.serializerByType(Boolean.class, serializer).build(); .serializerByType(Boolean.class, serializer).build();
assertTrue(getSerializerFactoryConfig(objectMapper).hasSerializers()); assertTrue(getSerializerFactoryConfig(objectMapper).hasSerializers());
Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next();
@ -241,6 +277,7 @@ public class Jackson2ObjectMapperBuilderTests {
public void deserializerByType() throws JsonMappingException { public void deserializerByType() throws JsonMappingException {
JsonDeserializer<Date> deserializer = new DateDeserializers.DateDeserializer(); JsonDeserializer<Date> deserializer = new DateDeserializers.DateDeserializer();
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.modules(new ArrayList<>()) // Disable well-known modules detection
.deserializerByType(Date.class, deserializer).build(); .deserializerByType(Date.class, deserializer).build();
assertTrue(getDeserializerFactoryConfig(objectMapper).hasDeserializers()); assertTrue(getDeserializerFactoryConfig(objectMapper).hasDeserializers());
Deserializers deserializers = getDeserializerFactoryConfig(objectMapper).deserializers().iterator().next(); Deserializers deserializers = getDeserializerFactoryConfig(objectMapper).deserializers().iterator().next();
@ -284,6 +321,7 @@ public class Jackson2ObjectMapperBuilderTests {
JsonSerializer<Number> serializer2 = new NumberSerializer(); JsonSerializer<Number> serializer2 = new NumberSerializer();
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json() Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json()
.modules(new ArrayList<>()) // Disable well-known modules detection
.serializers(serializer1) .serializers(serializer1)
.serializersByType(Collections.<Class<?>, JsonSerializer<?>>singletonMap(Boolean.class, serializer2)) .serializersByType(Collections.<Class<?>, JsonSerializer<?>>singletonMap(Boolean.class, serializer2))
.deserializersByType(deserializerMap) .deserializersByType(deserializerMap)
@ -346,4 +384,25 @@ public class Jackson2ObjectMapperBuilderTests {
assertTrue(xmlObjectMapper.getClass().isAssignableFrom(XmlMapper.class)); 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);
}
}
} }

View File

@ -16,7 +16,9 @@
package org.springframework.http.converter.json; package org.springframework.http.converter.json;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -28,6 +30,8 @@ import java.util.TimeZone;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser; 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.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer; 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.deser.std.DateDeserializers.DateDeserializer;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleModule; 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.BasicSerializerFactory;
import com.fasterxml.jackson.databind.ser.Serializers; import com.fasterxml.jackson.databind.ser.Serializers;
import com.fasterxml.jackson.databind.ser.std.ClassSerializer; import com.fasterxml.jackson.databind.ser.std.ClassSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer; import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.databind.type.SimpleType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper; 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.Before;
import org.junit.Test; import org.junit.Test;
@ -221,6 +230,40 @@ public class Jackson2ObjectMapperFactoryBeanTests {
assertTrue(serializers.findSerializer(null, SimpleType.construct(Integer.class), null) == serializer1); 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<Class<?>, 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 @Test
public void simpleSetup() { public void simpleSetup() {
this.factory.afterPropertiesSet(); this.factory.afterPropertiesSet();
@ -283,6 +326,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
JsonSerializer<Class<?>> serializer1 = new ClassSerializer(); JsonSerializer<Class<?>> serializer1 = new ClassSerializer();
JsonSerializer<Number> serializer2 = new NumberSerializer(); JsonSerializer<Number> serializer2 = new NumberSerializer();
factory.setModules(new ArrayList<>()); // Disable well-known modules detection
factory.setSerializers(serializer1); factory.setSerializers(serializer1);
factory.setSerializersByType(Collections.<Class<?>, JsonSerializer<?>> singletonMap(Boolean.class, serializer2)); factory.setSerializersByType(Collections.<Class<?>, JsonSerializer<?>> singletonMap(Boolean.class, serializer2));
factory.setDeserializersByType(deserializers); factory.setDeserializersByType(deserializers);
@ -350,4 +394,25 @@ public class Jackson2ObjectMapperFactoryBeanTests {
assertEquals(XmlMapper.class, this.factory.getObjectType()); 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);
}
}
} }