diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/Jackson2ObjectMapperBuilderCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/Jackson2ObjectMapperBuilderCustomizer.java new file mode 100644 index 0000000000..59b6a451f4 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/Jackson2ObjectMapperBuilderCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2016 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link ObjectMapper} via {@link Jackson2ObjectMapperBuilder} retaining its default + * auto-configuration. + * + * @author Grzegorz Poznachowski + * @since 1.4.0 + */ +public interface Jackson2ObjectMapperBuilderCustomizer { + + /** + * Customize the jacksonObjectMapperBuilder. + * @param jacksonObjectMapperBuilder the jacksonObjectMapperBuilder to customize + */ + void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder); +} 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 a58987c8f7..f8eef994d3 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.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -51,6 +52,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -160,137 +162,189 @@ public class JacksonAutoConfiguration { @Configuration @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) - @EnableConfigurationProperties(JacksonProperties.class) static class JacksonObjectMapperBuilderConfiguration { private final ApplicationContext applicationContext; - private final JacksonProperties jacksonProperties; - JacksonObjectMapperBuilderConfiguration(ApplicationContext applicationContext, - JacksonProperties jacksonProperties) { + JacksonProperties jacksonProperties, + List customizers) { this.applicationContext = applicationContext; - this.jacksonProperties = jacksonProperties; } @Bean @ConditionalOnMissingBean(Jackson2ObjectMapperBuilder.class) - public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder() { + public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder( + List customizers) { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); builder.applicationContext(this.applicationContext); - if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { - builder.serializationInclusion( - this.jacksonProperties.getDefaultPropertyInclusion()); - } - if (this.jacksonProperties.getTimeZone() != null) { - builder.timeZone(this.jacksonProperties.getTimeZone()); - } - 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); - configureModules(builder); - configureLocale(builder); + customize(builder, customizers); return builder; } - 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 customize(Jackson2ObjectMapperBuilder builder, + List customizers) { + for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) { + customizer.customize(builder); } } - 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 dateFormatClass = ClassUtils.forName(dateFormat, null); - builder.dateFormat( - (DateFormat) BeanUtils.instantiateClass(dateFormatClass)); + } + + @Configuration + @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) + @EnableConfigurationProperties(JacksonProperties.class) + static class Jackson2ObjectMapperBuilderCustomizerConfiguration { + + @Bean + public StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer( + ApplicationContext applicationContext, + JacksonProperties jacksonProperties) { + return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, + jacksonProperties); + } + + private static final class StandardJackson2ObjectMapperBuilderCustomizer + implements Jackson2ObjectMapperBuilderCustomizer, Ordered { + + private final ApplicationContext applicationContext; + + private final JacksonProperties jacksonProperties; + + StandardJackson2ObjectMapperBuilderCustomizer( + ApplicationContext applicationContext, + JacksonProperties jacksonProperties) { + this.applicationContext = applicationContext; + this.jacksonProperties = jacksonProperties; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(Jackson2ObjectMapperBuilder builder) { + + if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { + builder.serializationInclusion( + this.jacksonProperties.getDefaultPropertyInclusion()); } - catch (ClassNotFoundException ex) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat); - // Since Jackson 2.6.3 we always need to set a TimeZone (see gh-4170) - // If none in our properties fallback to the Jackson's default - TimeZone timeZone = this.jacksonProperties.getTimeZone(); - if (timeZone == null) { - timeZone = new ObjectMapper().getSerializationConfig() - .getTimeZone(); + if (this.jacksonProperties.getTimeZone() != null) { + builder.timeZone(this.jacksonProperties.getTimeZone()); + } + 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); + configureModules(builder); + configureLocale(builder); + } + + 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()); } - simpleDateFormat.setTimeZone(timeZone); - builder.dateFormat(simpleDateFormat); } } - } - 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 - String strategy = this.jacksonProperties.getPropertyNamingStrategy(); - if (strategy != null) { - try { - configurePropertyNamingStrategyClass(builder, - ClassUtils.forName(strategy, null)); + 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 dateFormatClass = ClassUtils.forName(dateFormat, null); + builder.dateFormat( + (DateFormat) BeanUtils.instantiateClass(dateFormatClass)); + } + catch (ClassNotFoundException ex) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat( + dateFormat); + // Since Jackson 2.6.3 we always need to set a TimeZone (see + // gh-4170). If none in our properties fallback to the Jackson's + // default + TimeZone timeZone = this.jacksonProperties.getTimeZone(); + if (timeZone == null) { + timeZone = new ObjectMapper().getSerializationConfig() + .getTimeZone(); + } + simpleDateFormat.setTimeZone(timeZone); + builder.dateFormat(simpleDateFormat); + } } - catch (ClassNotFoundException ex) { - configurePropertyNamingStrategyField(builder, strategy); + } + + 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 + String strategy = this.jacksonProperties.getPropertyNamingStrategy(); + if (strategy != null) { + try { + configurePropertyNamingStrategyClass(builder, + ClassUtils.forName(strategy, null)); + } + catch (ClassNotFoundException ex) { + configurePropertyNamingStrategyField(builder, strategy); + } } } - } - private void configurePropertyNamingStrategyClass( - Jackson2ObjectMapperBuilder builder, - Class propertyNamingStrategyClass) { - builder.propertyNamingStrategy((PropertyNamingStrategy) BeanUtils - .instantiateClass(propertyNamingStrategyClass)); - } + private void configurePropertyNamingStrategyClass( + Jackson2ObjectMapperBuilder builder, + Class propertyNamingStrategyClass) { + builder.propertyNamingStrategy((PropertyNamingStrategy) BeanUtils + .instantiateClass(propertyNamingStrategyClass)); + } - private void configurePropertyNamingStrategyField( - Jackson2ObjectMapperBuilder builder, String fieldName) { - // 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, - fieldName, PropertyNamingStrategy.class); - Assert.notNull(field, "Constant named '" + fieldName + "' not found on " - + PropertyNamingStrategy.class.getName()); - try { - builder.propertyNamingStrategy((PropertyNamingStrategy) field.get(null)); + private void configurePropertyNamingStrategyField( + Jackson2ObjectMapperBuilder builder, String fieldName) { + // 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, + fieldName, PropertyNamingStrategy.class); + Assert.notNull(field, "Constant named '" + fieldName + "' not found on " + + PropertyNamingStrategy.class.getName()); + try { + builder.propertyNamingStrategy( + (PropertyNamingStrategy) field.get(null)); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } } - catch (Exception ex) { - throw new IllegalStateException(ex); + + private void configureModules(Jackson2ObjectMapperBuilder builder) { + Collection moduleBeans = getBeans(this.applicationContext, + Module.class); + builder.modulesToInstall( + moduleBeans.toArray(new Module[moduleBeans.size()])); } - } - private void configureModules(Jackson2ObjectMapperBuilder builder) { - Collection moduleBeans = getBeans(this.applicationContext, - Module.class); - builder.modulesToInstall(moduleBeans.toArray(new Module[moduleBeans.size()])); - } + private void configureLocale(Jackson2ObjectMapperBuilder builder) { + Locale locale = this.jacksonProperties.getLocale(); + if (locale != null) { + builder.locale(locale); + } + } - private void configureLocale(Jackson2ObjectMapperBuilder builder) { - Locale locale = this.jacksonProperties.getLocale(); - if (locale != null) { - builder.locale(locale); + private static Collection getBeans(ListableBeanFactory beanFactory, + Class type) { + return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type) + .values(); } - } - private static Collection getBeans(ListableBeanFactory beanFactory, - Class type) { - return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type) - .values(); } } 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 5e016c8c24..9dc026d804 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 @@ -72,6 +72,7 @@ import static org.mockito.Mockito.mock; * @author Marcel Overdijk * @author Sebastien Deleuze * @author Johannes Edmeier + * @author Grzegorz Poznachowski */ public class JacksonAutoConfigurationTests { @@ -419,6 +420,15 @@ public class JacksonAutoConfigurationTests { .isEqualTo("\"Koordinierte Universalzeit\""); } + @Test + public void additionalJacksonBuilderCustomization() throws Exception { + this.context.register(JacksonAutoConfiguration.class, + ObjectMapperBuilderCustomConfig.class); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class); + } + @Test public void parameterNamesModuleIsAutoConfigured() { assertParameterNamesModuleCreatorBinding(Mode.DEFAULT, @@ -510,6 +520,22 @@ public class JacksonAutoConfigurationTests { } + @Configuration + protected static class ObjectMapperBuilderCustomConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer customDateFormat() { + return new Jackson2ObjectMapperBuilderCustomizer() { + @Override + public void customize( + Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) { + jackson2ObjectMapperBuilder.dateFormat(new MyDateFormat()); + } + }; + } + + } + protected static final class Foo { private String name;