From 456d741706af5952cf32731e2ba3cb4a73083c3e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 17 Aug 2021 13:34:12 +0200 Subject: [PATCH] Add support for configuring Jackson's ConstructorDetector Closes gh-27178 --- .../jackson/JacksonAutoConfiguration.java | 24 ++++++++ .../jackson/JacksonProperties.java | 39 +++++++++++++ ...itional-spring-configuration-metadata.json | 4 ++ .../Jackson211AutoConfigurationTests.java | 21 +++++++ .../JacksonAutoConfigurationTests.java | 58 +++++++++++++++++++ 5 files changed, 146 insertions(+) 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 a1318c4519..a28279c420 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 @@ -34,6 +34,7 @@ import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import org.springframework.beans.BeanUtils; @@ -41,6 +42,7 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jackson.JacksonProperties.ConstructorDetectorStrategy; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.context.ApplicationContext; @@ -188,6 +190,7 @@ public class JacksonAutoConfiguration { configureModules(builder); configureLocale(builder); configureDefaultLeniency(builder); + configureConstructorDetector(builder); } private void configureFeatures(Jackson2ObjectMapperBuilder builder, Map features) { @@ -297,6 +300,27 @@ public class JacksonAutoConfiguration { } } + private void configureConstructorDetector(Jackson2ObjectMapperBuilder builder) { + ConstructorDetectorStrategy strategy = this.jacksonProperties.getConstructorDetector(); + if (strategy != null) { + builder.postConfigurer((objectMapper) -> { + switch (strategy) { + case USE_PROPERTIES_BASED: + objectMapper.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED); + break; + case USE_DELEGATING: + objectMapper.setConstructorDetector(ConstructorDetector.USE_DELEGATING); + break; + case EXPLICIT_ONLY: + objectMapper.setConstructorDetector(ConstructorDetector.EXPLICIT_ONLY); + break; + default: + objectMapper.setConstructorDetector(ConstructorDetector.DEFAULT); + } + }); + } + } + private static Collection getBeans(ListableBeanFactory beanFactory, Class type) { return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type).values(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index fa9e17e912..8983072faf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -97,6 +97,12 @@ public class JacksonProperties { */ private Boolean defaultLeniency; + /** + * Strategy to use to to auto-detect constructor, and in particular behavior with + * single-argument constructors. + */ + private ConstructorDetectorStrategy constructorDetector; + /** * Time zone used when formatting dates. For instance, "America/Los_Angeles" or * "GMT+10". @@ -164,6 +170,14 @@ public class JacksonProperties { this.defaultLeniency = defaultLeniency; } + public ConstructorDetectorStrategy getConstructorDetector() { + return this.constructorDetector; + } + + public void setConstructorDetector(ConstructorDetectorStrategy constructorDetector) { + this.constructorDetector = constructorDetector; + } + public TimeZone getTimeZone() { return this.timeZone; } @@ -180,4 +194,29 @@ public class JacksonProperties { this.locale = locale; } + public enum ConstructorDetectorStrategy { + + /** + * Use heuristics to see if "properties" mode is to be used. + */ + DEFAULT, + + /** + * Assume "properties" mode if not explicitly annotated otherwise. + */ + USE_PROPERTIES_BASED, + + /** + * Assume "delegating" mode if not explicitly annotated otherwise. + */ + USE_DELEGATING, + + /** + * Refuse to decide implicit mode and instead throw an InvalidDefinitionException + * for ambiguous cases. + */ + EXPLICIT_ONLY; + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 2f90edada6..5226554718 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1048,6 +1048,10 @@ "name": "spring.integration.jdbc.initialize-schema", "defaultValue": "embedded" }, + { + "name": "spring.jackson.constructor-detector", + "defaultValue": "default" + }, { "name": "spring.jackson.joda-date-time-format", "type": "java.lang.String", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java index 7e6a9fd9dc..109601afc5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java @@ -54,4 +54,25 @@ public class Jackson211AutoConfigurationTests extends JacksonAutoConfigurationTe }); } + // ConstructorDetector only available as of Jackson 2.12 + @Override + void constructorDetectorWithNoStrategyUseDefault() { + } + + @Override + void constructorDetectorWithDefaultStrategy() { + } + + @Override + void constructorDetectorWithUsePropertiesBasedStrategy() { + } + + @Override + void constructorDetectorWithUseDelegatingStrategy() { + } + + @Override + void constructorDetectorWithExplicitOnlyStrategy() { + } + } 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 3d2856d576..555ca12f70 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 @@ -41,6 +41,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector.SingleArgConstructor; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.util.StdDateFormat; @@ -323,6 +325,62 @@ class JacksonAutoConfigurationTests { }); } + @Test + void constructorDetectorWithNoStrategyUseDefault() { + this.contextRunner.run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector(); + assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC); + assertThat(cd.requireCtorAnnotation()).isFalse(); + assertThat(cd.allowJDKTypeConstructors()).isFalse(); + }); + } + + @Test + void constructorDetectorWithDefaultStrategy() { + this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=default").run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector(); + assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC); + assertThat(cd.requireCtorAnnotation()).isFalse(); + assertThat(cd.allowJDKTypeConstructors()).isFalse(); + }); + } + + @Test + void constructorDetectorWithUsePropertiesBasedStrategy() { + this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-properties-based") + .run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector(); + assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.PROPERTIES); + assertThat(cd.requireCtorAnnotation()).isFalse(); + assertThat(cd.allowJDKTypeConstructors()).isFalse(); + }); + } + + @Test + void constructorDetectorWithUseDelegatingStrategy() { + this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-delegating").run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector(); + assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.DELEGATING); + assertThat(cd.requireCtorAnnotation()).isFalse(); + assertThat(cd.allowJDKTypeConstructors()).isFalse(); + }); + } + + @Test + void constructorDetectorWithExplicitOnlyStrategy() { + this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=explicit-only").run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector(); + assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.REQUIRE_MODE); + assertThat(cd.requireCtorAnnotation()).isFalse(); + assertThat(cd.allowJDKTypeConstructors()).isFalse(); + }); + } + @Test void additionalJacksonBuilderCustomization() { this.contextRunner.withUserConfiguration(ObjectMapperBuilderCustomConfig.class).run((context) -> {