From 993fbb3a0dc42603d4844c47b983374d6bd11edd Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Tue, 22 Oct 2024 23:54:56 +0300 Subject: [PATCH 1/2] Retain existing modules in JacksonAutoConfiguration Previously, the default Jackson2ObjectMapperBuilderCustomizer implementation did set the list of modules to use. This had the effect of removing any modules that were registered programmatically by a customizer with higher precedence. This commit uses the variant of modulesToInstall that retain any existing modules. It also adds a note in the documentation as this behavior can be easily missed. See gh-42836 --- .../jackson/JacksonAutoConfiguration.java | 2 +- .../JacksonAutoConfigurationTests.java | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index e388f4c3e5..3bc23da13d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -307,7 +307,7 @@ public class JacksonAutoConfiguration { } private void configureModules(Jackson2ObjectMapperBuilder builder) { - builder.modulesToInstall(this.modules.toArray(new Module[0])); + builder.modulesToInstall((modules) -> modules.addAll(this.modules)); } private void configureLocale(Jackson2ObjectMapperBuilder builder) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 29dde38a97..06a937bc7b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -73,6 +73,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -324,6 +326,16 @@ class JacksonAutoConfigurationTests { }); } + @Test + void customModulesRegisteredByBuilderCustomizerWithHighestPrecedenceShouldBeRetained() { + this.contextRunner.withUserConfiguration(ModuleConfig.class, CustomModuleBuilderCustomizerConfig.class) + .run((context) -> { + ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build(); + assertThat(context.getBean(CustomModule.class).getOwners()).contains(objectMapper); + assertThat(objectMapper.getRegisteredModuleIds()).contains("customizer-module"); + }); + } + @Test void defaultSerializationInclusion() { this.contextRunner.run((context) -> { @@ -592,6 +604,17 @@ class JacksonAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomModuleBuilderCustomizerConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + Jackson2ObjectMapperBuilderCustomizer customModuleCustomizer() { + return (builder) -> builder.modulesToInstall(new SimpleModule("customizer-module")); + } + + } + @Configuration(proxyBeanMethods = false) static class ObjectMapperBuilderConsumerConfig { From e6af48fc201060847f52445ae32869398c9bd70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 18 Nov 2024 15:37:22 +0100 Subject: [PATCH 2/2] Polish "Retain existing modules in JacksonAutoConfiguration" See gh-42836 --- .../jackson/JacksonAutoConfigurationTests.java | 18 ++++++++++++------ .../modules/how-to/pages/spring-mvc.adoc | 2 ++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 06a937bc7b..7ff485f6bd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -73,7 +73,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; -import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -327,12 +326,13 @@ class JacksonAutoConfigurationTests { } @Test - void customModulesRegisteredByBuilderCustomizerWithHighestPrecedenceShouldBeRetained() { + void customModulesRegisteredByBuilderCustomizerShouldBeRetained() { this.contextRunner.withUserConfiguration(ModuleConfig.class, CustomModuleBuilderCustomizerConfig.class) .run((context) -> { ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build(); assertThat(context.getBean(CustomModule.class).getOwners()).contains(objectMapper); - assertThat(objectMapper.getRegisteredModuleIds()).contains("customizer-module"); + assertThat(objectMapper.getRegisteredModuleIds()).contains("module-A", "module-B", + CustomModule.class.getName()); }); } @@ -608,9 +608,15 @@ class JacksonAutoConfigurationTests { static class CustomModuleBuilderCustomizerConfig { @Bean - @Order(Ordered.HIGHEST_PRECEDENCE) - Jackson2ObjectMapperBuilderCustomizer customModuleCustomizer() { - return (builder) -> builder.modulesToInstall(new SimpleModule("customizer-module")); + @Order(-1) + Jackson2ObjectMapperBuilderCustomizer highPrecedenceCustomizer() { + return (builder) -> builder.modulesToInstall((modules) -> modules.add(new SimpleModule("module-A"))); + } + + @Bean + @Order(1) + Jackson2ObjectMapperBuilderCustomizer lowPrecedenceCustomizer() { + return (builder) -> builder.modulesToInstall((modules) -> modules.add(new SimpleModule("module-B"))); } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index bdd2de2d89..fdeac720b7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -118,6 +118,8 @@ Such customizer beans can be ordered (Boot's own customizer has an order of 0), Any beans of type javadoc:com.fasterxml.jackson.databind.Module[] are automatically registered with the auto-configured javadoc:org.springframework.http.converter.json.Jackson2ObjectMapperBuilder[] and are applied to any javadoc:com.fasterxml.jackson.databind.ObjectMapper[] instances that it creates. This provides a global mechanism for contributing custom modules when you add new features to your application. +NOTE: If you wish to register additional modules programmatically using a javadoc:org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer[], make sure to use the `modulesToInstall` method that takes a consumer as the other variants are not additive. + If you want to replace the default javadoc:com.fasterxml.jackson.databind.ObjectMapper[] completely, either define a javadoc:org.springframework.context.annotation.Bean[format=annotation] of that type or, if you prefer the builder-based approach, define a javadoc:org.springframework.http.converter.json.Jackson2ObjectMapperBuilder[] javadoc:org.springframework.context.annotation.Bean[format=annotation]. When defining an javadoc:com.fasterxml.jackson.databind.ObjectMapper[] bean, marking it as javadoc:org.springframework.context.annotation.Primary[format=annotation] is recommended as the auto-configuration's javadoc:com.fasterxml.jackson.databind.ObjectMapper[] that it will replace is javadoc:org.springframework.context.annotation.Primary[format=annotation]. Note that, in either case, doing so disables all auto-configuration of the javadoc:com.fasterxml.jackson.databind.ObjectMapper[].