Register Module beans with Jackson2ObjectMapperBuilder

Prior to this commit, Module beans were registered with all
ObjectMapper beans, but were not registered with the auto-configured
Jackson2ObjectMapperBuilder. This meant that any ObjectMapper created
with the builder but not exposed as a bean would not have the Module
beans registered with it. One such ObjectMapper is the one used by the
auto-configured MappingJackson2XmlHttpMessageConverter. This caused
XML (de)serialization to be different to JSON (de)serialization.

This commit updates JacksonAutoConfiguration to register all of the
application context's Module beans with the auto-configured
Jackson2ObjectMapperBuilder. This ensures consistent configuration
of any ObjectMapper that's created using the builder, irrespective of
whether or not that ObjectMapper is also exposed as a bean, and
also ensures that (de)serialization of JSON and XML is consistent.

See gh-2327
This commit is contained in:
Andy Wilkinson 2015-02-12 13:42:32 +00:00
parent 0899ffdadf
commit f11bcb9495
2 changed files with 54 additions and 13 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@ -73,15 +73,15 @@ public class JacksonAutoConfiguration {
@PostConstruct
private void registerModulesWithObjectMappers() {
Collection<Module> modules = getBeans(Module.class);
for (ObjectMapper objectMapper : getBeans(ObjectMapper.class)) {
Collection<Module> modules = getBeans(this.beanFactory, Module.class);
for (ObjectMapper objectMapper : getBeans(this.beanFactory, ObjectMapper.class)) {
objectMapper.registerModules(modules);
}
}
private <T> Collection<T> getBeans(Class<T> type) {
return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type)
.values();
private static <T> Collection<T> getBeans(ListableBeanFactory beanFactory,
Class<T> type) {
return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type).values();
}
@Configuration
@ -131,6 +131,7 @@ public class JacksonAutoConfiguration {
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
return builder;
}
@ -200,6 +201,12 @@ public class JacksonAutoConfiguration {
}
}
private void configureModules(Jackson2ObjectMapperBuilder builder) {
Collection<Module> moduleBeans = getBeans(this.applicationContext,
Module.class);
builder.modulesToInstall(moduleBeans.toArray(new Module[moduleBeans.size()]));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;

View File

@ -19,6 +19,8 @@ package org.springframework.boot.autoconfigure.jackson;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
@ -36,6 +38,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
@ -90,7 +93,8 @@ public class JacksonAutoConfigurationTests {
@Test
public void customJacksonModules() throws Exception {
this.context.register(ModulesConfig.class, JacksonAutoConfiguration.class);
this.context.register(ModuleConfig.class, MockObjectMapperConfig.class,
JacksonAutoConfiguration.class);
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
@SuppressWarnings({ "unchecked", "unused" })
@ -376,13 +380,19 @@ public class JacksonAutoConfigurationTests {
SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS));
}
@Configuration
protected static class ModulesConfig {
@Test
public void moduleBeansAndWellKnownModulesAreRegisteredWithTheObjectMapperBuilder() {
this.context.register(ModuleConfig.class, JacksonAutoConfiguration.class);
this.context.refresh();
ObjectMapper objectMapper = this.context.getBean(
Jackson2ObjectMapperBuilder.class).build();
assertThat(this.context.getBean(CustomModule.class).getOwners(),
hasItem((ObjectCodec) objectMapper));
assertThat(objectMapper.canSerialize(LocalDateTime.class), is(true));
}
@Bean
public Module jacksonModule() {
return new SimpleModule();
}
@Configuration
protected static class MockObjectMapperConfig {
@Bean
@Primary
@ -392,6 +402,15 @@ public class JacksonAutoConfigurationTests {
}
@Configuration
protected static class ModuleConfig {
@Bean
public CustomModule jacksonModule() {
return new CustomModule();
}
}
@Configuration
protected static class DoubleModulesConfig {
@ -455,4 +474,19 @@ public class JacksonAutoConfigurationTests {
this.propertyName = propertyName;
}
}
private static class CustomModule extends SimpleModule {
private Set<ObjectCodec> owners = new HashSet<ObjectCodec>();
@Override
public void setupModule(SetupContext context) {
this.owners.add(context.getOwner());
}
Set<ObjectCodec> getOwners() {
return this.owners;
}
}
}