Commit 315213ea authored by Sebastien Deleuze's avatar Sebastien Deleuze Committed by Andy Wilkinson

Support Jackson based XML serialization and Jackson2ObjectMapperBuilder

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
parent 0773b89b
...@@ -40,6 +40,11 @@ ...@@ -40,6 +40,11 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId> <artifactId>jackson-datatype-joda</artifactId>
...@@ -104,6 +109,12 @@ ...@@ -104,6 +109,12 @@
<groupId>org.apache.solr</groupId> <groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId> <artifactId>solr-solrj</artifactId>
<optional>true</optional> <optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>wstx-asl</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.tomcat.embed</groupId> <groupId>org.apache.tomcat.embed</groupId>
......
...@@ -20,6 +20,7 @@ import java.lang.reflect.Field; ...@@ -20,6 +20,7 @@ import java.lang.reflect.Field;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
...@@ -29,41 +30,33 @@ import org.springframework.beans.factory.BeanFactoryUtils; ...@@ -29,41 +30,33 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.HttpMapperProperties; import org.springframework.boot.autoconfigure.web.HttpMapperProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils; 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.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature; 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: * Auto configuration for Jackson. The following auto-configuration will get applied:
* <ul> * <ul>
* <li>an {@link ObjectMapper} in case none is already configured.</li> * <li>an {@link ObjectMapper} in case none is already configured.</li>
* <li>the {@link JodaModule} registered if it's on the classpath.</li> * <li>a {@link Jackson2ObjectMapperBuilder} in case none is already configured.</li>
* <li>the {@link JSR310Module} registered if it's on the classpath and the application is
* running on Java 8 or better.</li>
* <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans * <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans
* (including the defaulted ones).</li> * (including the defaulted ones).</li>
* </ul> * </ul>
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Sebastien Deleuze
* @author Marcel Overdijk * @author Marcel Overdijk
* @since 1.1.0 * @since 1.1.0
*/ */
...@@ -88,10 +81,23 @@ public class JacksonAutoConfiguration { ...@@ -88,10 +81,23 @@ public class JacksonAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnClass(ObjectMapper.class) @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
@EnableConfigurationProperties({ HttpMapperProperties.class, JacksonProperties.class })
static class JacksonObjectMapperAutoConfiguration { 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 @Autowired
private HttpMapperProperties httpMapperProperties = new HttpMapperProperties(); private HttpMapperProperties httpMapperProperties = new HttpMapperProperties();
...@@ -99,29 +105,39 @@ public class JacksonAutoConfiguration { ...@@ -99,29 +105,39 @@ public class JacksonAutoConfiguration {
private JacksonProperties jacksonProperties = new JacksonProperties(); private JacksonProperties jacksonProperties = new JacksonProperties();
@Bean @Bean
@Primary @ConditionalOnMissingBean(Jackson2ObjectMapperBuilder.class)
@ConditionalOnMissingBean public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder() {
public ObjectMapper jacksonObjectMapper() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
ObjectMapper objectMapper = new ObjectMapper();
if (this.httpMapperProperties.isJsonSortKeys()) { if (this.httpMapperProperties.isJsonSortKeys()) {
objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, builder.featuresToEnable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
true);
} }
configureDeserializationFeatures(objectMapper); configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureSerializationFeatures(objectMapper); configureFeatures(builder, this.jacksonProperties.getSerialization());
configureMapperFeatures(objectMapper); configureFeatures(builder, this.jacksonProperties.getMapper());
configureParserFeatures(objectMapper); configureFeatures(builder, this.jacksonProperties.getParser());
configureGeneratorFeatures(objectMapper); configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureDateFormat(objectMapper); return builder;
configurePropertyNamingStrategy(objectMapper); }
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 // We support a fully qualified class name extending Jackson's
// PropertyNamingStrategy or a string value corresponding to the constant // PropertyNamingStrategy or a string value corresponding to the constant
// names in PropertyNamingStrategy which hold default provided implementations // names in PropertyNamingStrategy which hold default provided implementations
...@@ -130,9 +146,8 @@ public class JacksonAutoConfiguration { ...@@ -130,9 +146,8 @@ public class JacksonAutoConfiguration {
if (propertyNamingStrategy != null) { if (propertyNamingStrategy != null) {
try { try {
Class<?> clazz = ClassUtils.forName(propertyNamingStrategy, null); Class<?> clazz = ClassUtils.forName(propertyNamingStrategy, null);
objectMapper builder.propertyNamingStrategy((PropertyNamingStrategy) BeanUtils
.setPropertyNamingStrategy((PropertyNamingStrategy) BeanUtils .instantiateClass(clazz));
.instantiateClass(clazz));
} }
catch (ClassNotFoundException e) { catch (ClassNotFoundException e) {
// Find the field (this way we automatically support new constants // Find the field (this way we automatically support new constants
...@@ -141,9 +156,8 @@ public class JacksonAutoConfiguration { ...@@ -141,9 +156,8 @@ public class JacksonAutoConfiguration {
propertyNamingStrategy, PropertyNamingStrategy.class); propertyNamingStrategy, PropertyNamingStrategy.class);
if (field != null) { if (field != null) {
try { try {
objectMapper builder.propertyNamingStrategy((PropertyNamingStrategy) field
.setPropertyNamingStrategy((PropertyNamingStrategy) field .get(null));
.get(null));
} }
catch (Exception ex) { catch (Exception ex) {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
...@@ -158,85 +172,21 @@ public class JacksonAutoConfiguration { ...@@ -158,85 +172,21 @@ public class JacksonAutoConfiguration {
} }
} }
private void configureDateFormat(ObjectMapper objectMapper) { private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {
// We support a fully qualified class name extending DateFormat or a date // We support a fully qualified class name extending DateFormat or a date
// pattern string value // pattern string value
String dateFormat = this.jacksonProperties.getDateFormat(); String dateFormat = this.jacksonProperties.getDateFormat();
if (dateFormat != null) { if (dateFormat != null) {
try { try {
Class<?> clazz = ClassUtils.forName(dateFormat, null); Class<?> clazz = ClassUtils.forName(dateFormat, null);
objectMapper.setDateFormat((DateFormat) BeanUtils builder.dateFormat((DateFormat) BeanUtils.instantiateClass(clazz));
.instantiateClass(clazz));
} }
catch (ClassNotFoundException e) { 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();
}
} }
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -25,6 +25,7 @@ import java.util.List; ...@@ -25,6 +25,7 @@ import java.util.List;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter; import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
...@@ -141,7 +142,8 @@ public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> ...@@ -141,7 +142,8 @@ public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>>
for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator
.hasNext();) { .hasNext();) {
HttpMessageConverter<?> converter = iterator.next(); HttpMessageConverter<?> converter = iterator.next();
if (converter instanceof AbstractXmlHttpMessageConverter) { if ((converter instanceof AbstractXmlHttpMessageConverter)
|| (converter instanceof MappingJackson2XmlHttpMessageConverter)) {
xml.add(converter); xml.add(converter);
iterator.remove(); iterator.remove();
} }
......
...@@ -29,9 +29,12 @@ import org.springframework.context.annotation.Bean; ...@@ -29,9 +29,12 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter; 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.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.gson.Gson; import com.google.gson.Gson;
/** /**
...@@ -42,6 +45,7 @@ import com.google.gson.Gson; ...@@ -42,6 +45,7 @@ import com.google.gson.Gson;
* @author Piotr Maj * @author Piotr Maj
* @author Oliver Gierke * @author Oliver Gierke
* @author David Liu * @author David Liu
* @author Sebastien Deleuze
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
@Configuration @Configuration
...@@ -78,6 +82,27 @@ public class HttpMessageConvertersAutoConfiguration { ...@@ -78,6 +82,27 @@ public class HttpMessageConvertersAutoConfiguration {
} }
@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 @Configuration
@ConditionalOnClass(Gson.class) @ConditionalOnClass(Gson.class)
@ConditionalOnBean(Gson.class) @ConditionalOnBean(Gson.class)
......
...@@ -19,7 +19,6 @@ package org.springframework.boot.autoconfigure.jackson; ...@@ -19,7 +19,6 @@ package org.springframework.boot.autoconfigure.jackson;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Map;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.LocalDateTime; import org.joda.time.LocalDateTime;
...@@ -33,6 +32,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext ...@@ -33,6 +32,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
...@@ -45,11 +45,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; ...@@ -45,11 +45,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
...@@ -63,6 +60,7 @@ import static org.mockito.Mockito.verify; ...@@ -63,6 +60,7 @@ import static org.mockito.Mockito.verify;
* *
* @author Dave Syer * @author Dave Syer
* @author Oliver Gierke * @author Oliver Gierke
* @author Sebastien Deleuze
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Marcel Overdijk * @author Marcel Overdijk
*/ */
...@@ -86,9 +84,6 @@ public class JacksonAutoConfigurationTests { ...@@ -86,9 +84,6 @@ public class JacksonAutoConfigurationTests {
public void registersJodaModuleAutomatically() { public void registersJodaModuleAutomatically() {
this.context.register(JacksonAutoConfiguration.class); this.context.register(JacksonAutoConfiguration.class);
this.context.refresh(); this.context.refresh();
Map<String, Module> modules = this.context.getBeansOfType(Module.class);
assertThat(modules.size(), greaterThanOrEqualTo(1)); // Depends on the JDK
assertThat(modules.get("jacksonJodaModule"), is(instanceOf(JodaModule.class)));
ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class); ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class);
assertThat(objectMapper.canSerialize(LocalDateTime.class), is(true)); assertThat(objectMapper.canSerialize(LocalDateTime.class), is(true));
} }
...@@ -339,6 +334,26 @@ public class JacksonAutoConfigurationTests { ...@@ -339,6 +334,26 @@ public class JacksonAutoConfigurationTests {
.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET)); .isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET));
} }
@Test
public void defaultObjectMapperBuilder() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
this.context.refresh();
Jackson2ObjectMapperBuilder builder = this.context
.getBean(Jackson2ObjectMapperBuilder.class);
ObjectMapper mapper = builder.build();
assertTrue(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault());
assertFalse(mapper.getDeserializationConfig().isEnabled(
MapperFeature.DEFAULT_VIEW_INCLUSION));
assertTrue(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault());
assertFalse(mapper.getDeserializationConfig().isEnabled(
MapperFeature.DEFAULT_VIEW_INCLUSION));
assertFalse(mapper.getSerializationConfig().isEnabled(
MapperFeature.DEFAULT_VIEW_INCLUSION));
assertTrue(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault());
assertFalse(mapper.getDeserializationConfig().isEnabled(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES));
}
@Configuration @Configuration
protected static class ModulesConfig { protected static class ModulesConfig {
......
...@@ -25,7 +25,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext ...@@ -25,7 +25,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.GsonHttpMessageConverter; 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.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson; import com.google.gson.Gson;
...@@ -40,6 +42,7 @@ import static org.junit.Assert.assertTrue; ...@@ -40,6 +42,7 @@ import static org.junit.Assert.assertTrue;
* @author Oliver Gierke * @author Oliver Gierke
* @author David Liu * @author David Liu
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Sebastien Deleuze
*/ */
public class HttpMessageConvertersAutoConfigurationTests { public class HttpMessageConvertersAutoConfigurationTests {
...@@ -59,11 +62,13 @@ public class HttpMessageConvertersAutoConfigurationTests { ...@@ -59,11 +62,13 @@ public class HttpMessageConvertersAutoConfigurationTests {
assertTrue(this.context.getBeansOfType(ObjectMapper.class).isEmpty()); assertTrue(this.context.getBeansOfType(ObjectMapper.class).isEmpty());
assertTrue(this.context.getBeansOfType(MappingJackson2HttpMessageConverter.class) assertTrue(this.context.getBeansOfType(MappingJackson2HttpMessageConverter.class)
.isEmpty()); .isEmpty());
assertTrue(this.context.getBeansOfType(
MappingJackson2XmlHttpMessageConverter.class).isEmpty());
} }
@Test @Test
public void defaultJacksonConverter() throws Exception { public void defaultJacksonConverter() throws Exception {
this.context.register(JacksonConfig.class, this.context.register(JacksonObjectMapperConfig.class,
HttpMessageConvertersAutoConfiguration.class); HttpMessageConvertersAutoConfiguration.class);
this.context.refresh(); this.context.refresh();
...@@ -73,9 +78,25 @@ public class HttpMessageConvertersAutoConfigurationTests { ...@@ -73,9 +78,25 @@ public class HttpMessageConvertersAutoConfigurationTests {
assertConverterBeanRegisteredWithHttpMessageConverters(MappingJackson2HttpMessageConverter.class); assertConverterBeanRegisteredWithHttpMessageConverters(MappingJackson2HttpMessageConverter.class);
} }
@Test
public void defaultJacksonConvertersWithBuilder() throws Exception {
this.context.register(JacksonObjectMapperBuilderConfig.class,
HttpMessageConvertersAutoConfiguration.class);
this.context.refresh();
assertConverterBeanExists(MappingJackson2HttpMessageConverter.class,
"mappingJackson2HttpMessageConverter");
assertConverterBeanExists(MappingJackson2XmlHttpMessageConverter.class,
"mappingJackson2XmlHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(MappingJackson2HttpMessageConverter.class);
assertConverterBeanRegisteredWithHttpMessageConverters(MappingJackson2XmlHttpMessageConverter.class);
}
@Test @Test
public void customJacksonConverter() throws Exception { public void customJacksonConverter() throws Exception {
this.context.register(JacksonConfig.class, JacksonConverterConfig.class, this.context.register(JacksonObjectMapperConfig.class,
JacksonConverterConfig.class,
HttpMessageConvertersAutoConfiguration.class); HttpMessageConvertersAutoConfiguration.class);
this.context.refresh(); this.context.refresh();
...@@ -128,13 +149,27 @@ public class HttpMessageConvertersAutoConfigurationTests { ...@@ -128,13 +149,27 @@ public class HttpMessageConvertersAutoConfigurationTests {
} }
@Configuration @Configuration
protected static class JacksonConfig { protected static class JacksonObjectMapperConfig {
@Bean @Bean
public ObjectMapper objectMapper() { public ObjectMapper objectMapper() {
return new ObjectMapper(); return new ObjectMapper();
} }
} }
@Configuration
protected static class JacksonObjectMapperBuilderConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public Jackson2ObjectMapperBuilder builder() {
return new Jackson2ObjectMapperBuilder();
}
}
@Configuration @Configuration
protected static class JacksonConverterConfig { protected static class JacksonConverterConfig {
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -30,7 +30,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter; ...@@ -30,7 +30,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
...@@ -63,7 +63,7 @@ public class HttpMessageConvertersTests { ...@@ -63,7 +63,7 @@ public class HttpMessageConvertersTests {
ResourceHttpMessageConverter.class, SourceHttpMessageConverter.class, ResourceHttpMessageConverter.class, SourceHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class,
MappingJackson2HttpMessageConverter.class, MappingJackson2HttpMessageConverter.class,
Jaxb2RootElementHttpMessageConverter.class))); MappingJackson2XmlHttpMessageConverter.class)));
} }
@Test @Test
...@@ -106,7 +106,7 @@ public class HttpMessageConvertersTests { ...@@ -106,7 +106,7 @@ public class HttpMessageConvertersTests {
List<HttpMessageConverter<?>> converters) { List<HttpMessageConverter<?>> converters) {
for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator
.hasNext();) { .hasNext();) {
if (iterator.next() instanceof Jaxb2RootElementHttpMessageConverter) { if (iterator.next() instanceof MappingJackson2XmlHttpMessageConverter) {
iterator.remove(); iterator.remove();
} }
} }
......
...@@ -440,6 +440,11 @@ ...@@ -440,6 +440,11 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version> <version>${jackson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId> <artifactId>jackson-datatype-joda</artifactId>
......
...@@ -643,15 +643,40 @@ default as long as Jackson2 is on the classpath. For example: ...@@ -643,15 +643,40 @@ default as long as Jackson2 is on the classpath. For example:
As long as `MyThing` can be serialized by Jackson2 (e.g. a normal POJO or Groovy object) As long as `MyThing` can be serialized by Jackson2 (e.g. a normal POJO or Groovy object)
then `http://localhost:8080/thing` will serve a JSON representation of it by default. then `http://localhost:8080/thing` will serve a JSON representation of it by default.
Sometimes in a browser you might see XML responses (but by default only if `MyThing` was Sometimes in a browser you might see XML responses because browsers tend to send accept
a JAXB object) because browsers tend to send accept headers that prefer XML. headers that prefer XML.
[[howto-write-an-xml-rest-service]] [[howto-write-an-xml-rest-service]]
=== Write an XML REST service === Write an XML REST service
Since JAXB is in the JDK the same example as we used for JSON would work, as long as the If you have the Jackson XML extension (`jackson-dataformat-xml`) on the classpath, it will
`MyThing` was annotated as `@XmlRootElement`: be used to render XML responses and the very same example as we used for JSON would work.
To use it, add the following dependency to your project:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
----
You may also want to add a dependency on Woodstox. It's faster than the default Stax
implementation provided by the JDK and also adds pretty print support and improved
namespace handling:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
</dependency>
----
If Jackson's XML extension is not available, JAXB (provided by default in the JDK) will
be used, with the additional requirement to have `MyThing` annotated as
`@XmlRootElement`:
[source,java,indent=0,subs="verbatim,quotes,attributes"] [source,java,indent=0,subs="verbatim,quotes,attributes"]
---- ----
...@@ -670,14 +695,21 @@ To get the server to render XML instead of JSON you might have to send an ...@@ -670,14 +695,21 @@ To get the server to render XML instead of JSON you might have to send an
[[howto-customize-the-jackson-objectmapper]] [[howto-customize-the-jackson-objectmapper]]
=== Customize the Jackson ObjectMapper === Customize the Jackson ObjectMapper
Spring MVC (client and server side) uses `HttpMessageConverters` to negotiate content Spring MVC (client and server side) uses `HttpMessageConverters` to negotiate content
conversion in an HTTP exchange. If Jackson is on the classpath you already get a default conversion in an HTTP exchange. If Jackson is on the classpath you already get the
converter with a vanilla `ObjectMapper`. Spring Boot has some features to make it easier default converter(s) provided by `Jackson2ObjectMapperBuilder`.
to customize this behavior.
You can configure the vanilla `ObjectMapper` using the environment. Jackson provides an The `ObjectMapper` (or `XmlMapper` for Jackson XML converter) instance created by default
extensive suite of simple on/off features that can be used to configure various aspects have the following customized properties:
of its processing. These features are described in five enums in Jackson which map onto
properties in the environment: * MapperFeature.DEFAULT_VIEW_INCLUSION is disabled
* DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES is disabled
Spring Boot has also some features to make it easier to customize this behavior.
You can configure the `ObjectMapper` and `XmlMapper` instances using the environment.
Jackson provides an extensive suite of simple on/off features that can be used to
configure various aspects of its processing. These features are described in five enums in
Jackson which map onto properties in the environment:
|=== |===
|Jackson enum|Environment property |Jackson enum|Environment property
...@@ -698,14 +730,17 @@ properties in the environment: ...@@ -698,14 +730,17 @@ properties in the environment:
|`spring.jackson.serialization.<feature_name>=true\|false` |`spring.jackson.serialization.<feature_name>=true\|false`
|=== |===
For example, to allow deserialization to continue when an unknown property is encountered For example, to enable pretty print, set `spring.jackson.serialization.indent_output=true`.
during deserialization, set `spring.jackson.deserialization.fail_on_unknown_properties=false`. Note that, thanks to the use of <<boot-features-external-config-relaxed-binding,
Note that, thanks to the use of <<boot-features-external-config-relaxed-binding, relaxed binding>>, relaxed binding>>, the case of `indent_output` doesn't have to match the case of the
the case of `fail_on_unknown_properties` doesn't have to match the case of the corresponding corresponding enum constant which is `INDENT_OUTPUT`.
enum constant which is `FAIL_ON_UNKNOWN_PROPERTIES`.
If you want to replace the default `ObjectMapper` completely, define a `@Bean` of that
type and mark it as `@Primary`.
If you want to replace the default `ObjectMapper` completely, define a `@Bean` of that type Defining a `@Bean` of type `Jackson2ObjectMapperBuilder` will allow you to customize both
and mark it as `@Primary`. default `ObjectMapper` and `XmlMapper` (used in `MappingJackson2HttpMessageConverter` and
`MappingJackson2XmlHttpMessageConverter` respectively).
Another way to customize Jackson is to add beans of type Another way to customize Jackson is to add beans of type
`com.fasterxml.jackson.databind.Module` to your context. They will be registered with every `com.fasterxml.jackson.databind.Module` to your context. They will be registered with every
......
= Spring Boot Reference Guide = Spring Boot Reference Guide
Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson; Marcel Overdijk; Christian Dupuis; Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson; Marcel Overdijk; Christian Dupuis; Sébastien Deleuze
:doctype: book :doctype: book
:toc: :toc:
:toclevels: 4 :toclevels: 4
......
...@@ -885,7 +885,8 @@ formatters, view controllers etc.) you can add your own `@Bean` of type ...@@ -885,7 +885,8 @@ formatters, view controllers etc.) you can add your own `@Bean` of type
==== HttpMessageConverters ==== HttpMessageConverters
Spring MVC uses the `HttpMessageConverter` interface to convert HTTP requests and Spring MVC uses the `HttpMessageConverter` interface to convert HTTP requests and
responses. Sensible defaults are included out of the box, for example Objects can be responses. Sensible defaults are included out of the box, for example Objects can be
automatically converted to JSON (using the Jackson library) or XML (using JAXB). automatically converted to JSON (using the Jackson library) or XML (using the Jackson
XML extension if available, else using JAXB).
If you need to add or customize converters you can use Spring Boot's If you need to add or customize converters you can use Spring Boot's
`HttpMessageConverters` class: `HttpMessageConverters` class:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment