From 6f98c63ac03299f7ee873b3b21f7591ea9aff0ea Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 6 Jun 2014 12:17:06 +0200 Subject: [PATCH] Add auto-configuration for Jackson's JodaTime and JSR-310 modules We now register the Jackson JodaTime module with Jackson ObjectMappers if it is on the classpath. We also register the JSR-310 module if it's on the classpath and the application is running Java 8 or better. Extracted the Jackson specific configuration previously residing in HttpMessageConvertersAutoConfiguration into a JacksonAutoConfiguration class. Added the Jackson JSR-310 module as a managed Boot dependency. --- spring-boot-autoconfigure/pom.xml | 5 + .../jackson/JacksonAutoConfiguration.java | 117 ++++++++++++ ...ttpMessageConvertersAutoConfiguration.java | 43 +---- .../JacksonAutoConfigurationTests.java | 179 ++++++++++++++++++ ...ssageConvertersAutoConfigurationTests.java | 125 +----------- spring-boot-dependencies/pom.xml | 5 + 6 files changed, 316 insertions(+), 158 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index b7fc565c62d..16d865e228e 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -36,6 +36,11 @@ jackson-datatype-joda true + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + true + org.flywaydb flyway-core 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 new file mode 100644 index 00000000000..61e47ec5335 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -0,0 +1,117 @@ +/* + * 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. + * 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 java.util.Collection; + +import javax.annotation.PostConstruct; + +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 com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +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 + * @since 1.1.0 + */ +@Configuration +@ConditionalOnClass(ObjectMapper.class) +@EnableConfigurationProperties(HttpMapperProperties.class) +public class JacksonAutoConfiguration { + + @Autowired + private HttpMapperProperties properties = new HttpMapperProperties(); + + @Autowired + private ListableBeanFactory beanFactory; + + @Bean + @ConditionalOnMissingBean + @Primary + public ObjectMapper jacksonObjectMapper() { + + ObjectMapper objectMapper = new ObjectMapper(); + + if (this.properties.isJsonSortKeys()) { + objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + } + + return objectMapper; + } + + @PostConstruct + public void init() { + + Collection mappers = BeanFactoryUtils + .beansOfTypeIncludingAncestors(this.beanFactory, ObjectMapper.class) + .values(); + Collection modules = BeanFactoryUtils.beansOfTypeIncludingAncestors( + this.beanFactory, Module.class).values(); + + for (ObjectMapper mapper : mappers) { + mapper.registerModules(modules); + } + } + + @Configuration + @ConditionalOnClass(JodaModule.class) + static class JodaModuleAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + JodaModule jacksonJodaModule() { + return new JodaModule(); + } + } + + @Configuration + @ConditionalOnJava(JavaVersion.EIGHT) + @ConditionalOnClass(JSR310Module.class) + static class Jsr310ModuleAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + JSR310Module jacksonJsr310Module() { + return new JSR310Module(); + } + } +} 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 d6b56f4860d..5c64e7e1f9d 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 @@ -17,38 +17,34 @@ package org.springframework.boot.autoconfigure.web; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; -import javax.annotation.PostConstruct; - -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 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.context.annotation.Import; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link HttpMessageConverter}s. - * + * * @author Dave Syer * @author Christian Dupuis * @author Piotr Maj + * @author Oliver Gierke */ @Configuration @ConditionalOnClass(HttpMessageConverter.class) +@Import(JacksonAutoConfiguration.class) public class HttpMessageConvertersAutoConfiguration { @Autowired(required = false) @@ -70,33 +66,6 @@ public class HttpMessageConvertersAutoConfiguration { @Autowired private HttpMapperProperties properties = new HttpMapperProperties(); - @Autowired - private ListableBeanFactory beanFactory; - - @PostConstruct - public void init() { - Collection mappers = BeanFactoryUtils - .beansOfTypeIncludingAncestors(this.beanFactory, ObjectMapper.class) - .values(); - Collection modules = BeanFactoryUtils.beansOfTypeIncludingAncestors( - this.beanFactory, Module.class).values(); - for (ObjectMapper mapper : mappers) { - mapper.registerModules(modules); - } - } - - @Bean - @ConditionalOnMissingBean - @Primary - public ObjectMapper jacksonObjectMapper() { - ObjectMapper objectMapper = new ObjectMapper(); - if (this.properties.isJsonSortKeys()) { - objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, - true); - } - return objectMapper; - } - @Bean @ConditionalOnMissingBean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( @@ -106,7 +75,5 @@ public class HttpMessageConvertersAutoConfiguration { converter.setPrettyPrint(this.properties.isJsonPrettyPrint()); return converter; } - } - } 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 new file mode 100644 index 00000000000..455a5c46a1c --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -0,0 +1,179 @@ +/* + * 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. + * 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 java.io.IOException; +import java.util.Collection; + +import org.hamcrest.Matchers; +import org.joda.time.LocalDateTime; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +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 com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +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.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JacksonAutoConfiguration}. + * + * @author Dave Syer + * @author Oliver Gierke + */ +public class JacksonAutoConfigurationTests { + + AnnotationConfigApplicationContext context; + + @Before + public void setUp() { + this.context = new AnnotationConfigApplicationContext(); + } + + @After + public void tearDown() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void registersJodaModuleAutomatically() { + + this.context.register(JacksonAutoConfiguration.class); + this.context.refresh(); + + Collection modules = this.context.getBeansOfType(Module.class).values(); + assertThat(modules, is(Matchers. iterableWithSize(1))); + assertThat(modules.iterator().next(), is(instanceOf(JodaModule.class))); + + ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class); + assertThat(objectMapper.canSerialize(LocalDateTime.class), is(true)); + } + + @Test + public void customJacksonModules() throws Exception { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(ModulesConfig.class, JacksonAutoConfiguration.class); + this.context.refresh(); + + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + + @SuppressWarnings({ "unchecked", "unused" }) + ObjectMapper result = verify(mapper).registerModules( + (Iterable) argThat(hasItem(this.context.getBean("jacksonModule", + Module.class)))); + } + + @Test + public void doubleModuleRegistration() throws Exception { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(DoubleModulesConfig.class, + HttpMessageConvertersAutoConfiguration.class); + this.context.refresh(); + + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo())); + } + + @Configuration + protected static class ModulesConfig { + + @Bean + public Module jacksonModule() { + return new SimpleModule(); + } + + @Bean + @Primary + public ObjectMapper objectMapper() { + return Mockito.mock(ObjectMapper.class); + } + } + + @Configuration + protected static class DoubleModulesConfig { + + @Bean + public Module jacksonModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Foo.class, new JsonSerializer() { + + @Override + public void serialize(Foo value, JsonGenerator jgen, + SerializerProvider provider) throws IOException, + JsonProcessingException { + jgen.writeStartObject(); + jgen.writeStringField("foo", "bar"); + jgen.writeEndObject(); + } + }); + return module; + } + + @Bean + @Primary + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(jacksonModule()); + return mapper; + } + } + + protected static class Foo { + + private String name; + + private Foo() { + + } + + static Foo create() { + return new Foo(); + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } +} 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 b050baf773e..7679216d1a0 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 @@ -16,38 +16,23 @@ package org.springframework.boot.autoconfigure.web; -import java.io.IOException; - -import org.joda.time.LocalDateTime; import org.junit.After; import org.junit.Test; -import org.mockito.Mockito; 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.MappingJackson2HttpMessageConverter; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.verify; /** * Tests for {@link HttpMessageConvertersAutoConfiguration}. - * + * * @author Dave Syer + * @author Oliver Gierke */ public class HttpMessageConvertersAutoConfigurationTests { @@ -62,52 +47,22 @@ public class HttpMessageConvertersAutoConfigurationTests { @Test public void customJacksonConverter() throws Exception { + this.context = new AnnotationConfigApplicationContext(); this.context.register(JacksonConfig.class, HttpMessageConvertersAutoConfiguration.class); this.context.refresh(); + MappingJackson2HttpMessageConverter converter = this.context .getBean(MappingJackson2HttpMessageConverter.class); assertEquals(this.context.getBean(ObjectMapper.class), converter.getObjectMapper()); + HttpMessageConverters converters = this.context .getBean(HttpMessageConverters.class); assertTrue(converters.getConverters().contains(converter)); } - @Test - public void defaultJacksonModules() throws Exception { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(HttpMessageConvertersAutoConfiguration.class); - this.context.refresh(); - ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class); - assertThat(objectMapper.canSerialize(LocalDateTime.class), equalTo(true)); - } - - @Test - public void customJacksonModules() throws Exception { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(ModulesConfig.class, - HttpMessageConvertersAutoConfiguration.class); - this.context.refresh(); - ObjectMapper mapper = this.context.getBean(ObjectMapper.class); - - @SuppressWarnings({ "unchecked", "unused" }) - ObjectMapper result = verify(mapper).registerModules( - (Iterable) argThat(hasItem(this.context.getBean("jacksonModule", - Module.class)))); - } - - @Test - public void doubleModuleRegistration() throws Exception { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(DoubleModulesConfig.class, - HttpMessageConvertersAutoConfiguration.class); - this.context.refresh(); - ObjectMapper mapper = this.context.getBean(ObjectMapper.class); - assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo())); - } - @Configuration protected static class JacksonConfig { @@ -122,75 +77,5 @@ public class HttpMessageConvertersAutoConfigurationTests { public ObjectMapper objectMapper() { return new ObjectMapper(); } - } - - @Configuration - protected static class ModulesConfig { - - @Bean - public Module jacksonModule() { - return new SimpleModule(); - } - - @Bean - @Primary - public ObjectMapper objectMapper() { - return Mockito.mock(ObjectMapper.class); - } - - } - - @Configuration - protected static class DoubleModulesConfig { - - @Bean - public Module jacksonModule() { - SimpleModule module = new SimpleModule(); - module.addSerializer(Foo.class, new JsonSerializer() { - - @Override - public void serialize(Foo value, JsonGenerator jgen, - SerializerProvider provider) throws IOException, - JsonProcessingException { - jgen.writeStartObject(); - jgen.writeStringField("foo", "bar"); - jgen.writeEndObject(); - } - }); - return module; - } - - @Bean - @Primary - public ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(jacksonModule()); - return mapper; - } - - } - - protected static class Foo { - - private String name; - - private Foo() { - - } - - static Foo create() { - return new Foo(); - } - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - } - } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index d9871684ba9..a19ec3870ab 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -370,6 +370,11 @@ jackson-datatype-joda ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + com.gemstone.gemfire gemfire