From 315213ea4e2fb3b4dd42c056b0a0da5f05d9f01a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 3 Oct 2014 10:37:02 +0200 Subject: [PATCH] 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 --- spring-boot-autoconfigure/pom.xml | 11 ++ .../jackson/JacksonAutoConfiguration.java | 152 ++++++------------ .../web/HttpMessageConverters.java | 6 +- ...ttpMessageConvertersAutoConfiguration.java | 25 +++ .../JacksonAutoConfigurationTests.java | 29 +++- ...ssageConvertersAutoConfigurationTests.java | 41 ++++- .../web/HttpMessageConvertersTests.java | 8 +- spring-boot-dependencies/pom.xml | 5 + spring-boot-docs/src/main/asciidoc/howto.adoc | 71 +++++--- spring-boot-docs/src/main/asciidoc/index.adoc | 2 +- .../main/asciidoc/spring-boot-features.adoc | 3 +- 11 files changed, 216 insertions(+), 137 deletions(-) diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index d880067a3d3..07634036b62 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -40,6 +40,11 @@ jackson-databind true + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + true + com.fasterxml.jackson.datatype jackson-datatype-joda @@ -104,6 +109,12 @@ org.apache.solr solr-solrj true + + + org.codehaus.woodstox + wstx-asl + + org.apache.tomcat.embed diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index bc86a82c676..6301babf85d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -20,6 +20,7 @@ import java.lang.reflect.Field; 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; @@ -29,41 +30,33 @@ import org.springframework.beans.factory.BeanFactoryUtils; 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: * * * @author Oliver Gierke * @author Andy Wilkinson + * @author Sebastien Deleuze * @author Marcel Overdijk * @since 1.1.0 */ @@ -88,10 +81,23 @@ public class JacksonAutoConfiguration { } @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(); @@ -99,29 +105,39 @@ public class JacksonAutoConfiguration { 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 features) { + for (Entry 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 @@ -130,9 +146,8 @@ public class JacksonAutoConfiguration { 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 @@ -141,9 +156,8 @@ public class JacksonAutoConfiguration { 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); @@ -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 // 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 entry : this.jacksonProperties - .getDeserialization().entrySet()) { - objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); - } - } - - private void configureSerializationFeatures(ObjectMapper objectMapper) { - for (Entry entry : this.jacksonProperties - .getSerialization().entrySet()) { - objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); - } - } - - private void configureMapperFeatures(ObjectMapper objectMapper) { - for (Entry entry : this.jacksonProperties.getMapper() - .entrySet()) { - objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); - } - } - - private void configureParserFeatures(ObjectMapper objectMapper) { - for (Entry entry : this.jacksonProperties - .getParser().entrySet()) { - objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); - } - } - - private void configureGeneratorFeatures(ObjectMapper objectMapper) { - for (Entry entry : this.jacksonProperties - .getGenerator().entrySet()) { - objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); - } - } - - private boolean isFeatureEnabled(Entry 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(); - } - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java index 4b0290e1b1d..db752ddd0db 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConverters.java @@ -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. @@ -25,6 +25,7 @@ import java.util.List; 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; @@ -141,7 +142,8 @@ public class HttpMessageConverters implements Iterable> for (Iterator> 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(); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfiguration.java index 68b5b910d7c..23827a2a4fb 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfiguration.java @@ -29,9 +29,12 @@ import org.springframework.context.annotation.Bean; 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; /** @@ -42,6 +45,7 @@ import com.google.gson.Gson; * @author Piotr Maj * @author Oliver Gierke * @author David Liu + * @author Sebastien Deleuze * @author Andy Wilkinson */ @Configuration @@ -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 @ConditionalOnClass(Gson.class) @ConditionalOnBean(Gson.class) diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 4b5ba52888f..af34adacaf2 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -19,7 +19,6 @@ 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; @@ -33,6 +32,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext 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 com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -45,11 +45,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; 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.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -63,6 +60,7 @@ import static org.mockito.Mockito.verify; * * @author Dave Syer * @author Oliver Gierke + * @author Sebastien Deleuze * @author Andy Wilkinson * @author Marcel Overdijk */ @@ -86,9 +84,6 @@ public class JacksonAutoConfigurationTests { public void registersJodaModuleAutomatically() { this.context.register(JacksonAutoConfiguration.class); this.context.refresh(); - Map 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); assertThat(objectMapper.canSerialize(LocalDateTime.class), is(true)); } @@ -339,6 +334,26 @@ public class JacksonAutoConfigurationTests { .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 protected static class ModulesConfig { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java index 1c4368101cc..99cddea47f2 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersAutoConfigurationTests.java @@ -25,7 +25,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.google.gson.Gson; @@ -40,6 +42,7 @@ import static org.junit.Assert.assertTrue; * @author Oliver Gierke * @author David Liu * @author Andy Wilkinson + * @author Sebastien Deleuze */ public class HttpMessageConvertersAutoConfigurationTests { @@ -59,11 +62,13 @@ public class HttpMessageConvertersAutoConfigurationTests { assertTrue(this.context.getBeansOfType(ObjectMapper.class).isEmpty()); assertTrue(this.context.getBeansOfType(MappingJackson2HttpMessageConverter.class) .isEmpty()); + assertTrue(this.context.getBeansOfType( + MappingJackson2XmlHttpMessageConverter.class).isEmpty()); } @Test public void defaultJacksonConverter() throws Exception { - this.context.register(JacksonConfig.class, + this.context.register(JacksonObjectMapperConfig.class, HttpMessageConvertersAutoConfiguration.class); this.context.refresh(); @@ -73,9 +78,25 @@ public class HttpMessageConvertersAutoConfigurationTests { 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 public void customJacksonConverter() throws Exception { - this.context.register(JacksonConfig.class, JacksonConverterConfig.class, + this.context.register(JacksonObjectMapperConfig.class, + JacksonConverterConfig.class, HttpMessageConvertersAutoConfiguration.class); this.context.refresh(); @@ -128,13 +149,27 @@ public class HttpMessageConvertersAutoConfigurationTests { } @Configuration - protected static class JacksonConfig { + protected static class JacksonObjectMapperConfig { @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } } + @Configuration + protected static class JacksonObjectMapperBuilderConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean + public Jackson2ObjectMapperBuilder builder() { + return new Jackson2ObjectMapperBuilder(); + } + } + @Configuration protected static class JacksonConverterConfig { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java index 6363679f1be..f5fa9b517c7 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/HttpMessageConvertersTests.java @@ -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. @@ -30,7 +30,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 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 static org.hamcrest.Matchers.equalTo; @@ -63,7 +63,7 @@ public class HttpMessageConvertersTests { ResourceHttpMessageConverter.class, SourceHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class, - Jaxb2RootElementHttpMessageConverter.class))); + MappingJackson2XmlHttpMessageConverter.class))); } @Test @@ -106,7 +106,7 @@ public class HttpMessageConvertersTests { List> converters) { for (Iterator> iterator = converters.iterator(); iterator .hasNext();) { - if (iterator.next() instanceof Jaxb2RootElementHttpMessageConverter) { + if (iterator.next() instanceof MappingJackson2XmlHttpMessageConverter) { iterator.remove(); } } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index c976e95db3d..05b56297087 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -440,6 +440,11 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + com.fasterxml.jackson.datatype jackson-datatype-joda diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 890008f3116..0c4a58da468 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -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) 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 -a JAXB object) because browsers tend to send accept headers that prefer XML. +Sometimes in a browser you might see XML responses because browsers tend to send accept +headers that prefer XML. [[howto-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 -`MyThing` was annotated as `@XmlRootElement`: +If you have the Jackson XML extension (`jackson-dataformat-xml`) on the classpath, it will +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"] +---- + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + +---- + +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"] +---- + + org.codehaus.woodstox + woodstox-core-asl + +---- + +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"] ---- @@ -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]] === Customize the Jackson ObjectMapper 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 -converter with a vanilla `ObjectMapper`. Spring Boot has some features to make it easier -to customize this behavior. +conversion in an HTTP exchange. If Jackson is on the classpath you already get the +default converter(s) provided by `Jackson2ObjectMapperBuilder`. -You can configure the vanilla `ObjectMapper` 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: +The `ObjectMapper` (or `XmlMapper` for Jackson XML converter) instance created by default +have the following customized properties: + +* 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 @@ -698,14 +730,17 @@ properties in the environment: |`spring.jackson.serialization.=true\|false` |=== -For example, to allow deserialization to continue when an unknown property is encountered -during deserialization, set `spring.jackson.deserialization.fail_on_unknown_properties=false`. -Note that, thanks to the use of <>, -the case of `fail_on_unknown_properties` doesn't have to match the case of the corresponding -enum constant which is `FAIL_ON_UNKNOWN_PROPERTIES`. +For example, to enable pretty print, set `spring.jackson.serialization.indent_output=true`. +Note that, thanks to the use of <>, the case of `indent_output` doesn't have to match the case of the +corresponding enum constant which is `INDENT_OUTPUT`. + +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 -and mark it as `@Primary`. +Defining a `@Bean` of type `Jackson2ObjectMapperBuilder` will allow you to customize both +default `ObjectMapper` and `XmlMapper` (used in `MappingJackson2HttpMessageConverter` and +`MappingJackson2XmlHttpMessageConverter` respectively). 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 diff --git a/spring-boot-docs/src/main/asciidoc/index.adoc b/spring-boot-docs/src/main/asciidoc/index.adoc index e3386f7dad0..8055def48ed 100644 --- a/spring-boot-docs/src/main/asciidoc/index.adoc +++ b/spring-boot-docs/src/main/asciidoc/index.adoc @@ -1,5 +1,5 @@ = 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 :toc: :toclevels: 4 diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index a6de6df80aa..21984975e06 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -885,7 +885,8 @@ formatters, view controllers etc.) you can add your own `@Bean` of type ==== HttpMessageConverters 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 -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 `HttpMessageConverters` class: