From 918e59b81781bc021fc27c79a03f329ade53ba9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Feb 2026 11:31:17 +0000 Subject: [PATCH] Add customizer and property support for configuring Jackson factories Closes gh-34709 --- .../modules/how-to/pages/spring-mvc.adoc | 2 + .../CborFactoryBuilderCustomizer.java | 39 ++++ .../JacksonAutoConfiguration.java | 194 ++++++++++++++++-- .../autoconfigure/JacksonProperties.java | 135 ++++++++++++ .../JsonFactoryBuilderCustomizer.java | 39 ++++ .../XmlFactoryBuilderCustomizer.java | 39 ++++ .../JacksonAutoConfigurationTests.java | 117 ++++++++++- 7 files changed, 542 insertions(+), 23 deletions(-) create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java create mode 100644 module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 3f12e6491e1..82a99e7cce9 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -108,6 +108,8 @@ To ease the migration when working on an application that previously used Jackso The context's javadoc:tools.jackson.databind.json.JsonMapper$Builder[] can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer[] beans. Such customizer beans can be ordered (Boot's own customizer has an order of 0), letting additional customization be applied both before and after Boot's customization. +Furthermore, the `JsonFactory` used by the builder and the mapper that it creates can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonFactoryBuilderCustomizer[] beans. +Various `spring.jackson.factory` properties can also be used to configure the factory. Any beans of type javadoc:tools.jackson.databind.JacksonModule[] are automatically registered with the auto-configured javadoc:tools.jackson.databind.json.JsonMapper$Builder[] and are applied to any javadoc:tools.jackson.databind.json.JsonMapper[] instances that it creates. This provides an application-wide mechanism for contributing custom modules when you add new features to your application. diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java new file mode 100644 index 00000000000..7fb6a14823e --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/CborFactoryBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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 + * + * https://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.jackson.autoconfigure; + +import tools.jackson.dataformat.cbor.CBORFactory; +import tools.jackson.dataformat.cbor.CBORFactoryBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link CBORFactory} through {@link CBORFactoryBuilder} to fine-tune its + * auto-configuration. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface CborFactoryBuilderCustomizer { + + /** + * Customize the CBORFactoryBuilder. + * @param cborFactoryBuilder the builder to customize + */ + void customize(CBORFactoryBuilder cborFactoryBuilder); + +} diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index ff1aebdd0bd..7a0c9041765 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -32,6 +32,11 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.PropertyAccessor; import org.jspecify.annotations.Nullable; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.base.DecorableTSFactory.DecorableTSFBuilder; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.json.JsonFactoryBuilder; import tools.jackson.databind.JacksonModule; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.PropertyNamingStrategies; @@ -40,7 +45,11 @@ import tools.jackson.databind.cfg.ConstructorDetector; import tools.jackson.databind.cfg.DateTimeFeature; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.dataformat.cbor.CBORFactory; +import tools.jackson.dataformat.cbor.CBORFactoryBuilder; import tools.jackson.dataformat.cbor.CBORMapper; +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlFactoryBuilder; import tools.jackson.dataformat.xml.XmlMapper; import org.springframework.aot.hint.ReflectionHints; @@ -55,10 +64,14 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.jackson.JacksonComponentModule; import org.springframework.boot.jackson.JacksonMixinModule; import org.springframework.boot.jackson.JacksonMixinModuleEntries; import org.springframework.boot.jackson.autoconfigure.JacksonProperties.ConstructorDetectorStrategy; +import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints; +import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints.Read; +import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints.Write; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -94,11 +107,21 @@ public final class JacksonAutoConfiguration { return new JacksonComponentModule(); } + @Bean + @ConditionalOnMissingBean + JsonFactory jsonFactory(List customizers) { + JsonFactoryBuilder builder = JsonFactory.builder(); + for (JsonFactoryBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } + return builder.build(); + } + @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean - JsonMapper.Builder jsonMapperBuilder(List customizers) { - JsonMapper.Builder builder = JsonMapper.builder(); + JsonMapper.Builder jsonMapperBuilder(List customizers, JsonFactory jsonFactory) { + JsonMapper.Builder builder = JsonMapper.builder(jsonFactory); customize(builder, customizers); return builder; } @@ -137,12 +160,36 @@ public final class JacksonAutoConfiguration { @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(JacksonProperties.class) - static class JacksonJsonMapperBuilderCustomizerConfiguration { + static class JacksonJsonCustomizerConfiguration { + + private final JacksonProperties jacksonProperties; + + JacksonJsonCustomizerConfiguration(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } @Bean - StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties, - ObjectProvider modules) { - return new StandardJsonMapperBuilderCustomizer(jacksonProperties, modules.stream().toList()); + StandardJsonFactoryBuilderCustomizer standardJsonFactoryBuilderCustomizer() { + return new StandardJsonFactoryBuilderCustomizer(this.jacksonProperties); + } + + @Bean + StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider modules) { + return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList()); + } + + static final class StandardJsonFactoryBuilderCustomizer + extends AbstractFactoryBuilderCustomizer implements JsonFactoryBuilderCustomizer { + + StandardJsonFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + super(jacksonProperties); + } + + @Override + public void customize(JsonFactoryBuilder jsonFactoryBuilder) { + super.customize(jsonFactoryBuilder); + } + } static final class StandardJsonMapperBuilderCustomizer @@ -189,17 +236,27 @@ public final class JacksonAutoConfiguration { @EnableConfigurationProperties(JacksonCborProperties.class) static class CborConfiguration { + private final JacksonProperties jacksonProperties; + + CborConfiguration(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } + @Bean @ConditionalOnMissingBean - CBORMapper cborMapper(CBORMapper.Builder builder) { + CBORFactory cborFactory(List customizers) { + CBORFactoryBuilder builder = CBORFactory.builder(); + for (CborFactoryBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } return builder.build(); } @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean - CBORMapper.Builder cborMapperBuilder(List customizers) { - CBORMapper.Builder builder = CBORMapper.builder(); + CBORMapper.Builder cborMapperBuilder(CBORFactory factory, List customizers) { + CBORMapper.Builder builder = CBORMapper.builder(factory); customize(builder, customizers); return builder; } @@ -211,12 +268,37 @@ public final class JacksonAutoConfiguration { } @Bean - StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(JacksonProperties jacksonProperties, - ObjectProvider modules, JacksonCborProperties cborProperties) { - return new StandardCborMapperBuilderCustomizer(jacksonProperties, modules.stream().toList(), + @ConditionalOnMissingBean + CBORMapper cborMapper(CBORMapper.Builder builder) { + return builder.build(); + } + + @Bean + StandardCborFactoryBuilderCustomizer standardCborFactoryBuilderCustomizer() { + return new StandardCborFactoryBuilderCustomizer(this.jacksonProperties); + } + + @Bean + StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(ObjectProvider modules, + JacksonCborProperties cborProperties) { + return new StandardCborMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), cborProperties); } + static final class StandardCborFactoryBuilderCustomizer + extends AbstractFactoryBuilderCustomizer implements CborFactoryBuilderCustomizer { + + StandardCborFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + super(jacksonProperties); + } + + @Override + public void customize(CBORFactoryBuilder cborFactoryBuilder) { + super.customize(cborFactoryBuilder); + } + + } + static class StandardCborMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer implements CborMapperBuilderCustomizer { @@ -244,17 +326,27 @@ public final class JacksonAutoConfiguration { @EnableConfigurationProperties(JacksonXmlProperties.class) static class XmlConfiguration { + private final JacksonProperties jacksonProperties; + + XmlConfiguration(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } + @Bean @ConditionalOnMissingBean - XmlMapper xmlMapper(XmlMapper.Builder builder) { + XmlFactory xmlFactory(List customizers) { + XmlFactoryBuilder builder = XmlFactory.builder(); + for (XmlFactoryBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } return builder.build(); } @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean - XmlMapper.Builder xmlMapperBuilder(List customizers) { - XmlMapper.Builder builder = XmlMapper.builder(); + XmlMapper.Builder xmlMapperBuilder(XmlFactory xmlFactory, List customizers) { + XmlMapper.Builder builder = XmlMapper.builder(xmlFactory); customize(builder, customizers); return builder; } @@ -266,9 +358,21 @@ public final class JacksonAutoConfiguration { } @Bean - StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(JacksonProperties jacksonProperties, - ObjectProvider modules, JacksonXmlProperties xmlProperties) { - return new StandardXmlMapperBuilderCustomizer(jacksonProperties, modules.stream().toList(), xmlProperties); + @ConditionalOnMissingBean + XmlMapper xmlMapper(XmlMapper.Builder builder) { + return builder.build(); + } + + @Bean + StandardXmlFactoryBuilderCustomizer standardXmlFactoryBuilderCustomizer() { + return new StandardXmlFactoryBuilderCustomizer(this.jacksonProperties); + } + + @Bean + StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(ObjectProvider modules, + JacksonXmlProperties xmlProperties) { + return new StandardXmlMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(), + xmlProperties); } @Configuration(proxyBeanMethods = false) @@ -291,6 +395,20 @@ public final class JacksonAutoConfiguration { } + static final class StandardXmlFactoryBuilderCustomizer + extends AbstractFactoryBuilderCustomizer implements XmlFactoryBuilderCustomizer { + + StandardXmlFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + super(jacksonProperties); + } + + @Override + public void customize(XmlFactoryBuilder xmlFactoryBuilder) { + super.customize(xmlFactoryBuilder); + } + + } + static class StandardXmlMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer implements XmlMapperBuilderCustomizer { @@ -344,6 +462,46 @@ public final class JacksonAutoConfiguration { } + abstract static class AbstractFactoryBuilderCustomizer> implements Ordered { + + private final JacksonProperties jacksonProperties; + + AbstractFactoryBuilderCustomizer(JacksonProperties jacksonProperties) { + this.jacksonProperties = jacksonProperties; + } + + @Override + public int getOrder() { + return 0; + } + + protected void customize(B builder) { + Constraints constraints = this.jacksonProperties.getFactory().getConstraints(); + builder.streamReadConstraints(readConstraintsFrom(constraints.getRead())); + builder.streamWriteConstraints(writeConstraintsFrom(constraints.getWrite())); + } + + private StreamReadConstraints readConstraintsFrom(Read read) { + PropertyMapper map = PropertyMapper.get(); + StreamReadConstraints.Builder constraintsBuilder = StreamReadConstraints.builder(); + map.from(read::getMaxDocumentLength).to(constraintsBuilder::maxDocumentLength); + map.from(read::getMaxNameLength).to(constraintsBuilder::maxNameLength); + map.from(read::getMaxNestingDepth).to(constraintsBuilder::maxNestingDepth); + map.from(read::getMaxNumberLength).to(constraintsBuilder::maxNumberLength); + map.from(read::getMaxStringLength).to(constraintsBuilder::maxStringLength); + map.from(read::getMaxTokenCount).to(constraintsBuilder::maxTokenCount); + return constraintsBuilder.build(); + } + + private StreamWriteConstraints writeConstraintsFrom(Write write) { + PropertyMapper map = PropertyMapper.get(); + StreamWriteConstraints.Builder constraintsBuilder = StreamWriteConstraints.builder(); + map.from(write::getMaxNestingDepth).to(constraintsBuilder::maxNestingDepth); + return constraintsBuilder.build(); + } + + } + abstract static class AbstractMapperBuilderCustomizer> implements Ordered { private final JacksonProperties jacksonProperties; diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java index f62083ad76b..472b6804519 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java @@ -137,6 +137,8 @@ public class JacksonProperties { private final Json json = new Json(); + private final Factory factory = new Factory(); + public @Nullable String getDateFormat() { return this.dateFormat; } @@ -241,6 +243,10 @@ public class JacksonProperties { return this.json; } + public Factory getFactory() { + return this.factory; + } + public enum ConstructorDetectorStrategy { /** @@ -319,4 +325,133 @@ public class JacksonProperties { } + public static class Factory { + + private final Constraints constraints = new Constraints(); + + public Constraints getConstraints() { + return this.constraints; + } + + public static class Constraints { + + private final Read read = new Read(); + + private final Write write = new Write(); + + public Read getRead() { + return this.read; + } + + public Write getWrite() { + return this.write; + } + + public static class Read { + + /** + * Maximum nesting depth. The depth is a count of objects and arrays that + * have not been closed. + */ + private int maxNestingDepth = 500; + + /** + * Maximum allowed document length. A value less than or equal to zero + * indicates that any length is acceptable. + */ + private long maxDocumentLength = -1L; + + /** + * Maximum allowed token count. A value less than or equal to zero + * indicates that any count is acceptable. + */ + private long maxTokenCount = -1L; + + /** + * Maximum number length. + */ + private int maxNumberLength = 1_000; + + /** + * Maximum string length. + */ + private int maxStringLength = 20_000_000; + + /** + * Maximum name length. + */ + private int maxNameLength = 50_000; + + public int getMaxNestingDepth() { + return this.maxNestingDepth; + } + + public void setMaxNestingDepth(int maxNestingDepth) { + this.maxNestingDepth = maxNestingDepth; + } + + public long getMaxDocumentLength() { + return this.maxDocumentLength; + } + + public void setMaxDocumentLength(long maxDocumentLength) { + this.maxDocumentLength = maxDocumentLength; + } + + public long getMaxTokenCount() { + return this.maxTokenCount; + } + + public void setMaxTokenCount(long maxTokenCount) { + this.maxTokenCount = maxTokenCount; + } + + public int getMaxNumberLength() { + return this.maxNumberLength; + } + + public void setMaxNumberLength(int maxNumberLength) { + this.maxNumberLength = maxNumberLength; + } + + public int getMaxStringLength() { + return this.maxStringLength; + } + + public void setMaxStringLength(int maxStringLength) { + this.maxStringLength = maxStringLength; + } + + public int getMaxNameLength() { + return this.maxNameLength; + } + + public void setMaxNameLength(int maxNameLength) { + this.maxNameLength = maxNameLength; + } + + } + + public static class Write { + + /** + * Maximum nesting depth. The depth is a count of objects and arrays that + * have not been closed. + */ + private int maxNestingDepth = 500; + + public int getMaxNestingDepth() { + return this.maxNestingDepth; + } + + public void setMaxNestingDepth(int maxNestingDepth) { + this.maxNestingDepth = maxNestingDepth; + } + + } + + } + + } + } diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java new file mode 100644 index 00000000000..4865a1a2b6e --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JsonFactoryBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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 + * + * https://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.jackson.autoconfigure; + +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.json.JsonFactoryBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link JsonFactory} through {@link JsonFactoryBuilder} to fine-tune its + * auto-configuration. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface JsonFactoryBuilderCustomizer { + + /** + * Customize the JsonFactoryBuilder. + * @param jsonFactoryBuilder the builder to customize + */ + void customize(JsonFactoryBuilder jsonFactoryBuilder); + +} diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java new file mode 100644 index 00000000000..0e92a646884 --- /dev/null +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/XmlFactoryBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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 + * + * https://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.jackson.autoconfigure; + +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlFactoryBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link XmlFactory} through {@link XmlFactoryBuilder} to fine-tune its + * auto-configuration. + * + * @author Andy Wilkinson + * @since 4.1.0 + */ +@FunctionalInterface +public interface XmlFactoryBuilderCustomizer { + + /** + * Customize the XmlFactoryBuilder. + * @param xmlFactoryBuilder the builder to customize + */ + void customize(XmlFactoryBuilder xmlFactoryBuilder); + +} diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index 02eaa6c0ef4..96ecbab9748 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -32,8 +32,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import tools.jackson.core.JsonGenerator; +import tools.jackson.core.StreamReadConstraints; import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.StreamWriteConstraints; import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.json.JsonFactory; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.json.JsonWriteFeature; import tools.jackson.databind.DeserializationFeature; @@ -56,7 +60,9 @@ import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.json.JsonMapper.Builder; import tools.jackson.databind.module.SimpleModule; import tools.jackson.databind.util.StdDateFormat; +import tools.jackson.dataformat.cbor.CBORFactory; import tools.jackson.dataformat.cbor.CBORMapper; +import tools.jackson.dataformat.xml.XmlFactory; import tools.jackson.dataformat.xml.XmlMapper; import tools.jackson.module.kotlin.KotlinModule; @@ -70,9 +76,12 @@ import org.springframework.boot.jackson.JacksonMixin; import org.springframework.boot.jackson.JacksonMixinModule; import org.springframework.boot.jackson.JacksonMixinModuleEntries; import org.springframework.boot.jackson.ObjectValueSerializer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.CborConfiguration.StandardCborFactoryBuilderCustomizer; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.CborConfiguration.StandardCborMapperBuilderCustomizer; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints; -import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonMapperBuilderCustomizerConfiguration.StandardJsonMapperBuilderCustomizer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonCustomizerConfiguration.StandardJsonFactoryBuilderCustomizer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonCustomizerConfiguration.StandardJsonMapperBuilderCustomizer; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.XmlConfiguration.StandardXmlFactoryBuilderCustomizer; import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.XmlConfiguration.StandardXmlMapperBuilderCustomizer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; @@ -106,6 +115,12 @@ class JacksonAutoConfigurationTests { protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)); + @EnumSource + @ParameterizedTest + void definesFactory(MapperType mapperType) { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(mapperType.factoryClass)); + } + @EnumSource @ParameterizedTest void definesMapper(MapperType mapperType) { @@ -118,6 +133,15 @@ class JacksonAutoConfigurationTests { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(mapperType.builderClass)); } + @EnumSource + @ParameterizedTest + void factoryBacksOffWhenCustomFactoryIsDefined(MapperType mapperType) { + this.contextRunner.withBean("customFactory", mapperType.factoryClass).run((context) -> { + assertThat(context).hasSingleBean(mapperType.factoryClass); + assertThat(context).hasBean("customFactory"); + }); + } + @EnumSource @ParameterizedTest void mapperBacksOffWhenCustomMapperIsDefined(MapperType mapperType) { @@ -161,6 +185,24 @@ class JacksonAutoConfigurationTests { .run((context) -> assertThat(context).hasSingleBean(StandardXmlMapperBuilderCustomizer.class)); } + @Test + void standardJsonFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { + this.contextRunner.withBean(JsonFactoryBuilderCustomizer.class, () -> mock(JsonFactoryBuilderCustomizer.class)) + .run((context) -> assertThat(context).hasSingleBean(StandardJsonFactoryBuilderCustomizer.class)); + } + + @Test + void standardCborFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { + this.contextRunner.withBean(CborFactoryBuilderCustomizer.class, () -> mock(CborFactoryBuilderCustomizer.class)) + .run((context) -> assertThat(context).hasSingleBean(StandardCborFactoryBuilderCustomizer.class)); + } + + @Test + void standardXmlFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() { + this.contextRunner.withBean(XmlFactoryBuilderCustomizer.class, () -> mock(XmlFactoryBuilderCustomizer.class)) + .run((context) -> assertThat(context).hasSingleBean(StandardXmlFactoryBuilderCustomizer.class)); + } + @Test void doubleModuleRegistration() { this.contextRunner.withUserConfiguration(DoubleModulesConfig.class).run((context) -> { @@ -814,6 +856,63 @@ class JacksonAutoConfigurationTests { .doesNotHaveAnyElementsOfTypes(KotlinModule.class)); } + @EnumSource + @ParameterizedTest + void defaultStreamReadConstraintsMatchJacksonDefaults(MapperType mapperType) { + this.contextRunner.run((context) -> { + StreamReadConstraints streamReadConstraints = mapperType.getFactory(context).streamReadConstraints(); + assertThat(streamReadConstraints.getMaxDocumentLength()) + .isEqualTo(StreamReadConstraints.DEFAULT_MAX_DOC_LEN); + assertThat(streamReadConstraints.getMaxNameLength()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_NAME_LEN); + assertThat(streamReadConstraints.getMaxNestingDepth()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_DEPTH); + assertThat(streamReadConstraints.getMaxNumberLength()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_NUM_LEN); + assertThat(streamReadConstraints.getMaxStringLength()) + .isEqualTo(StreamReadConstraints.DEFAULT_MAX_STRING_LEN); + assertThat(streamReadConstraints.getMaxTokenCount()) + .isEqualTo(StreamReadConstraints.DEFAULT_MAX_TOKEN_COUNT); + }); + } + + @EnumSource + @ParameterizedTest + void customStreamReadConstraintsAreAppliedToAutoConfiguredFactory(MapperType mapperType) { + this.contextRunner + .withPropertyValues("spring.jackson.factory.constraints.read.max-document-length=1000", + "spring.jackson.factory.constraints.read.max-name-length=1001", + "spring.jackson.factory.constraints.read.max-nesting-depth=1002", + "spring.jackson.factory.constraints.read.max-number-length=1003", + "spring.jackson.factory.constraints.read.max-string-length=1004", + "spring.jackson.factory.constraints.read.max-token-count=1005") + .run((context) -> { + StreamReadConstraints streamReadConstraints = mapperType.getFactory(context).streamReadConstraints(); + assertThat(streamReadConstraints.getMaxDocumentLength()).isEqualTo(1000); + assertThat(streamReadConstraints.getMaxNameLength()).isEqualTo(1001); + assertThat(streamReadConstraints.getMaxNestingDepth()).isEqualTo(1002); + assertThat(streamReadConstraints.getMaxNumberLength()).isEqualTo(1003); + assertThat(streamReadConstraints.getMaxStringLength()).isEqualTo(1004); + assertThat(streamReadConstraints.getMaxTokenCount()).isEqualTo(1005); + }); + } + + @EnumSource + @ParameterizedTest + void customStreamWriteConstraintsAreAppliedToAutoConfiguredFactory(MapperType mapperType) { + this.contextRunner.withPropertyValues("spring.jackson.factory.constraints.write.max-nesting-depth=1000") + .run((context) -> { + StreamWriteConstraints streamWriteConstraints = mapperType.getFactory(context).streamWriteConstraints(); + assertThat(streamWriteConstraints.getMaxNestingDepth()).isEqualTo(1000); + }); + } + + @EnumSource + @ParameterizedTest + void defaultStreamWriteConstraintsMatchJacksonDefaults(MapperType mapperType) { + this.contextRunner.run((context) -> { + StreamWriteConstraints streamWriteConstraints = mapperType.getFactory(context).streamWriteConstraints(); + assertThat(streamWriteConstraints.getMaxNestingDepth()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_DEPTH); + }); + } + static class MyDateFormat extends SimpleDateFormat { MyDateFormat() { @@ -1137,15 +1236,19 @@ class JacksonAutoConfigurationTests { enum MapperType { - CBOR(CBORMapper.class, CBORMapper.Builder.class), JSON(JsonMapper.class, JsonMapper.Builder.class), - XML(XmlMapper.class, XmlMapper.Builder.class); + CBOR(CBORFactory.class, CBORMapper.class, CBORMapper.Builder.class), + JSON(JsonFactory.class, JsonMapper.class, JsonMapper.Builder.class), + XML(XmlFactory.class, XmlMapper.class, XmlMapper.Builder.class); + + private final Class factoryClass; private final Class mapperClass; private final Class> builderClass; - > MapperType(Class mapperClass, - Class builderClass) { + > MapperType( + Class factoryClass, Class mapperClass, Class builderClass) { + this.factoryClass = factoryClass; this.mapperClass = mapperClass; this.builderClass = builderClass; } @@ -1154,6 +1257,10 @@ class JacksonAutoConfigurationTests { return context.getBean(this.mapperClass); } + TokenStreamFactory getFactory(ApplicationContext context) { + return context.getBean(this.mapperClass).tokenStreamFactory(); + } + } }