Make Jackson date format and property naming strategy configurable

Allow the environment to be used to configure Jackson's date format
and property naming strategy

Closes gh-1628
This commit is contained in:
Marcel Overdijk 2014-09-26 13:41:15 +02:00 committed by Andy Wilkinson
parent fcd855cd5e
commit e070d55491
4 changed files with 208 additions and 0 deletions

View File

@ -16,11 +16,15 @@
package org.springframework.boot.autoconfigure.jackson;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -33,6 +37,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@ -40,6 +46,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
@ -57,6 +64,7 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
*
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
* @since 1.1.0
*/
@Configuration
@ -107,9 +115,65 @@ public class JacksonAutoConfiguration {
configureParserFeatures(objectMapper);
configureGeneratorFeatures(objectMapper);
configureDateFormat(objectMapper);
configurePropertyNamingStrategy(objectMapper);
return objectMapper;
}
private void configurePropertyNamingStrategy(ObjectMapper objectMapper) {
// We support a fully qualified class name extending Jackson's
// PropertyNamingStrategy or a string value corresponding to the constant
// names in PropertyNamingStrategy which hold default provided implementations
String propertyNamingStrategy = this.jacksonProperties
.getPropertyNamingStrategy();
if (propertyNamingStrategy != null) {
try {
Class<?> clazz = ClassUtils.forName(propertyNamingStrategy, null);
objectMapper
.setPropertyNamingStrategy((PropertyNamingStrategy) BeanUtils
.instantiateClass(clazz));
}
catch (ClassNotFoundException e) {
// Find the field (this way we automatically support new constants
// that may be added by Jackson in the future)
Field field = ReflectionUtils.findField(PropertyNamingStrategy.class,
propertyNamingStrategy, PropertyNamingStrategy.class);
if (field != null) {
try {
objectMapper
.setPropertyNamingStrategy((PropertyNamingStrategy) field
.get(null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
else {
throw new IllegalArgumentException("Constant named '"
+ propertyNamingStrategy + "' not found on "
+ PropertyNamingStrategy.class.getName());
}
}
}
}
private void configureDateFormat(ObjectMapper objectMapper) {
// We support a fully qualified class name extending DateFormat or a date
// pattern string value
String dateFormat = this.jacksonProperties.getDateFormat();
if (dateFormat != null) {
try {
Class<?> clazz = ClassUtils.forName(dateFormat, null);
objectMapper.setDateFormat((DateFormat) BeanUtils
.instantiateClass(clazz));
}
catch (ClassNotFoundException e) {
objectMapper.setDateFormat(new SimpleDateFormat(dateFormat));
}
}
}
private void configureDeserializationFeatures(ObjectMapper objectMapper) {
for (Entry<DeserializationFeature, Boolean> entry : this.jacksonProperties
.getDeserialization().entrySet()) {

View File

@ -31,11 +31,16 @@ import com.fasterxml.jackson.databind.SerializationFeature;
* Configuration properties to configure Jackson
*
* @author Andy Wilkinson
* @author Marcel Overdijk
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "spring.jackson")
public class JacksonProperties {
private String dateFormat;
private String propertyNamingStrategy;
private Map<SerializationFeature, Boolean> serialization = new HashMap<SerializationFeature, Boolean>();
private Map<DeserializationFeature, Boolean> deserialization = new HashMap<DeserializationFeature, Boolean>();
@ -46,6 +51,22 @@ public class JacksonProperties {
private Map<JsonGenerator.Feature, Boolean> generator = new HashMap<JsonGenerator.Feature, Boolean>();
public String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public String getPropertyNamingStrategy() {
return this.propertyNamingStrategy;
}
public void setPropertyNamingStrategy(String propertyNamingStrategy) {
this.propertyNamingStrategy = propertyNamingStrategy;
}
public Map<SerializationFeature, Boolean> getSerialization() {
return this.serialization;
}

View File

@ -17,8 +17,11 @@
package org.springframework.boot.autoconfigure.jackson;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
import org.junit.After;
import org.junit.Before;
@ -60,6 +63,8 @@ import static org.mockito.Mockito.verify;
*
* @author Dave Syer
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
*/
public class JacksonAutoConfigurationTests {
@ -108,6 +113,110 @@ public class JacksonAutoConfigurationTests {
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo()));
}
/*
* ObjectMapper does not contain method to get the date format of the mapper. See
* https://github.com/FasterXML/jackson-databind/issues/559 If such a method will be
* provided below tests can be simplified.
*/
@Test
public void noCustomDateFormat() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertEquals(String.valueOf(date.getTime()), mapper.writeValueAsString(date));
}
@Test
public void customDateFormat() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.date-format:yyyyMMddHHmmss");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertEquals("\"19880625203000\"", mapper.writeValueAsString(date));
}
@Test
public void customDateFormatClass() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils
.addEnvironment(
this.context,
"spring.jackson.date-format:org.springframework.boot.autoconfigure.jackson.JacksonAutoConfigurationTests.MyDateFormat");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertEquals("\"1988-06-25 20:30:00\"", mapper.writeValueAsString(date));
}
public static class MyDateFormat extends SimpleDateFormat {
public MyDateFormat() {
super("yyyy-MM-dd HH:mm:ss");
}
}
/*
* ObjectMapper does not contain method to get the property naming strategy of the
* mapper. See https://github.com/FasterXML/jackson-databind/issues/559 If such a
* method will be provided below tests can be simplified.
*/
@Test
public void noCustomPropertyNamingStrategy() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"propertyName\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyCamelCaseToLowerCaseWithUnderscores()
throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils
.addEnvironment(this.context,
"spring.jackson.property-naming-strategy:CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"property_name\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyPascalCaseToCamelCase() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.property-naming-strategy:PASCAL_CASE_TO_CAMEL_CASE");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"PropertyName\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyLowerCase() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.property-naming-strategy:LOWER_CASE");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"propertyname\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyClass() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils
.addEnvironment(
this.context,
"spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"property_name\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void enableSerializationFeature() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
@ -297,4 +406,16 @@ public class JacksonAutoConfigurationTests {
}
protected static class Bar {
private String propertyName;
public String getPropertyName() {
return this.propertyName;
}
public void setPropertyName(String propertyName) {
this.propertyName = propertyName;
}
}
}

View File

@ -94,6 +94,8 @@ content into your application; rather pick only the properties that you need.
spring.resources.add-mappings=true # if default mappings should be added
# JACKSON ({sc-spring-boot-autoconfigure}}/jackson/JacksonProperties.{sc-ext}[JacksonProperties])
spring.jackson.date-format= # Date format string (e.g. yyyy-MM-dd HH:mm:ss), or a fully-qualified date format class name (e.g. com.fasterxml.jackson.databind.util.ISO8601DateFormat)
spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy (e.g. CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES) or the fully-qualified class name of a PropertyNamingStrategy subclass
spring.jackson.deserialization.*= # see Jackson's DeserializationFeature
spring.jackson.generator.*= # see Jackson's JsonGenerator.Feature
spring.jackson.mapper.*= # see Jackson's MapperFeature