Skip to content

Commit

Permalink
Support Jackson based XML serialization and Jackson2ObjectMapperBuilder
Browse files Browse the repository at this point in the history
This commit introduces support for Jackson based XML serialization, using the
new MappingJackson2XmlHttpMessageConverter provided by Spring Framework
4.1. It is automatically activated when Jackson XML extension is detected on the
classpath.

Jackson2ObjectMapperBuilder is now used to create ObjectMapper and XmlMapper
instances with the following customized properties:
 - MapperFeature.DEFAULT_VIEW_INCLUSION is disabled
 - DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES is disabled

JodaModuleAutoConfiguration and Jsr310ModuleAutoConfiguration have been removed
since their behaviors are now handled directly by the ObjectMapper builder.

In addition to the existing @bean of type ObjectMapper support, it is now
possible to customize Jackson based serialization properties by declaring
a @bean of type Jackson2ObjectMapperBuilder.

Fixes gh-1237
Fixes gh-1580
Fixes gh-1644
  • Loading branch information
sdeleuze authored and wilkinsona committed Oct 6, 2014
1 parent 0773b89 commit 315213e
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 137 deletions.
11 changes: 11 additions & 0 deletions spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
Expand Down Expand Up @@ -104,6 +109,12 @@
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>wstx-asl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.PostConstruct;
Expand All @@ -29,41 +30,33 @@
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnJava;
import org.springframework.boot.autoconfigure.condition.ConditionalOnJava.JavaVersion;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.HttpMapperProperties;
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.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
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;

/**
* Auto configuration for Jackson. The following auto-configuration will get applied:
* <ul>
* <li>an {@link ObjectMapper} in case none is already configured.</li>
* <li>the {@link JodaModule} registered if it's on the classpath.</li>
* <li>the {@link JSR310Module} registered if it's on the classpath and the application is
* running on Java 8 or better.</li>
* <li>a {@link Jackson2ObjectMapperBuilder} in case none is already configured.</li>
* <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans
* (including the defaulted ones).</li>
* </ul>
*
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Sebastien Deleuze
* @author Marcel Overdijk
* @since 1.1.0
*/
Expand All @@ -88,40 +81,63 @@ private <T> Collection<T> getBeans(Class<T> type) {
}

@Configuration
@ConditionalOnClass(ObjectMapper.class)
@EnableConfigurationProperties({ HttpMapperProperties.class, JacksonProperties.class })
@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
static class JacksonObjectMapperAutoConfiguration {

@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}

}

@Configuration
@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
@EnableConfigurationProperties({ HttpMapperProperties.class, JacksonProperties.class })
static class JacksonObjectMapperBuilderAutoConfiguration {

@Autowired
private HttpMapperProperties httpMapperProperties = new HttpMapperProperties();

@Autowired
private JacksonProperties jacksonProperties = new JacksonProperties();

@Bean
@Primary
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
@ConditionalOnMissingBean(Jackson2ObjectMapperBuilder.class)
public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();

if (this.httpMapperProperties.isJsonSortKeys()) {
objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS,
true);
builder.featuresToEnable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
}

configureDeserializationFeatures(objectMapper);
configureSerializationFeatures(objectMapper);
configureMapperFeatures(objectMapper);
configureParserFeatures(objectMapper);
configureGeneratorFeatures(objectMapper);
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());

configureDateFormat(builder);
configurePropertyNamingStrategy(builder);

configureDateFormat(objectMapper);
configurePropertyNamingStrategy(objectMapper);
return builder;
}

return objectMapper;
private void configureFeatures(Jackson2ObjectMapperBuilder builder,
Map<?, Boolean> features) {
for (Entry<?, Boolean> entry : features.entrySet()) {
if (entry.getValue() != null && entry.getValue()) {
builder.featuresToEnable(entry.getKey());
}
else {
builder.featuresToDisable(entry.getKey());
}
}
}

private void configurePropertyNamingStrategy(ObjectMapper objectMapper) {
private void configurePropertyNamingStrategy(Jackson2ObjectMapperBuilder builder) {
// 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
Expand All @@ -130,9 +146,8 @@ private void configurePropertyNamingStrategy(ObjectMapper objectMapper) {
if (propertyNamingStrategy != null) {
try {
Class<?> clazz = ClassUtils.forName(propertyNamingStrategy, null);
objectMapper
.setPropertyNamingStrategy((PropertyNamingStrategy) BeanUtils
.instantiateClass(clazz));
builder.propertyNamingStrategy((PropertyNamingStrategy) BeanUtils
.instantiateClass(clazz));
}
catch (ClassNotFoundException e) {
// Find the field (this way we automatically support new constants
Expand All @@ -141,9 +156,8 @@ private void configurePropertyNamingStrategy(ObjectMapper objectMapper) {
propertyNamingStrategy, PropertyNamingStrategy.class);
if (field != null) {
try {
objectMapper
.setPropertyNamingStrategy((PropertyNamingStrategy) field
.get(null));
builder.propertyNamingStrategy((PropertyNamingStrategy) field
.get(null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
Expand All @@ -158,85 +172,21 @@ private void configurePropertyNamingStrategy(ObjectMapper objectMapper) {
}
}

private void configureDateFormat(ObjectMapper objectMapper) {
private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {
// 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));
builder.dateFormat((DateFormat) BeanUtils.instantiateClass(clazz));
}
catch (ClassNotFoundException e) {
objectMapper.setDateFormat(new SimpleDateFormat(dateFormat));
builder.dateFormat(new SimpleDateFormat(dateFormat));
}
}
}

private void configureDeserializationFeatures(ObjectMapper objectMapper) {
for (Entry<DeserializationFeature, Boolean> entry : this.jacksonProperties
.getDeserialization().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}

private void configureSerializationFeatures(ObjectMapper objectMapper) {
for (Entry<SerializationFeature, Boolean> entry : this.jacksonProperties
.getSerialization().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}

private void configureMapperFeatures(ObjectMapper objectMapper) {
for (Entry<MapperFeature, Boolean> entry : this.jacksonProperties.getMapper()
.entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}

private void configureParserFeatures(ObjectMapper objectMapper) {
for (Entry<JsonParser.Feature, Boolean> entry : this.jacksonProperties
.getParser().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}

private void configureGeneratorFeatures(ObjectMapper objectMapper) {
for (Entry<JsonGenerator.Feature, Boolean> entry : this.jacksonProperties
.getGenerator().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}

private boolean isFeatureEnabled(Entry<?, Boolean> entry) {
return entry.getValue() != null && entry.getValue();
}
}

@Configuration
@ConditionalOnClass(JodaModule.class)
static class JodaModuleAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public JodaModule jacksonJodaModule() {
return new JodaModule();
}

}

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
@ConditionalOnClass(JSR310Module.class)
static class Jsr310ModuleAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public JSR310Module jacksonJsr310Module() {
return new JSR310Module();
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,7 @@

import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
Expand Down Expand Up @@ -141,7 +142,8 @@ private void reorderXmlConvertersToEnd(List<HttpMessageConverter<?>> converters)
for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator
.hasNext();) {
HttpMessageConverter<?> converter = iterator.next();
if (converter instanceof AbstractXmlHttpMessageConverter) {
if ((converter instanceof AbstractXmlHttpMessageConverter)
|| (converter instanceof MappingJackson2XmlHttpMessageConverter)) {
xml.add(converter);
iterator.remove();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.gson.Gson;

/**
Expand All @@ -42,6 +45,7 @@
* @author Piotr Maj
* @author Oliver Gierke
* @author David Liu
* @author Sebastien Deleuze
* @author Andy Wilkinson
*/
@Configuration
Expand Down Expand Up @@ -78,6 +82,27 @@ public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(

}

@Configuration
@ConditionalOnClass(XmlMapper.class)
@ConditionalOnBean(Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(HttpMapperProperties.class)
protected static class XmlMappers {

@Autowired
private HttpMapperProperties properties = new HttpMapperProperties();

@Bean
@ConditionalOnMissingBean
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder builder) {
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
converter.setObjectMapper(builder.createXmlMapper(true).build());
converter.setPrettyPrint(this.properties.isJsonPrettyPrint());
return converter;
}

}

@Configuration
@ConditionalOnClass(Gson.class)
@ConditionalOnBean(Gson.class)
Expand Down
Loading

0 comments on commit 315213e

Please sign in to comment.