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.
This commit is contained in:
Oliver Gierke 2014-06-06 12:17:06 +02:00 committed by Andy Wilkinson
parent 30bef1e95e
commit 6f98c63ac0
6 changed files with 316 additions and 158 deletions

View File

@ -36,6 +36,11 @@
<artifactId>jackson-datatype-joda</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>

View File

@ -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:
* <ul>
* <li>an {@link ObjectMapper} in case none is already configured.</li>
* <li>the {@link JodaModule} registered if it's on the classpath.</li>
* <li>the {@link JSR310Module} registered if it's on the classpath and the application is
* running on Java 8 or better.</li>
* <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans
* (including the defaulted ones).</li>
* </ul>
*
* @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<ObjectMapper> mappers = BeanFactoryUtils
.beansOfTypeIncludingAncestors(this.beanFactory, ObjectMapper.class)
.values();
Collection<Module> 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();
}
}
}

View File

@ -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<ObjectMapper> mappers = BeanFactoryUtils
.beansOfTypeIncludingAncestors(this.beanFactory, ObjectMapper.class)
.values();
Collection<Module> 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;
}
}
}

View File

@ -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<Module> modules = this.context.getBeansOfType(Module.class).values();
assertThat(modules, is(Matchers.<Module> 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<Module>) 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<Foo>() {
@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;
}
}
}

View File

@ -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<Module>) 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<Foo>() {
@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;
}
}
}

View File

@ -370,6 +370,11 @@
<artifactId>jackson-datatype-joda</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.gemstone.gemfire</groupId>
<artifactId>gemfire</artifactId>