Isolate Jackson2ObjectMapperBuilder mutation

Previously, Jackson2ObjectMapperBuilder was a singleton bean. This
meant that if it was injected and mutated in one injection point,
usage in a subsequent injection point would see the previous
injection point's mutation which can lead to unexpected failures.

This commit updates the auto-configuration of the builder to make it
a protoype bean. Mutation of the builder that is intended to apply
globally should be made using a customizer.

Closes gh-17477
This commit is contained in:
Andy Wilkinson 2019-07-19 13:57:55 +01:00
parent c7d2799f4e
commit ea1dc85d50
2 changed files with 33 additions and 0 deletions

View File

@ -54,6 +54,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.context.annotation.Scope;
import org.springframework.core.Ordered;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.Assert;
@ -168,6 +169,7 @@ public class JacksonAutoConfiguration {
static class JacksonObjectMapperBuilderConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {

View File

@ -391,6 +391,16 @@ class JacksonAutoConfigurationTests {
});
}
@Test
void builderIsNotSharedAcrossMultipleInjectionPoints() {
this.contextRunner.withUserConfiguration(ObjectMapperBuilderConsumerConfig.class).run((context) -> {
ObjectMapperBuilderConsumerConfig consumer = context.getBean(ObjectMapperBuilderConsumerConfig.class);
assertThat(consumer.builderOne).isNotNull();
assertThat(consumer.builderTwo).isNotNull();
assertThat(consumer.builderOne).isNotSameAs(consumer.builderTwo);
});
}
private void assertParameterNamesModuleCreatorBinding(Mode expectedMode, Class<?>... configClasses) {
this.contextRunner.withUserConfiguration(configClasses).run((context) -> {
DeserializationConfig deserializationConfig = context.getBean(ObjectMapper.class)
@ -479,6 +489,27 @@ class JacksonAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class ObjectMapperBuilderConsumerConfig {
Jackson2ObjectMapperBuilder builderOne;
Jackson2ObjectMapperBuilder builderTwo;
@Bean
String consumerOne(Jackson2ObjectMapperBuilder builder) {
this.builderOne = builder;
return "one";
}
@Bean
String consumerTwo(Jackson2ObjectMapperBuilder builder) {
this.builderTwo = builder;
return "two";
}
}
protected static final class Foo {
private String name;