Add Jackson 2 modules and support

Closes gh-47688
This commit is contained in:
Phillip Webb
2025-10-17 13:19:02 -07:00
109 changed files with 6600 additions and 345 deletions
@@ -165,6 +165,7 @@ public class AntoraAsciidocAttributes {
addDependencyVersion(attributes, "jackson-core", "tools.jackson.core:jackson-core");
addDependencyVersion(attributes, "jackson-databind", "tools.jackson.core:jackson-databind");
addDependencyVersion(attributes, "jackson-dataformat-xml", "tools.jackson.dataformat:jackson-dataformat-xml");
addDependencyVersion(attributes, "jackson2-databind", "com.fasterxml.jackson.core:jackson-databind");
addSpringDataDependencyVersion(attributes, internal, "spring-data-commons");
addSpringDataDependencyVersion(attributes, internal, "spring-data-couchbase");
addSpringDataDependencyVersion(attributes, internal, "spring-data-cassandra");
@@ -80,10 +80,12 @@ url-jackson-annotations-javadoc=https://javadoc.io/doc/com.fasterxml.jackson.cor
url-jackson-core-javadoc=https://javadoc.io/doc/tools.jackson.core/jackson-core/{version-jackson-core}
url-jackson-databind-javadoc=https://javadoc.io/doc/tools.jackson.core/jackson-databind/{version-jackson-databind}
url-jackson-dataformat-xml-javadoc=https://javadoc.io/doc/tools.jackson.dataformat/jackson-dataformat-xml/{version-jackson-dataformat-xml}
url-jackson2-databind-javadoc=https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/{version-jackson2-databind}
# === Javadoc Locations ===
javadoc-location-com-fasterxml-jackson-annotation={url-jackson-annotations-javadoc}
javadoc-location-com-fasterxml-jackson-databind={url-jackson2-databind-javadoc}
javadoc-location-org-apache-pulsar-client-api={url-pulsar-client-api-javadoc}
javadoc-location-org-apache-pulsar-reactive-client-api={url-pulsar-client-reactive-api-javadoc}
javadoc-location-org-springframework-data-cassandra={url-spring-data-cassandra-javadoc}
@@ -282,8 +282,10 @@ class AntoraAsciidocAttributesTests {
addMockTestcontainersVersion(versions, "redpanda", version);
addMockTestcontainersVersion(versions, "r2dbc", version);
addMockJackson2CoreVersion(versions, "jackson-annotations", version);
addMockJackson2CoreVersion(versions, "jackson-databind", version);
addMockJacksonCoreVersion(versions, "jackson-core", version);
addMockJacksonCoreVersion(versions, "jackson-databind", version);
addMockJacksonCoreVersion(versions, "jackson-databind", version);
versions.put("org.apache.pulsar:pulsar-client-api", version);
versions.put("org.apache.pulsar:pulsar-client-reactive-api", version);
versions.put("tools.jackson.dataformat:jackson-dataformat-xml", version);
+2 -1
View File
@@ -27,7 +27,7 @@ dependencies {
api(project(":core:spring-boot"))
api("org.springframework:spring-test")
optional("tools.jackson.core:jackson-databind")
optional("com.fasterxml.jackson.core:jackson-databind")
optional("com.google.code.gson:gson")
optional("com.jayway.jsonpath:json-path")
optional("jakarta.json.bind:jakarta.json.bind-api")
@@ -45,6 +45,7 @@ dependencies {
optional("org.seleniumhq.selenium:selenium-api")
optional("org.skyscreamer:jsonassert")
optional("org.springframework:spring-web")
optional("tools.jackson.core:jackson-databind")
testImplementation(project(":test-support:spring-boot-test-support"))
testImplementation("ch.qos.logback:logback-classic")
@@ -0,0 +1,207 @@
/*
* 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.test.json;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.core.ResolvableType;
import org.springframework.util.Assert;
/**
* AssertJ based JSON tester backed by Jackson 2. Usually instantiated via
* {@link #initFields(Object, ObjectMapper)}, for example: <pre class="code">
* public class ExampleObjectJsonTests {
*
* private Jackson2Tester&lt;ExampleObject&gt; json;
*
* &#064;Before
* public void setup() {
* ObjectMapper objectMapper = new ObjectMapper();
* Jackson2Tester.initFields(this, objectMapper);
* }
*
* &#064;Test
* public void testWriteJson() throws IOException {
* ExampleObject object = //...
* assertThat(json.write(object)).isEqualToJson("expected.json");
* }
*
* }
* </pre>
*
* See {@link AbstractJsonMarshalTester} for more details.
*
* @param <T> the type under test
* @author Phillip Webb
* @author Madhura Bhave
* @author Diego Berrueta
* @since 4.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
public class Jackson2Tester<T> extends AbstractJsonMarshalTester<T> {
private final ObjectMapper objectMapper;
private @Nullable Class<?> view;
/**
* Create a new {@link Jackson2Tester} instance.
* @param objectMapper the Jackson object mapper
*/
protected Jackson2Tester(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
/**
* Create a new {@link Jackson2Tester} instance.
* @param resourceLoadClass the source class used to load resources
* @param type the type under test
* @param objectMapper the Jackson object mapper
*/
public Jackson2Tester(Class<?> resourceLoadClass, ResolvableType type, ObjectMapper objectMapper) {
this(resourceLoadClass, type, objectMapper, null);
}
/**
* Create a new {@link Jackson2Tester} instance.
* @param resourceLoadClass the source class used to load resources
* @param type the type under test
* @param objectMapper the Jackson object mapper
* @param view the JSON view
*/
public Jackson2Tester(Class<?> resourceLoadClass, ResolvableType type, ObjectMapper objectMapper,
@Nullable Class<?> view) {
super(resourceLoadClass, type);
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
this.view = view;
}
@Override
protected JsonContent<T> getJsonContent(String json) {
Configuration configuration = Configuration.builder()
.jsonProvider(new JacksonJsonProvider(this.objectMapper))
.mappingProvider(new JacksonMappingProvider(this.objectMapper))
.build();
Class<?> resourceLoadClass = getResourceLoadClass();
Assert.state(resourceLoadClass != null, "'resourceLoadClass' must not be null");
return new JsonContent<>(resourceLoadClass, getType(), json, configuration);
}
@Override
protected T readObject(InputStream inputStream, ResolvableType type) throws IOException {
return getObjectReader(type).readValue(inputStream);
}
@Override
protected T readObject(Reader reader, ResolvableType type) throws IOException {
return getObjectReader(type).readValue(reader);
}
private ObjectReader getObjectReader(ResolvableType type) {
ObjectReader objectReader = this.objectMapper.readerFor(getType(type));
if (this.view != null) {
return objectReader.withView(this.view);
}
return objectReader;
}
@Override
protected String writeObject(T value, ResolvableType type) throws IOException {
return getObjectWriter(type).writeValueAsString(value);
}
private ObjectWriter getObjectWriter(ResolvableType type) {
ObjectWriter objectWriter = this.objectMapper.writerFor(getType(type));
if (this.view != null) {
return objectWriter.withView(this.view);
}
return objectWriter;
}
private JavaType getType(ResolvableType type) {
return this.objectMapper.constructType(type.getType());
}
/**
* Utility method to initialize {@link Jackson2Tester} fields. See
* {@link Jackson2Tester class-level documentation} for example usage.
* @param testInstance the test instance
* @param objectMapper the JSON mapper
* @see #initFields(Object, ObjectMapper)
*/
public static void initFields(Object testInstance, ObjectMapper objectMapper) {
new Jackson2FieldInitializer().initFields(testInstance, objectMapper);
}
/**
* Utility method to initialize {@link Jackson2Tester} fields. See
* {@link Jackson2Tester class-level documentation} for example usage.
* @param testInstance the test instance
* @param objectMapperFactory a factory to create the object mapper
* @see #initFields(Object, ObjectMapper)
*/
public static void initFields(Object testInstance, ObjectFactory<ObjectMapper> objectMapperFactory) {
new Jackson2FieldInitializer().initFields(testInstance, objectMapperFactory);
}
/**
* Returns a new instance of {@link Jackson2Tester} with the view that should be used
* for json serialization/deserialization.
* @param view the view class
* @return the new instance
*/
public Jackson2Tester<T> forView(Class<?> view) {
Class<?> resourceLoadClass = getResourceLoadClass();
ResolvableType type = getType();
Assert.state(resourceLoadClass != null, "'resourceLoadClass' must not be null");
Assert.state(type != null, "'type' must not be null");
return new Jackson2Tester<>(resourceLoadClass, type, this.objectMapper, view);
}
/**
* {@link FieldInitializer} for Jackson.
*/
private static class Jackson2FieldInitializer extends FieldInitializer<ObjectMapper> {
protected Jackson2FieldInitializer() {
super(Jackson2Tester.class);
}
@Override
protected AbstractJsonMarshalTester<Object> createTester(Class<?> resourceLoadClass, ResolvableType type,
ObjectMapper marshaller) {
return new Jackson2Tester<>(resourceLoadClass, type, marshaller);
}
}
}
@@ -103,6 +103,14 @@ public class JacksonTester<T> extends AbstractJsonMarshalTester<T> {
this(resourceLoadClass, type, jsonMapper, null);
}
/**
* Create a new {@link JacksonTester} instance.
* @param resourceLoadClass the source class used to load resources
* @param type the type under test
* @param jsonMapper the Jackson JSON mapper
* @param view the JSON view
* @since 4.0.0
*/
public JacksonTester(Class<?> resourceLoadClass, ResolvableType type, JsonMapper jsonMapper,
@Nullable Class<?> view) {
super(resourceLoadClass, type);
@@ -0,0 +1,92 @@
/*
* 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.test.json;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Jackson2Tester}.
*
* @author Phillip Webb
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of JacksonTesterTests
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class Jackson2TesterTests extends AbstractJsonMarshalTesterTests {
@Test
@SuppressWarnings("NullAway") // Test null check
void initFieldsWhenTestIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> Jackson2Tester.initFields(null, new ObjectMapper()))
.withMessageContaining("'testInstance' must not be null");
}
@Test
@SuppressWarnings("NullAway") // Test null check
void initFieldsWhenMarshallerIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> Jackson2Tester.initFields(new InitFieldsTestClass(), (ObjectMapper) null))
.withMessageContaining("'marshaller' must not be null");
}
@Test
void initFieldsShouldSetNullFields() {
InitFieldsTestClass test = new InitFieldsTestClass();
assertThat(test.test).isNull();
assertThat(test.base).isNull();
Jackson2Tester.initFields(test, new ObjectMapper());
assertThat(test.test).isNotNull();
assertThat(test.base).isNotNull();
ResolvableType type = test.test.getType();
assertThat(type).isNotNull();
assertThat(type.resolve()).isEqualTo(List.class);
assertThat(type.resolveGeneric()).isEqualTo(ExampleObject.class);
}
@Override
protected AbstractJsonMarshalTester<Object> createTester(Class<?> resourceLoadClass, ResolvableType type) {
return new org.springframework.boot.test.json.Jackson2Tester<>(resourceLoadClass, type, new ObjectMapper());
}
abstract static class InitFieldsBaseClass {
public org.springframework.boot.test.json.@Nullable Jackson2Tester<ExampleObject> base;
public org.springframework.boot.test.json.Jackson2Tester<ExampleObject> baseSet = new org.springframework.boot.test.json.Jackson2Tester<>(
InitFieldsBaseClass.class, ResolvableType.forClass(ExampleObject.class), new ObjectMapper());
}
static class InitFieldsTestClass extends InitFieldsBaseClass {
public org.springframework.boot.test.json.@Nullable Jackson2Tester<List<ExampleObject>> test;
public org.springframework.boot.test.json.Jackson2Tester<ExampleObject> testSet = new org.springframework.boot.test.json.Jackson2Tester<>(
InitFieldsBaseClass.class, ResolvableType.forClass(ExampleObject.class), new ObjectMapper());
}
}
@@ -3,19 +3,23 @@
Spring Boot provides integration with the following JSON mapping libraries:
- Gson
- Jackson 3
- Jackson 2
- Gson
- JSON-B
- Kotlin Serialization
Jackson is the preferred and default library.
Jackson 3 is the preferred and default library.
Support for Jackson 2 is deprecated and will be removed in a future Spring Boot 4.x release.
It is provided purely to ease the migration from Jackson 2 to Jackson 3 and should not be relied up in the longer term.
[[features.json.jackson]]
== Jackson
== Jackson 3
Auto-configuration for Jackson is provided and Jackson is part of `spring-boot-starter-json`.
Auto-configuration for Jackson 3 is provided and Jackson is part of `spring-boot-starter-json`.
When Jackson is on the classpath a javadoc:tools.jackson.databind.json.JsonMapper[] bean is automatically configured.
Several configuration properties are provided for xref:how-to:spring-mvc.adoc#howto.spring-mvc.customize-jackson-jsonmapper[customizing the configuration of the javadoc:tools.jackson.databind.json.JsonMapper[]].
@@ -53,6 +57,26 @@ The registration is performed by Spring Boot's javadoc:org.springframework.boot.
[[features.json.jackson2]]
== Jackson 2
Deprecated auto-configuration for Jackson 2 is provided by the `spring-boot-jackson2` module.
When this module is on the classpath a javadoc:com.fasterxml.jackson.databind.ObjectMapper[] bean is automatically configured.
Several `+spring.jackson2.*+` configuration properties are provided for customizing the configuration.
To take more control, define one or more javadoc:org.springframework.boot.jackson2.autoconfigure.Jackson2ObjectMapperBuilderCustomizer[] beans.
When both Jackson 3 and Jackson 2 are present, various configuration properties can be used to indicate that Jackson 2 is preferred:
- configprop:spring.graphql.rsocket.preferred-json-mapper[]
- configprop:spring.http.codecs.preferred-json-mapper[] (used by Spring WebFlux and reactive HTTP clients)
- configprop:spring.http.converters.preferred-json-mapper[] (used by Spring MVC and imperative HTTP clients)
- configprop:spring.rsocket.preferred-mapper[]
- configprop:spring.websocket.messaging.preferred-json-mapper[]
In each case, set the relevant property to `jackson2` to indicate that Jackson 2 is preferred.
[[features.json.gson]]
== Gson
@@ -50,6 +50,7 @@ dependencies {
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-jackson")
app(files(sourceSets.main.output))
app("org.springframework.boot:spring-boot-webflux")
@@ -45,7 +45,7 @@ dependencies {
compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0")
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation("org.springframework.boot:spring-boot-starter-web") {
implementation("org.springframework.boot:spring-boot-starter-webmvc") {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation("org.springframework.boot:spring-boot-starter-actuator")
@@ -29,13 +29,13 @@ dependencies {
api(project(":core:spring-boot-autoconfigure"))
api(project(":module:spring-boot-actuator"))
implementation("tools.jackson.core:jackson-databind")
optional(project(":module:spring-boot-web-server"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("io.micrometer:micrometer-core")
optional("io.projectreactor:reactor-core")
optional("jakarta.servlet:jakarta.servlet-api")
optional("org.springframework.security:spring-security-config")
optional("tools.jackson.core:jackson-databind")
testFixturesImplementation(project(":core:spring-boot-test"))
testFixturesImplementation(project(":test-support:spring-boot-test-support"))
@@ -0,0 +1,55 @@
/*
* 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.actuate.autoconfigure.endpoint.jackson;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Endpoint Jackson 2 support.
*
* @author Phillip Webb
* @since 3.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public final class Jackson2EndpointAutoConfiguration {
@Bean
@ConditionalOnBooleanProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
EndpointJackson2ObjectMapper jackson2EndpointJsonMapper() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
.serializationInclusion(Include.NON_NULL)
.build();
return () -> objectMapper;
}
}
@@ -34,11 +34,11 @@ import org.springframework.context.annotation.Bean;
* @since 3.0.0
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
public final class JacksonEndpointAutoConfiguration {
@Bean
@ConditionalOnBooleanProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
@ConditionalOnClass(ObjectMapper.class)
EndpointJsonMapper endpointJsonMapper() {
JsonMapper jsonMapper = JsonMapper.builder()
.changeDefaultPropertyInclusion(
@@ -42,6 +42,7 @@ import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDisco
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
@@ -99,15 +100,17 @@ public final class JmxEndpointAutoConfiguration {
@Bean
@ConditionalOnSingleCandidate(MBeanServer.class)
@ConditionalOnClass(ObjectMapper.class)
JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, EndpointObjectNameFactory endpointObjectNameFactory,
ObjectProvider<ObjectMapper> objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) {
JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper(
objectMapper.getIfAvailable());
return new JmxEndpointExporter(mBeanServer, endpointObjectNameFactory, responseMapper,
jmxEndpointsSupplier.getEndpoints());
}
// FIXME
@Bean
IncludeExcludeEndpointFilter<ExposableJmxEndpoint> jmxIncludeExcludePropertyEndpointFilter() {
JmxEndpointProperties.Exposure exposure = this.properties.getExposure();
@@ -5,6 +5,7 @@ org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoin
org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.Jackson2EndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration
+2
View File
@@ -27,6 +27,8 @@ description = "Spring Boot Actuator"
dependencies {
api(project(":core:spring-boot"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
optional("com.github.ben-manes.caffeine:caffeine")
optional("com.google.code.findbugs:jsr305")
optional("com.zaxxer:HikariCP")
@@ -0,0 +1,34 @@
/*
* 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.actuate.context.properties;
import java.util.Map;
import org.jspecify.annotations.Nullable;
/**
* Strategy used by {@link ConfigurationPropertiesReportEndpoint} to serialize beans into
* a {@link Map}.
*
* @author Phillip Webb
*/
@FunctionalInterface
interface BeanSerializer {
Map<String, @Nullable Object> serialize(@Nullable Object bean);
}
@@ -16,10 +16,7 @@
package org.springframework.boot.actuate.context.properties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -28,31 +25,7 @@ import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.BeanDescription;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationConfig;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.introspect.Annotated;
import tools.jackson.databind.introspect.AnnotatedMethod;
import tools.jackson.databind.introspect.DefaultAccessorNamingStrategy;
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.BeanPropertyWriter;
import tools.jackson.databind.ser.BeanSerializerFactory;
import tools.jackson.databind.ser.PropertyWriter;
import tools.jackson.databind.ser.SerializerFactory;
import tools.jackson.databind.ser.ValueSerializerModifier;
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter;
import tools.jackson.databind.ser.std.SimpleFilterProvider;
import tools.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
@@ -66,24 +39,15 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.context.properties.BoundConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
/**
* {@link Endpoint @Endpoint} to expose application properties from
@@ -107,20 +71,19 @@ import org.springframework.util.unit.DataSize;
@Endpoint(id = "configprops")
public class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware {
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
private final Sanitizer sanitizer;
private final Show showValues;
private final BeanSerializer serializer;
@SuppressWarnings("NullAway.Init")
private ApplicationContext context;
private @Nullable ObjectMapper objectMapper;
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions, Show showValues) {
this.sanitizer = new Sanitizer(sanitizingFunctions);
this.showValues = showValues;
this.serializer = getBeanSerializer();
}
@Override
@@ -151,74 +114,46 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
private ConfigurationPropertiesDescriptor getConfigurationProperties(ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
ObjectMapper mapper = getObjectMapper();
Map<@Nullable String, ContextConfigurationPropertiesDescriptor> contexts = new HashMap<>();
ApplicationContext target = context;
while (target != null) {
contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate, showUnsanitized));
contexts.put(target.getId(), describeBeans(target, beanFilterPredicate, showUnsanitized));
target = target.getParent();
}
return new ConfigurationPropertiesDescriptor(contexts);
}
private ObjectMapper getObjectMapper() {
if (this.objectMapper == null) {
JsonMapper.Builder builder = JsonMapper.builder();
configureJsonMapper(builder);
this.objectMapper = builder.build();
@SuppressWarnings("removal")
private static BeanSerializer getBeanSerializer() {
ClassLoader classLoader = ConfigurationPropertiesReportEndpoint.class.getClassLoader();
if (ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader)) {
return new JacksonBeanSerializer();
}
return this.objectMapper;
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
return new Jackson2BeanSerializer();
}
return (bean) -> {
throw new IllegalStateException("Jackson is required for the 'configprops' endpoint");
};
}
/**
* Configure Jackson's {@link JsonMapper} to be used to serialize the
* {@link ConfigurationProperties @ConfigurationProperties} objects into a {@link Map}
* structure.
* @param builder the json mapper builder
* @since 2.6.0
*/
protected void configureJsonMapper(JsonMapper.Builder builder) {
builder.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
builder.changeDefaultPropertyInclusion((value) -> value.withValueInclusion(Include.NON_NULL));
builder.accessorNaming(new DefaultAccessorNamingStrategy.Provider().withFirstCharAcceptance(true, false));
applyConfigurationPropertiesFilter(builder);
applySerializationModifier(builder);
builder.addModule(new ConfigurationPropertiesModule());
}
private void applyConfigurationPropertiesFilter(JsonMapper.Builder builder) {
builder.annotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
builder
.filterProvider(new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param builder the JsonMapper builder
*/
private void applySerializationModifier(JsonMapper.Builder builder) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
builder.serializerFactory(factory);
}
private ContextConfigurationPropertiesDescriptor describeBeans(ObjectMapper mapper, ApplicationContext context,
private ContextConfigurationPropertiesDescriptor describeBeans(ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values()
.stream()
.filter(beanFilterPredicate)
.collect(Collectors.toMap(ConfigurationPropertiesBean::getName,
(bean) -> describeBean(mapper, bean, showUnsanitized)));
(bean) -> describeBean(bean, showUnsanitized)));
return new ContextConfigurationPropertiesDescriptor(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null);
}
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean,
private ConfigurationPropertiesBeanDescriptor describeBean(ConfigurationPropertiesBean bean,
boolean showUnsanitized) {
String prefix = bean.getAnnotation().prefix();
Map<String, @Nullable Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
Map<String, @Nullable Object> serialized = safeSerialize(bean.getInstance(), prefix);
Map<String, @Nullable Object> properties = sanitize(prefix, serialized, showUnsanitized);
Map<String, Object> inputs = getInputs(prefix, serialized, showUnsanitized);
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
@@ -227,15 +162,13 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
/**
* Cautiously serialize the bean to a map (returning a map with an error message
* instead of throwing an exception if there is a problem).
* @param mapper the object mapper
* @param bean the source bean
* @param prefix the prefix
* @return the serialized instance
*/
@SuppressWarnings({ "unchecked" })
private Map<String, @Nullable Object> safeSerialize(ObjectMapper mapper, @Nullable Object bean, String prefix) {
private Map<String, @Nullable Object> safeSerialize(@Nullable Object bean, String prefix) {
try {
return new HashMap<>(mapper.convertValue(bean, Map.class));
return new HashMap<>(this.serializer.serialize(bean));
}
catch (Exception ex) {
return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'"));
@@ -401,173 +334,6 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
return (prefix.isEmpty() ? prefix : prefix + ".") + key;
}
/**
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
* properties.
*/
private static final class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(MapperConfig<?> config, Annotated a) {
Object id = super.findFilterId(config, a);
if (id == null) {
id = CONFIGURATION_PROPERTIES_FILTER_ID;
}
return id;
}
}
/**
* {@link SimpleBeanPropertyFilter} for serialization of
* {@link ConfigurationProperties @ConfigurationProperties} beans. The filter hides:
*
* <ul>
* <li>Properties that have a name starting with '$$'.
* <li>Properties that are self-referential.
* <li>Properties that throw an exception when retrieving their value.
* </ul>
*/
private static final class ConfigurationPropertiesPropertyFilter extends SimpleBeanPropertyFilter {
private static final Log logger = LogFactory.getLog(ConfigurationPropertiesPropertyFilter.class);
@Override
protected boolean include(BeanPropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
@Override
protected boolean include(PropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
private boolean include(String name) {
return !name.startsWith("$$");
}
@Override
public void serializeAsProperty(Object pojo, JsonGenerator jgen, SerializationContext context,
PropertyWriter writer) throws Exception {
if (writer instanceof BeanPropertyWriter beanPropertyWriter) {
try {
if (pojo == beanPropertyWriter.get(pojo)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName()
+ "' as it is self-referential");
}
return;
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName()
+ "' as an exception was thrown when retrieving its value", ex);
}
return;
}
}
super.serializeAsProperty(pojo, jgen, context, writer);
}
}
/**
* {@link SimpleModule} for configuring the serializer.
*/
private static final class ConfigurationPropertiesModule extends SimpleModule {
private ConfigurationPropertiesModule() {
addSerializer(DataSize.class, ToStringSerializer.instance);
}
}
/**
* {@link ValueSerializerModifier} to return only relevant configuration properties.
*/
protected static class GenericSerializerModifier extends ValueSerializerModifier {
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription.Supplier beanDesc,
List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
Class<?> beanClass = beanDesc.getType().getRawClass();
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
for (BeanPropertyWriter writer : beanProperties) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);
}
}
return result;
}
private boolean isCandidate(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer,
@Nullable Constructor<?> constructor) {
if (constructor != null) {
Parameter[] parameters = constructor.getParameters();
@Nullable String @Nullable [] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor);
if (names == null) {
names = new String[parameters.length];
}
for (int i = 0; i < parameters.length; i++) {
String name = MergedAnnotations.from(parameters[i])
.get(Name.class)
.getValue(MergedAnnotation.VALUE, String.class)
.orElse((names[i] != null) ? names[i] : parameters[i].getName());
if (name != null && name.equals(writer.getName())) {
return true;
}
}
}
return isReadable(beanDesc, writer);
}
private boolean isReadable(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer) {
Class<?> parentType = beanDesc.get().getType().getRawClass();
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = findSetter(beanDesc.get(), writer);
// If there's a setter, we assume it's OK to report on the value,
// similarly, if there's no setter but the package names match, we assume
// that it is a nested class used solely for binding to config props, so it
// should be kosher. Lists and Maps are also auto-detected by default since
// that's what the metadata generator does. This filter is not used if there
// is JSON metadata for the property, so it's mainly for user-defined beans.
return (setter != null) || ClassUtils.getPackageName(parentType).equals(ClassUtils.getPackageName(type))
|| Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type);
}
private @Nullable AnnotatedMethod findSetter(BeanDescription beanDesc, BeanPropertyWriter writer) {
String name = "set" + determineAccessorSuffix(writer.getName());
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = beanDesc.findMethod(name, new Class<?>[] { type });
// The enabled property of endpoints returns a boolean primitive but is set
// using a Boolean class
if (setter == null && type.equals(Boolean.TYPE)) {
setter = beanDesc.findMethod(name, new Class<?>[] { Boolean.class });
}
return setter;
}
/**
* Determine the accessor suffix of the specified {@code propertyName}, see
* section 8.8 "Capitalization of inferred names" of the JavaBean specs for more
* details.
* @param propertyName the property name to turn into an accessor suffix
* @return the accessor suffix for {@code propertyName}
*/
private String determineAccessorSuffix(String propertyName) {
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
return propertyName;
}
return StringUtils.capitalize(propertyName);
}
}
/**
* Description of an application's
* {@link ConfigurationProperties @ConfigurationProperties} beans.
@@ -0,0 +1,284 @@
/*
* 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.actuate.context.properties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
/**
* {@link BeanSerializer} backed by Jackson 2.
*
* @author Phillip Webb
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
class Jackson2BeanSerializer implements BeanSerializer {
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
private final ObjectMapper mapper;
Jackson2BeanSerializer() {
JsonMapper.Builder builder = JsonMapper.builder();
configureMapper(builder);
this.mapper = builder.build();
}
private void configureMapper(JsonMapper.Builder builder) {
builder.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
builder.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
builder.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
builder.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
builder.serializationInclusion(Include.NON_NULL);
applyConfigurationPropertiesFilter(builder);
applySerializationModifier(builder);
if (ClassUtils.isPresent("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule",
builder.getClass().getClassLoader())) {
builder.addModule(new JavaTimeModule());
}
builder.addModule(new ConfigurationPropertiesModule());
}
private void applyConfigurationPropertiesFilter(JsonMapper.Builder builder) {
builder.annotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
ConfigurationPropertiesPropertyFilter filter = new ConfigurationPropertiesPropertyFilter();
builder.filterProvider(new SimpleFilterProvider().setDefaultFilter(filter));
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param builder the JsonMapper builder
*/
private void applySerializationModifier(JsonMapper.Builder builder) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
builder.serializerFactory(factory);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, @Nullable Object> serialize(@Nullable Object bean) {
return this.mapper.convertValue(bean, Map.class);
}
/**
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
* properties.
*/
private static final class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(Annotated a) {
Object id = super.findFilterId(a);
return (id != null) ? id : CONFIGURATION_PROPERTIES_FILTER_ID;
}
}
/**
* {@link SimpleBeanPropertyFilter} for serialization of
* {@link ConfigurationProperties @ConfigurationProperties} beans. The filter hides:
*
* <ul>
* <li>Properties that have a name starting with '$$'.
* <li>Properties that are self-referential.
* <li>Properties that throw an exception when retrieving their value.
* </ul>
*/
private static final class ConfigurationPropertiesPropertyFilter extends SimpleBeanPropertyFilter {
private static final Log logger = LogFactory.getLog(ConfigurationPropertiesPropertyFilter.class);
@Override
protected boolean include(BeanPropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
@Override
protected boolean include(PropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
private boolean include(String name) {
return !name.startsWith("$$");
}
@Override
public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider,
PropertyWriter writer) throws Exception {
if (writer instanceof BeanPropertyWriter beanPropertyWriter) {
try {
if (pojo == beanPropertyWriter.get(pojo)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName()
+ "' as it is self-referential");
}
return;
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName()
+ "' as an exception was thrown when retrieving its value", ex);
}
return;
}
}
super.serializeAsField(pojo, jgen, provider, writer);
}
}
/**
* {@link SimpleModule} for configuring the serializer.
*/
private static final class ConfigurationPropertiesModule extends SimpleModule {
private ConfigurationPropertiesModule() {
addSerializer(DataSize.class, ToStringSerializer.instance);
}
}
/**
* {@link BeanSerializerModifier} to return only relevant configuration properties.
*/
protected static class GenericSerializerModifier extends BeanSerializerModifier {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
Class<?> beanClass = beanDesc.getType().getRawClass();
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
for (BeanPropertyWriter writer : beanProperties) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);
}
}
return result;
}
private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer,
@Nullable Constructor<?> constructor) {
if (constructor != null) {
Parameter[] parameters = constructor.getParameters();
@Nullable String @Nullable [] names = parameterNameDiscoverer.getParameterNames(constructor);
if (names == null) {
names = new String[parameters.length];
}
for (int i = 0; i < parameters.length; i++) {
String name = MergedAnnotations.from(parameters[i])
.get(Name.class)
.getValue(MergedAnnotation.VALUE, String.class)
.orElse((names[i] != null) ? names[i] : parameters[i].getName());
if (name.equals(writer.getName())) {
return true;
}
}
}
return isReadable(beanDesc, writer);
}
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
Class<?> parentType = beanDesc.getType().getRawClass();
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = findSetter(beanDesc, writer);
// If there's a setter, we assume it's OK to report on the value,
// similarly, if there's no setter but the package names match, we assume
// that it is a nested class used solely for binding to config props, so it
// should be kosher. Lists and Maps are also auto-detected by default since
// that's what the metadata generator does. This filter is not used if there
// is JSON metadata for the property, so it's mainly for user-defined beans.
return (setter != null) || ClassUtils.getPackageName(parentType).equals(ClassUtils.getPackageName(type))
|| Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type);
}
private @Nullable AnnotatedMethod findSetter(BeanDescription beanDesc, BeanPropertyWriter writer) {
String name = "set" + determineAccessorSuffix(writer.getName());
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = beanDesc.findMethod(name, new Class<?>[] { type });
// The enabled property of endpoints returns a boolean primitive but is set
// using a Boolean class
if (setter == null && type.equals(Boolean.TYPE)) {
setter = beanDesc.findMethod(name, new Class<?>[] { Boolean.class });
}
return setter;
}
/**
* Determine the accessor suffix of the specified {@code propertyName}, see
* section 8.8 "Capitalization of inferred names" of the JavaBean specs for more
* details.
* @param propertyName the property name to turn into an accessor suffix
* @return the accessor suffix for {@code propertyName}
*/
private String determineAccessorSuffix(String propertyName) {
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
return propertyName;
}
return StringUtils.capitalize(propertyName);
}
}
}
@@ -0,0 +1,275 @@
/*
* 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.actuate.context.properties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.BeanDescription;
import tools.jackson.databind.SerializationConfig;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.introspect.Annotated;
import tools.jackson.databind.introspect.AnnotatedMethod;
import tools.jackson.databind.introspect.DefaultAccessorNamingStrategy;
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.BeanPropertyWriter;
import tools.jackson.databind.ser.BeanSerializerFactory;
import tools.jackson.databind.ser.PropertyWriter;
import tools.jackson.databind.ser.SerializerFactory;
import tools.jackson.databind.ser.ValueSerializerModifier;
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter;
import tools.jackson.databind.ser.std.SimpleFilterProvider;
import tools.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
/**
* {@link BeanSerializer} backed by Jackson.
*
* @author Phillip Webb
*/
class JacksonBeanSerializer implements BeanSerializer {
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
private final JsonMapper mapper;
JacksonBeanSerializer() {
JsonMapper.Builder builder = JsonMapper.builder();
configureMapper(builder);
this.mapper = builder.build();
}
private void configureMapper(JsonMapper.Builder builder) {
builder.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
builder.changeDefaultPropertyInclusion((value) -> value.withValueInclusion(Include.NON_NULL));
builder.accessorNaming(new DefaultAccessorNamingStrategy.Provider().withFirstCharAcceptance(true, false));
applyConfigurationPropertiesFilter(builder);
applySerializationModifier(builder);
builder.addModule(new ConfigurationPropertiesModule());
}
private void applyConfigurationPropertiesFilter(JsonMapper.Builder builder) {
builder.annotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
ConfigurationPropertiesPropertyFilter filter = new ConfigurationPropertiesPropertyFilter();
builder.filterProvider(new SimpleFilterProvider().setDefaultFilter(filter));
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param builder the JsonMapper builder
*/
private void applySerializationModifier(JsonMapper.Builder builder) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
builder.serializerFactory(factory);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, @Nullable Object> serialize(@Nullable Object bean) {
return this.mapper.convertValue(bean, Map.class);
}
/**
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
* properties.
*/
private static final class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(MapperConfig<?> config, Annotated a) {
Object id = super.findFilterId(config, a);
return (id != null) ? id : CONFIGURATION_PROPERTIES_FILTER_ID;
}
}
/**
* {@link SimpleBeanPropertyFilter} for serialization of
* {@link ConfigurationProperties @ConfigurationProperties} beans. The filter hides:
*
* <ul>
* <li>Properties that have a name starting with '$$'.
* <li>Properties that are self-referential.
* <li>Properties that throw an exception when retrieving their value.
* </ul>
*/
private static final class ConfigurationPropertiesPropertyFilter extends SimpleBeanPropertyFilter {
private static final Log logger = LogFactory.getLog(ConfigurationPropertiesPropertyFilter.class);
@Override
protected boolean include(BeanPropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
@Override
protected boolean include(PropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
private boolean include(String name) {
return !name.startsWith("$$");
}
@Override
public void serializeAsProperty(Object pojo, JsonGenerator jgen, SerializationContext context,
PropertyWriter writer) throws Exception {
if (writer instanceof BeanPropertyWriter beanPropertyWriter) {
try {
if (pojo == beanPropertyWriter.get(pojo)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName()
+ "' as it is self-referential");
}
return;
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName()
+ "' as an exception was thrown when retrieving its value", ex);
}
return;
}
}
super.serializeAsProperty(pojo, jgen, context, writer);
}
}
/**
* {@link SimpleModule} for configuring the serializer.
*/
private static final class ConfigurationPropertiesModule extends SimpleModule {
private ConfigurationPropertiesModule() {
addSerializer(DataSize.class, ToStringSerializer.instance);
}
}
/**
* {@link ValueSerializerModifier} to return only relevant configuration properties.
*/
protected static class GenericSerializerModifier extends ValueSerializerModifier {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription.Supplier beanDesc,
List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
Class<?> beanClass = beanDesc.getType().getRawClass();
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
for (BeanPropertyWriter writer : beanProperties) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);
}
}
return result;
}
private boolean isCandidate(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer,
@Nullable Constructor<?> constructor) {
if (constructor != null) {
Parameter[] parameters = constructor.getParameters();
@Nullable String @Nullable [] names = parameterNameDiscoverer.getParameterNames(constructor);
if (names == null) {
names = new String[parameters.length];
}
for (int i = 0; i < parameters.length; i++) {
String name = MergedAnnotations.from(parameters[i])
.get(Name.class)
.getValue(MergedAnnotation.VALUE, String.class)
.orElse((names[i] != null) ? names[i] : parameters[i].getName());
if (name != null && name.equals(writer.getName())) {
return true;
}
}
}
return isReadable(beanDesc, writer);
}
private boolean isReadable(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer) {
Class<?> parentType = beanDesc.get().getType().getRawClass();
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = findSetter(beanDesc.get(), writer);
// If there's a setter, we assume it's OK to report on the value,
// similarly, if there's no setter but the package names match, we assume
// that it is a nested class used solely for binding to config props, so it
// should be kosher. Lists and Maps are also auto-detected by default since
// that's what the metadata generator does. This filter is not used if there
// is JSON metadata for the property, so it's mainly for user-defined beans.
return (setter != null) || ClassUtils.getPackageName(parentType).equals(ClassUtils.getPackageName(type))
|| Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type);
}
private @Nullable AnnotatedMethod findSetter(BeanDescription beanDesc, BeanPropertyWriter writer) {
String name = "set" + determineAccessorSuffix(writer.getName());
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = beanDesc.findMethod(name, new Class<?>[] { type });
// The enabled property of endpoints returns a boolean primitive but is set
// using a Boolean class
if (setter == null && type.equals(Boolean.TYPE)) {
setter = beanDesc.findMethod(name, new Class<?>[] { Boolean.class });
}
return setter;
}
/**
* Determine the accessor suffix of the specified {@code propertyName}, see
* section 8.8 "Capitalization of inferred names" of the JavaBean specs for more
* details.
* @param propertyName the property name to turn into an accessor suffix
* @return the accessor suffix for {@code propertyName}
*/
private String determineAccessorSuffix(String propertyName) {
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
return propertyName;
}
return StringUtils.capitalize(propertyName);
}
}
}
@@ -0,0 +1,44 @@
/*
* 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.actuate.endpoint.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
/**
* Interface used to supply the Jackson 2 {@link ObjectMapper} that should be used when
* serializing endpoint results.
*
* @author Phillip Webb
* @since 4.0.0
* @see OperationResponseBody
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@FunctionalInterface
@Deprecated(since = "4.0.0", forRemoval = true)
public interface EndpointJackson2ObjectMapper {
/**
* Return the {@link JsonMapper} that should be used to serialize
* {@link OperationResponseBody} endpoint results.
* @return the object mapper
*/
ObjectMapper get();
}
+2 -1
View File
@@ -28,10 +28,11 @@ dependencies {
api(project(":core:spring-boot"))
api("org.springframework.graphql:spring-graphql")
implementation(project(":module:spring-boot-jackson"))
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-http-converter"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
optional(project(":module:spring-boot-micrometer-observation"))
optional(project(":module:spring-boot-rsocket"))
optional(project(":module:spring-boot-security"))
@@ -16,6 +16,7 @@
package org.springframework.boot.graphql.autoconfigure.rsocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.GraphQL;
import io.rsocket.core.RSocketServer;
import reactor.netty.http.server.HttpServer;
@@ -24,12 +25,17 @@ import tools.jackson.databind.json.JsonMapper;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.Encoder;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.GraphQlSource;
@@ -46,7 +52,9 @@ import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHa
* @since 4.0.0
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class,
afterName = "org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration")
afterName = { "org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration",
"org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" })
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
@ConditionalOnProperty("spring.graphql.rsocket.mapping")
@@ -55,9 +63,9 @@ public final class GraphQlRSocketAutoConfiguration {
@Bean
@ConditionalOnMissingBean
GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
ObjectProvider<RSocketGraphQlInterceptor> interceptors, JsonMapper jsonMapper) {
ObjectProvider<RSocketGraphQlInterceptor> interceptors, JsonEncoderSupplier jsonEncoderSupplier) {
return new GraphQlRSocketHandler(graphQlService, interceptors.orderedStream().toList(),
new JacksonJsonEncoder(jsonMapper));
jsonEncoderSupplier.jsonEncoder());
}
@Bean
@@ -66,4 +74,55 @@ public final class GraphQlRSocketAutoConfiguration {
return new GraphQlRSocketController(handler);
}
interface JsonEncoderSupplier {
Encoder<?> jsonEncoder();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(JsonMapper.class)
@ConditionalOnProperty(name = "spring.graphql.rsocket.preferred-json-mapper", havingValue = "jackson",
matchIfMissing = true)
static class JacksonJsonEncoderSupplierConfiguration {
@Bean
JsonEncoderSupplier jacksonJsonEncoderSupplier(JsonMapper jsonMapper) {
return () -> new JacksonJsonEncoder(jsonMapper);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ObjectMapper.class)
@Conditional(NoJacksonOrJackson2Preferred.class)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
static class Jackson2JsonEncoderSupplierConfiguration {
@Bean
JsonEncoderSupplier jackson2JsonEncoderSupplier(ObjectMapper objectMapper) {
return () -> new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper);
}
}
static class NoJacksonOrJackson2Preferred extends AnyNestedCondition {
NoJacksonOrJackson2Preferred() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper")
static class NoJackson {
}
@ConditionalOnProperty(name = "spring.graphql.rsocket.preferred-json-mapper", havingValue = "jackson2")
static class Jackson2Preferred {
}
}
}
@@ -37,6 +37,12 @@
"since": "3.5.0"
}
},
{
"name": "spring.graphql.rsocket.preferred-json-mapper",
"type": "java.lang.String",
"defaultValue": "jackson",
"description": "Preferred JSON mapper to use. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)."
},
{
"name": "spring.graphql.schema.file-extensions",
"defaultValue": ".graphqls,.gqls"
@@ -95,6 +101,22 @@
"name": "any"
}
]
},
{
"name": "spring.graphql.rsocket.preferred-json-mapper",
"values": [
{
"value": "jackson"
},
{
"value": "jackson2"
}
],
"providers": [
{
"name": "any"
}
]
}
]
}
@@ -22,18 +22,22 @@ import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.reactor.netty.NettyRouteProvider;
import org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration;
import org.springframework.boot.rsocket.autoconfigure.RSocketServerAutoConfiguration;
import org.springframework.boot.rsocket.autoconfigure.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
@@ -99,6 +103,33 @@ class GraphQlRSocketAutoConfigurationTests {
testWithRSocketWebSocket(this::assertThatSimpleQueryWorks);
}
@Test
void usesJacksonByDefault() {
this.contextRunner.run((context) -> assertThat(context).hasBean("jacksonJsonEncoderSupplier"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void usesJackson2WhenItIsPreferred() {
this.contextRunner.withPropertyValues("spring.graphql.rsocket.preferred-json-mapper=jackson2")
.withConfiguration(AutoConfigurations
.of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class))
.run((context) -> assertThat(context).hasBean("jackson2JsonEncoderSupplier"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void usesJackson2WhenJacksonIsAbsent() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(Thread.currentThread().getContextClassLoader(), JsonMapper.class))
.withConfiguration(AutoConfigurations
.of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class))
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run((context) -> assertThat(context).hasBean("jackson2JsonEncoderSupplier"));
}
private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) {
String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
String bookName = client.document(document)
@@ -31,6 +31,7 @@ dependencies {
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":core:spring-boot-test"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
optional("org.springframework:spring-webflux")
testImplementation(project(":core:spring-boot-test"))
@@ -16,18 +16,22 @@
package org.springframework.boot.http.codec.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.Nullable;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@@ -43,14 +47,17 @@ import org.springframework.web.reactive.function.client.WebClient;
* {@link org.springframework.core.codec.Decoder Decoders}.
*
* @author Brian Clozel
* @since 2.0.0
* @since 4.0.0
*/
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" })
@ConditionalOnClass({ CodecConfigurer.class, WebClient.class })
public final class CodecsAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnClass(JsonMapper.class)
@ConditionalOnProperty(name = "spring.http.codecs.preferred-json-mapper", havingValue = "jackson",
matchIfMissing = true)
static class JacksonJsonCodecConfiguration {
@Bean
@@ -66,6 +73,26 @@ public final class CodecsAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@Conditional(NoJacksonOrJackson2Preferred.class)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
static class Jackson2JsonCodecConfiguration {
@Bean
@Order(0)
@ConditionalOnBean(ObjectMapper.class)
CodecCustomizer jackson2CodecCustomizer(ObjectMapper objectMapper) {
return (configurer) -> {
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
defaults.jacksonJsonDecoder(new org.springframework.http.codec.json.Jackson2JsonDecoder(objectMapper));
defaults.jacksonJsonEncoder(new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper));
};
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HttpCodecsProperties.class)
static class DefaultCodecsConfiguration {
@@ -104,4 +131,22 @@ public final class CodecsAutoConfiguration {
}
static class NoJacksonOrJackson2Preferred extends AnyNestedCondition {
NoJacksonOrJackson2Preferred() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper")
static class NoJackson {
}
@ConditionalOnProperty(name = "spring.http.codecs.preferred-json-mapper", havingValue = "jackson2")
static class Jackson2Preferred {
}
}
}
@@ -1,4 +1,28 @@
{
"groups": [],
"properties": []
{
"properties": [
{
"name": "spring.http.codecs.preferred-json-mapper",
"type": "java.lang.String",
"defaultValue": "jackson",
"description": "Preferred JSON mapper to use for HTTP encoding and decoding. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)."
}
],
"hints": [
{
"name": "spring.http.codecs.preferred-json-mapper",
"values": [
{
"value": "jackson"
},
{
"value": "jackson2"
}
],
"providers": [
{
"name": "any"
}
]
}
]
}
@@ -18,11 +18,13 @@ package org.springframework.boot.http.codec.autoconfigure;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.http.codec.CodecCustomizer;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@@ -72,7 +74,7 @@ class CodecsAutoConfigurationTests {
}
@Test
void jacksonCodecCustomizerBacksOffWhenThereIsNoObjectMapper() {
void jacksonCodecCustomizerBacksOffWhenThereIsNoJsonMapper() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("jacksonCodecCustomizer"));
}
@@ -82,6 +84,37 @@ class CodecsAutoConfigurationTests {
.run((context) -> assertThat(context).hasBean("jacksonCodecCustomizer"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
void jacksonCodecCustomizerBacksOffWhenJackson2IsPreferred() {
this.contextRunner.withUserConfiguration(JsonMapperConfiguration.class)
.withPropertyValues("spring.http.codecs.preferred-json-mapper=jackson2")
.run((context) -> assertThat(context).doesNotHaveBean("jacksonCodecCustomizer"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
void jackson2CodecCustomizerIsAutoConfiguredWhenObjectMapperIsPresentAndJackson2IsPreferred() {
this.contextRunner.withUserConfiguration(ObjectMapperConfiguration.class)
.withPropertyValues("spring.http.codecs.preferred-json-mapper=jackson2")
.run((context) -> assertThat(context).hasBean("jackson2CodecCustomizer"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
void jackson2CodecCustomizerIsAutoConfiguredWhenObjectMapperIsPresentAndJacksonIsMissing() {
this.contextRunner.withUserConfiguration(ObjectMapperConfiguration.class)
.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName()))
.run((context) -> assertThat(context).hasBean("jackson2CodecCustomizer"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
void jackson2CodecCustomizerBacksOffWhenJackson2IsPreferredButThereIsNoObjectMapper() {
this.contextRunner.withPropertyValues("spring.http.codecs.preferred-json-mapper=jackson2")
.run((context) -> assertThat(context).doesNotHaveBean("jackson2CodecCustomizer"));
}
@Test
void userProvidedCustomizerCanOverrideJacksonCodecCustomizer() {
this.contextRunner.withUserConfiguration(JsonMapperConfiguration.class, CodecCustomizerConfiguration.class)
@@ -116,6 +149,16 @@ class CodecsAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class ObjectMapperConfiguration {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
@Configuration(proxyBeanMethods = false)
static class CodecCustomizerConfiguration {
@@ -33,8 +33,10 @@ dependencies {
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-gson"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
optional(project(":module:spring-boot-jsonb"))
optional(project(":module:spring-boot-kotlin-serialization"))
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
optional("com.google.code.gson:gson")
optional("jakarta.json.bind:jakarta.json.bind-api")
optional("org.springframework:spring-webmvc")
@@ -85,6 +85,12 @@ class GsonHttpMessageConvertersConfiguration {
}
@SuppressWarnings("removal")
@ConditionalOnBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)
static class Jackson2Available {
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "jsonb")
static class JsonbPreferred {
@@ -51,14 +51,17 @@ import org.springframework.http.converter.StringHttpMessageConverter;
* @author Brian Clozel
* @since 4.0.0
*/
@SuppressWarnings("removal")
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration",
"org.springframework.boot.jsonb.autoconfigure.JsonbAutoConfiguration",
"org.springframework.boot.gson.autoconfigure.GsonAutoConfiguration",
"org.springframework.boot.kotlin.serialization.autoconfigure.KotlinSerializationAutoConfiguration" })
@ConditionalOnClass(HttpMessageConverter.class)
@Conditional(NotReactiveWebApplicationCondition.class)
@Import({ JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class,
JsonbHttpMessageConvertersConfiguration.class, KotlinSerializationHttpMessageConvertersConfiguration.class })
@Import({ JacksonHttpMessageConvertersConfiguration.class, Jackson2HttpMessageConvertersConfiguration.class,
GsonHttpMessageConvertersConfiguration.class, JsonbHttpMessageConvertersConfiguration.class,
KotlinSerializationHttpMessageConvertersConfiguration.class })
public final class HttpMessageConvertersAutoConfiguration {
static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
@@ -0,0 +1,93 @@
/*
* 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.http.converter.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
/**
* Configuration for HTTP message converters that use Jackson 2.
*
* @author Andy Wilkinson
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Configuration(proxyBeanMethods = false)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings({ "deprecation", "removal" })
class Jackson2HttpMessageConvertersConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnBean(ObjectMapper.class)
@Conditional(PreferJackson2OrJacksonUnavailableCondition.class)
static class MappingJackson2HttpMessageConverterConfiguration {
@Bean
@ConditionalOnMissingBean
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(
ObjectMapper objectMapper) {
return new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(objectMapper);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(XmlMapper.class)
@ConditionalOnBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
@Bean
@ConditionalOnMissingBean
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder builder) {
return new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build());
}
}
private static class PreferJackson2OrJacksonUnavailableCondition extends AnyNestedCondition {
PreferJackson2OrJacksonUnavailableCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "jackson2")
static class Jackson2Preferred {
}
@ConditionalOnMissingBean(JacksonJsonHttpMessageConverter.class)
static class JacksonUnavailable {
}
}
}
@@ -66,7 +66,10 @@ class JsonbHttpMessageConvertersConfiguration {
}
@ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, GsonHttpMessageConverter.class })
@SuppressWarnings("removal")
@ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class,
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class,
GsonHttpMessageConverter.class })
static class JacksonAndGsonMissing {
}
@@ -4,7 +4,7 @@
"name": "spring.http.converters.preferred-json-mapper",
"type": "java.lang.String",
"defaultValue": "jackson",
"description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'gson', 'jsonb' and 'kotlin-serialization'. When other json mapping libraries are present, use a custom HttpMessageConverters bean to control the preferred mapper."
"description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'jackson2' (deprecated), 'gson', 'jsonb' and 'kotlin-serialization'. When other json mapping libraries are present, use a custom HttpMessageConverters bean to control the preferred mapper."
}
],
"hints": [
@@ -17,6 +17,9 @@
{
"value": "jackson"
},
{
"value": "jackson2"
},
{
"value": "jsonb"
}
@@ -19,18 +19,20 @@ package org.springframework.boot.http.converter.autoconfigure;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import kotlinx.serialization.json.Json;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConverterConfiguration;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -77,7 +79,7 @@ class HttpMessageConvertersAutoConfigurationTests {
@Test
void jacksonNotAvailable() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(ObjectMapper.class);
assertThat(context).doesNotHaveBean(JsonMapper.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonXmlHttpMessageConverter.class);
});
@@ -107,6 +109,34 @@ class HttpMessageConvertersAutoConfigurationTests {
.run(assertConverter(JacksonJsonHttpMessageConverter.class, "customJacksonMessageConverter"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void jackson2DefaultConverter() {
this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class)
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class,
"mappingJackson2HttpMessageConverter"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void jackson2ConverterWithBuilder() {
this.contextRunner.withUserConfiguration(Jackson2ObjectMapperBuilderConfig.class)
.run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class,
"mappingJackson2HttpMessageConverter"));
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void jackson2CustomConverter() {
this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class, Jackson2ConverterConfig.class)
.run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class,
"customJacksonMessageConverter"));
}
@Test
void gsonNotAvailable() {
this.contextRunner.run((context) -> {
@@ -244,8 +274,26 @@ class HttpMessageConvertersAutoConfigurationTests {
}
@Test
void gsonIsPreferredIfJacksonIsNotAvailable() {
allOptionsRunner().withClassLoader(new FilteredClassLoader(ObjectMapper.class.getPackage().getName()))
@SuppressWarnings("removal")
void jackson2IsPreferredIfJacksonIsNotAvailable() {
allOptionsRunner().withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName()))
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run((context) -> {
assertConverterBeanExists(context,
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class,
"mappingJackson2HttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context,
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
});
}
@Test
void gsonIsPreferredIfJacksonAndJackson2AreNotAvailable() {
allOptionsRunner()
.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
ObjectMapper.class.getPackage().getName()))
.run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
@@ -256,8 +304,8 @@ class HttpMessageConvertersAutoConfigurationTests {
@Test
void jsonbIsPreferredIfJacksonAndGsonAreNotAvailable() {
allOptionsRunner()
.withClassLoader(new FilteredClassLoader(ObjectMapper.class.getPackage().getName(),
Gson.class.getPackage().getName()))
.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
ObjectMapper.class.getPackage().getName(), Gson.class.getPackage().getName()))
.run(assertConverter(JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter"));
}
@@ -303,6 +351,7 @@ class HttpMessageConvertersAutoConfigurationTests {
private ApplicationContextRunner allOptionsRunner() {
return this.contextRunner.withBean(Gson.class)
.withBean(JsonMapper.class)
.withBean(ObjectMapper.class, ObjectMapper::new)
.withBean(Jsonb.class, JsonbBuilder::create)
.withBean(Json.class, () -> Json.Default);
}
@@ -403,6 +452,48 @@ class HttpMessageConvertersAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class Jackson2ObjectMapperConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
@Configuration(proxyBeanMethods = false)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
static class Jackson2ObjectMapperBuilderConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder() {
return new org.springframework.http.converter.json.Jackson2ObjectMapperBuilder();
}
}
@Deprecated(since = "4.0.0", forRemoval = true)
@Configuration(proxyBeanMethods = false)
@SuppressWarnings("removal")
static class Jackson2ConverterConfig {
@Bean
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter customJacksonMessageConverter(
ObjectMapper objectMapper) {
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter converter = new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
return converter;
}
}
@Configuration(proxyBeanMethods = false)
static class GsonConverterConfig {
+51
View File
@@ -0,0 +1,51 @@
/*
* 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.
*/
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies"
}
description = "Spring Boot Jackson 2 (deprecated in favor of spring-boot-jackson)"
dependencies {
api(project(":core:spring-boot"))
api("org.springframework:spring-web")
api("com.fasterxml.jackson.core:jackson-databind")
api("com.fasterxml.jackson.datatype:jackson-datatype-jdk8")
api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
api("com.fasterxml.jackson.module:jackson-module-parameter-names")
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":core:spring-boot-test-autoconfigure"))
optional(project(":module:spring-boot-jackson")) {
exclude(group: "tools.jackson.core", module: "jackson-databind")
}
optional("org.springframework:spring-web")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
testRuntimeOnly("ch.qos.logback:logback-classic")
}
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
@@ -0,0 +1,116 @@
/*
* 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.jackson2;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
/**
* {@link Component @Component} that provides {@link JsonSerializer},
* {@link JsonDeserializer} or {@link KeyDeserializer} implementations to be registered
* with Jackson when {@link JsonComponentModule} is in use. Can be used to annotate
* implementations directly or a class that contains them as inner-classes. For example:
* <pre class="code">
* &#064;JsonComponent
* public class CustomerJsonComponent {
*
* public static class Serializer extends JsonSerializer&lt;Customer&gt; {
*
* // ...
*
* }
*
* public static class Deserializer extends JsonDeserializer&lt;Customer&gt; {
*
* // ...
*
* }
*
* }
*
* </pre>
*
* @see JsonComponentModule
* @since 4.0.0
* @author Phillip Webb
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link org.springframework.boot.jackson.JacksonComponent}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Deprecated(since = "4.0.0", forRemoval = true)
public @interface JsonComponent {
/**
* The value may indicate a suggestion for a logical component name, to be turned into
* a Spring bean in case of an autodetected component.
* @return the component name
*/
@AliasFor(annotation = Component.class)
String value() default "";
/**
* The types that are handled by the provided serializer/deserializer. This attribute
* is mandatory for a {@link KeyDeserializer}, as the type cannot be inferred. For a
* {@link JsonSerializer} or {@link JsonDeserializer} it can be used to limit handling
* to a subclasses of type inferred from the generic.
* @return the types that should be handled by the component
*/
Class<?>[] type() default {};
/**
* The scope under which the serializer/deserializer should be registered with the
* module.
* @return the component's handle type
*/
Scope scope() default Scope.VALUES;
/**
* The various scopes under which a serializer/deserializer can be registered.
*/
enum Scope {
/**
* A serializer/deserializer for regular value content.
* @see JsonSerializer
* @see JsonDeserializer
*/
VALUES,
/**
* A serializer/deserializer for keys.
* @see JsonSerializer
* @see KeyDeserializer
*/
KEYS
}
}
@@ -0,0 +1,210 @@
/*
* 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.jackson2;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Spring Bean and Jackson {@link Module} to register {@link JsonComponent @JsonComponent}
* annotated beans.
*
* @author Phillip Webb
* @author Paul Aly
* @since 4.0.0
* @see JsonComponent
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link org.springframework.boot.jackson.JacksonComponentModule}.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public class JsonComponentModule extends SimpleModule implements BeanFactoryAware, InitializingBean {
@SuppressWarnings("NullAway.Init")
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void afterPropertiesSet() {
registerJsonComponents();
}
public void registerJsonComponents() {
BeanFactory beanFactory = this.beanFactory;
while (beanFactory != null) {
if (beanFactory instanceof ListableBeanFactory listableBeanFactory) {
addJsonBeans(listableBeanFactory);
}
beanFactory = (beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory)
? hierarchicalBeanFactory.getParentBeanFactory() : null;
}
}
private void addJsonBeans(ListableBeanFactory beanFactory) {
Map<String, Object> beans = beanFactory.getBeansWithAnnotation(JsonComponent.class);
for (Object bean : beans.values()) {
addJsonBean(bean);
}
}
private void addJsonBean(Object bean) {
MergedAnnotation<JsonComponent> annotation = MergedAnnotations
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY)
.get(JsonComponent.class);
Class<?>[] types = annotation.getClassArray("type");
JsonComponent.Scope scope = annotation.getEnum("scope", JsonComponent.Scope.class);
addJsonBean(bean, types, scope);
}
private void addJsonBean(Object bean, Class<?>[] types, JsonComponent.Scope scope) {
if (bean instanceof JsonSerializer<?> jsonSerializer) {
addJsonSerializerBean(jsonSerializer, scope, types);
}
else if (bean instanceof JsonDeserializer<?> jsonDeserializer) {
addJsonDeserializerBean(jsonDeserializer, types);
}
else if (bean instanceof KeyDeserializer keyDeserializer) {
addKeyDeserializerBean(keyDeserializer, types);
}
for (Class<?> innerClass : bean.getClass().getDeclaredClasses()) {
if (isSuitableInnerClass(innerClass)) {
Object innerInstance = BeanUtils.instantiateClass(innerClass);
addJsonBean(innerInstance, types, scope);
}
}
}
private static boolean isSuitableInnerClass(Class<?> innerClass) {
return !Modifier.isAbstract(innerClass.getModifiers()) && (JsonSerializer.class.isAssignableFrom(innerClass)
|| JsonDeserializer.class.isAssignableFrom(innerClass)
|| KeyDeserializer.class.isAssignableFrom(innerClass));
}
@SuppressWarnings("unchecked")
private <T> void addJsonSerializerBean(JsonSerializer<T> serializer, JsonComponent.Scope scope, Class<?>[] types) {
Class<T> baseType = (Class<T>) ResolvableType.forClass(JsonSerializer.class, serializer.getClass())
.resolveGeneric();
Assert.state(baseType != null, "'baseType' must not be null");
addBeanToModule(serializer, baseType, types,
(scope == JsonComponent.Scope.VALUES) ? this::addSerializer : this::addKeySerializer);
}
@SuppressWarnings("unchecked")
private <T> void addJsonDeserializerBean(JsonDeserializer<T> deserializer, Class<?>[] types) {
Class<T> baseType = (Class<T>) ResolvableType.forClass(JsonDeserializer.class, deserializer.getClass())
.resolveGeneric();
Assert.state(baseType != null, "'baseType' must not be null");
addBeanToModule(deserializer, baseType, types, this::addDeserializer);
}
private void addKeyDeserializerBean(KeyDeserializer deserializer, Class<?>[] types) {
Assert.notEmpty(types, "'types' must not be empty");
addBeanToModule(deserializer, Object.class, types, this::addKeyDeserializer);
}
@SuppressWarnings("unchecked")
private <E, T> void addBeanToModule(E element, Class<T> baseType, Class<?>[] types,
BiConsumer<Class<T>, E> consumer) {
if (ObjectUtils.isEmpty(types)) {
consumer.accept(baseType, element);
return;
}
for (Class<?> type : types) {
Assert.isAssignable(baseType, type);
consumer.accept((Class<T>) type, element);
}
}
static class JsonComponentBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
@Override
public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(
ConfigurableListableBeanFactory beanFactory) {
String[] jsonComponents = beanFactory.getBeanNamesForAnnotation(JsonComponent.class);
Map<Class<?>, List<Class<?>>> innerComponents = new HashMap<>();
for (String jsonComponent : jsonComponents) {
Class<?> type = beanFactory.getType(jsonComponent, true);
Assert.state(type != null, "'type' must not be null");
for (Class<?> declaredClass : type.getDeclaredClasses()) {
if (isSuitableInnerClass(declaredClass)) {
innerComponents.computeIfAbsent(type, (t) -> new ArrayList<>()).add(declaredClass);
}
}
}
return innerComponents.isEmpty() ? null : new JsonComponentAotContribution(innerComponents);
}
}
private static final class JsonComponentAotContribution implements BeanFactoryInitializationAotContribution {
private final Map<Class<?>, List<Class<?>>> innerComponents;
private JsonComponentAotContribution(Map<Class<?>, List<Class<?>>> innerComponents) {
this.innerComponents = innerComponents;
}
@Override
public void applyTo(GenerationContext generationContext,
BeanFactoryInitializationCode beanFactoryInitializationCode) {
ReflectionHints reflection = generationContext.getRuntimeHints().reflection();
this.innerComponents.forEach((outer, inners) -> {
reflection.registerType(outer);
inners.forEach((inner) -> reflection.registerType(inner, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
});
}
}
}
@@ -0,0 +1,61 @@
/*
* 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.jackson2;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Provides a mixin class implementation that registers with Jackson when using
* {@link JsonMixinModule}.
*
* @author Guirong Hu
* @see JsonMixinModule
* @since 4.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link org.springframework.boot.jackson.JacksonMixin}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public @interface JsonMixin {
/**
* Alias for the {@link #type()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @JsonMixin(MyType.class)} instead of
* {@code @JsonMixin(type=MyType.class)}.
* @return the mixed-in classes
*/
@AliasFor("type")
Class<?>[] value() default {};
/**
* The types that are handled by the provided mix-in class. {@link #value()} is an
* alias for (and mutually exclusive with) this attribute.
* @return the mixed-in classes
*/
@AliasFor("value")
Class<?>[] type() default {};
}
@@ -0,0 +1,48 @@
/*
* 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.jackson2;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.jspecify.annotations.Nullable;
/**
* Spring Bean and Jackson {@link Module} to find and
* {@link SimpleModule#setMixInAnnotation(Class, Class) register}
* {@link JsonMixin @JsonMixin}-annotated classes.
*
* @author Guirong Hu
* @author Stephane Nicoll
* @since 4.0.0
* @see JsonMixin
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link org.springframework.boot.jackson.JacksonMixinModule}
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public class JsonMixinModule extends SimpleModule {
/**
* Register the specified {@link JsonMixinModuleEntries entries}.
* @param entries the entries to register to this instance
* @param classLoader the classloader to use
*/
public void registerEntries(JsonMixinModuleEntries entries, @Nullable ClassLoader classLoader) {
entries.doWithEntry(classLoader, this::setMixInAnnotation);
}
}
@@ -0,0 +1,176 @@
/*
* 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.jackson2;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Provide the mapping of json mixin class to consider.
*
* @author Stephane Nicoll
* @since 4.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link JsonMixinModuleEntries}
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public final class JsonMixinModuleEntries {
private final Map<Object, Object> entries;
private JsonMixinModuleEntries(Builder builder) {
this.entries = new LinkedHashMap<>(builder.entries);
}
/**
* Create an instance using the specified {@link Builder}.
* @param mixins a consumer of the builder
* @return an instance with the state of the customized builder.
*/
public static JsonMixinModuleEntries create(Consumer<Builder> mixins) {
Builder builder = new Builder();
mixins.accept(builder);
return builder.build();
}
/**
* Scan the classpath for {@link JsonMixin @JsonMixin} in the specified
* {@code basePackages}.
* @param context the application context to use
* @param basePackages the base packages to consider
* @return an instance with the result of the scanning
*/
public static JsonMixinModuleEntries scan(ApplicationContext context, Collection<String> basePackages) {
return JsonMixinModuleEntries.create((builder) -> {
if (ObjectUtils.isEmpty(basePackages)) {
return;
}
JsonMixinComponentScanner scanner = new JsonMixinComponentScanner();
scanner.setEnvironment(context.getEnvironment());
scanner.setResourceLoader(context);
for (String basePackage : basePackages) {
if (StringUtils.hasText(basePackage)) {
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) {
String beanClassName = candidate.getBeanClassName();
Assert.state(beanClassName != null, "'beanClassName' must not be null");
Class<?> mixinClass = ClassUtils.resolveClassName(beanClassName, context.getClassLoader());
registerMixinClass(builder, mixinClass);
}
}
}
});
}
private static void registerMixinClass(Builder builder, Class<?> mixinClass) {
MergedAnnotation<JsonMixin> annotation = MergedAnnotations
.from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
.get(JsonMixin.class);
Class<?>[] types = annotation.getClassArray("type");
Assert.state(!ObjectUtils.isEmpty(types),
() -> "@JsonMixin annotation on class '" + mixinClass.getName() + "' does not specify any types");
for (Class<?> type : types) {
builder.and(type, mixinClass);
}
}
/**
* Perform an action on each entry defined by this instance. If a class needs to be
* resolved from its class name, the specified {@link ClassLoader} is used.
* @param classLoader the classloader to use to resolve class name if necessary
* @param action the action to invoke on each type to mixin class entry
*/
public void doWithEntry(@Nullable ClassLoader classLoader, BiConsumer<Class<?>, Class<?>> action) {
this.entries.forEach((type, mixin) -> action.accept(resolveClassNameIfNecessary(type, classLoader),
resolveClassNameIfNecessary(mixin, classLoader)));
}
private Class<?> resolveClassNameIfNecessary(Object nameOrType, @Nullable ClassLoader classLoader) {
return (nameOrType instanceof Class<?> type) ? type
: ClassUtils.resolveClassName((String) nameOrType, classLoader);
}
/**
* Builder for {@link JsonMixinModuleEntries}.
*/
public static class Builder {
private final Map<Object, Object> entries;
Builder() {
this.entries = new LinkedHashMap<>();
}
/**
* Add a mapping for the specified class names.
* @param typeClassName the type class name
* @param mixinClassName the mixin class name
* @return {@code this}, to facilitate method chaining
*/
public Builder and(String typeClassName, String mixinClassName) {
this.entries.put(typeClassName, mixinClassName);
return this;
}
/**
* Add a mapping for the specified classes.
* @param type the type class
* @param mixinClass the mixin class
* @return {@code this}, to facilitate method chaining
*/
public Builder and(Class<?> type, Class<?> mixinClass) {
this.entries.put(type, mixinClass);
return this;
}
JsonMixinModuleEntries build() {
return new JsonMixinModuleEntries(this);
}
}
static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider {
JsonMixinComponentScanner() {
addIncludeFilter(new AnnotationTypeFilter(JsonMixin.class));
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return true;
}
}
}
@@ -0,0 +1,118 @@
/*
* 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.jackson2;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.AccessControl;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
/**
* {@link BeanRegistrationAotProcessor} that replaces any {@link JsonMixinModuleEntries}
* by an hard-coded equivalent. This has the effect of disabling scanning at runtime.
*
* @author Stephane Nicoll
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class JsonMixinModuleEntriesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
@Override
public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
if (registeredBean.getBeanClass().equals(JsonMixinModuleEntries.class)) {
return BeanRegistrationAotContribution
.withCustomCodeFragments((codeFragments) -> new AotContribution(codeFragments, registeredBean));
}
return null;
}
static class AotContribution extends BeanRegistrationCodeFragmentsDecorator {
private static final Class<?> BEAN_TYPE = JsonMixinModuleEntries.class;
private final RegisteredBean registeredBean;
private final @Nullable ClassLoader classLoader;
AotContribution(BeanRegistrationCodeFragments delegate, RegisteredBean registeredBean) {
super(delegate);
this.registeredBean = registeredBean;
this.classLoader = registeredBean.getBeanFactory().getBeanClassLoader();
}
@Override
public ClassName getTarget(RegisteredBean registeredBean) {
return ClassName.get(BEAN_TYPE);
}
@Override
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
JsonMixinModuleEntries entries = this.registeredBean.getBeanFactory()
.getBean(this.registeredBean.getBeanName(), JsonMixinModuleEntries.class);
contributeHints(generationContext.getRuntimeHints(), entries);
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> {
method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName());
method.addModifiers(Modifier.PRIVATE, Modifier.STATIC);
method.returns(BEAN_TYPE);
CodeBlock.Builder code = CodeBlock.builder();
code.add("return $T.create(", JsonMixinModuleEntries.class).beginControlFlow("(mixins) ->");
entries.doWithEntry(this.classLoader, (type, mixin) -> addEntryCode(code, type, mixin));
code.endControlFlow(")");
method.addCode(code.build());
});
return generatedMethod.toMethodReference().toCodeBlock();
}
private void addEntryCode(CodeBlock.Builder code, Class<?> type, Class<?> mixin) {
AccessControl accessForTypes = AccessControl.lowest(AccessControl.forClass(type),
AccessControl.forClass(mixin));
if (accessForTypes.isPublic()) {
code.addStatement("$L.and($T.class, $T.class)", "mixins", type, mixin);
}
else {
code.addStatement("$L.and($S, $S)", "mixins", type.getName(), mixin.getName());
}
}
private void contributeHints(RuntimeHints runtimeHints, JsonMixinModuleEntries entries) {
Set<Class<?>> mixins = new LinkedHashSet<>();
entries.doWithEntry(this.classLoader, (type, mixin) -> mixins.add(mixin));
new BindingReflectionHintsRegistrar().registerReflectionHints(runtimeHints.reflection(),
mixins.toArray(Class<?>[]::new));
}
}
}
@@ -0,0 +1,158 @@
/*
* 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.jackson2;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.jackson.ObjectValueDeserializer;
import org.springframework.util.Assert;
/**
* Helper base class for {@link JsonDeserializer} implementations that deserialize
* objects.
*
* @param <T> the supported object type
* @author Phillip Webb
* @since 4.0.0
* @see JsonObjectSerializer
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link ObjectValueDeserializer}
*/
@Deprecated(since = "4.0.0", forRemoval = true)
public abstract class JsonObjectDeserializer<T> extends com.fasterxml.jackson.databind.JsonDeserializer<T> {
@Override
public final T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
try {
ObjectCodec codec = jp.getCodec();
JsonNode tree = codec.readTree(jp);
return deserializeObject(jp, ctxt, codec, tree);
}
catch (Exception ex) {
if (ex instanceof IOException ioException) {
throw ioException;
}
throw new JsonMappingException(jp, "Object deserialize error", ex);
}
}
/**
* Deserialize JSON content into the value type this serializer handles.
* @param jsonParser the source parser used for reading JSON content
* @param context context that can be used to access information about this
* deserialization activity
* @param codec the {@link ObjectCodec} associated with the parser
* @param tree deserialized JSON content as tree expressed using set of
* {@link TreeNode} instances
* @return the deserialized object
* @throws IOException on error
* @see #deserialize(JsonParser, DeserializationContext)
*/
protected abstract T deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) throws IOException;
/**
* Helper method to extract a value from the given {@code jsonNode} or return
* {@code null} when the node itself is {@code null}.
* @param jsonNode the source node (may be {@code null})
* @param type the data type. May be {@link String}, {@link Boolean}, {@link Long},
* {@link Integer}, {@link Short}, {@link Double}, {@link Float}, {@link BigDecimal}
* or {@link BigInteger}.
* @param <D> the data type requested
* @param <R> the result type
* @param mapper a mapper to convert the value when it is not {@code null}
* @return the node value or {@code null}
* @since 3.4.0
*/
protected final <D, R> @Nullable R nullSafeValue(JsonNode jsonNode, Class<D> type, Function<D, R> mapper) {
D value = nullSafeValue(jsonNode, type);
return (value != null) ? mapper.apply(value) : null;
}
/**
* Helper method to extract a value from the given {@code jsonNode} or return
* {@code null} when the node itself is {@code null}.
* @param jsonNode the source node (may be {@code null})
* @param type the data type. May be {@link String}, {@link Boolean}, {@link Long},
* {@link Integer}, {@link Short}, {@link Double}, {@link Float}, {@link BigDecimal}
* or {@link BigInteger}.
* @param <D> the data type requested
* @return the node value or {@code null}
*/
@SuppressWarnings({ "unchecked" })
protected final <D> @Nullable D nullSafeValue(JsonNode jsonNode, Class<D> type) {
Assert.notNull(type, "'type' must not be null");
if (jsonNode == null) {
return null;
}
if (type == String.class) {
return (D) jsonNode.textValue();
}
if (type == Boolean.class) {
return (D) Boolean.valueOf(jsonNode.booleanValue());
}
if (type == Long.class) {
return (D) Long.valueOf(jsonNode.longValue());
}
if (type == Integer.class) {
return (D) Integer.valueOf(jsonNode.intValue());
}
if (type == Short.class) {
return (D) Short.valueOf(jsonNode.shortValue());
}
if (type == Double.class) {
return (D) Double.valueOf(jsonNode.doubleValue());
}
if (type == Float.class) {
return (D) Float.valueOf(jsonNode.floatValue());
}
if (type == BigDecimal.class) {
return (D) jsonNode.decimalValue();
}
if (type == BigInteger.class) {
return (D) jsonNode.bigIntegerValue();
}
throw new IllegalArgumentException("Unsupported value type " + type.getName());
}
/**
* Helper method to return a {@link JsonNode} from the tree.
* @param tree the source tree
* @param fieldName the field name to extract
* @return the {@link JsonNode}
*/
protected final JsonNode getRequiredNode(JsonNode tree, String fieldName) {
Assert.notNull(tree, "'tree' must not be null");
JsonNode node = tree.get(fieldName);
Assert.state(node != null && !(node instanceof NullNode), () -> "Missing JSON field '" + fieldName + "'");
return node;
}
}
@@ -0,0 +1,66 @@
/*
* 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.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.ObjectValueSerializer;
/**
* Helper base class for {@link JsonSerializer} implementations that serialize objects.
*
* @param <T> the supported object type
* @author Phillip Webb
* @since 1.4.0
* @see JsonObjectDeserializer
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3 and
* {@link ObjectValueSerializer}
*/
@Deprecated(since = "4.0.0", forRemoval = true)
public abstract class JsonObjectSerializer<T> extends JsonSerializer<T> {
@Override
public final void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
try {
jgen.writeStartObject();
serializeObject(value, jgen, provider);
jgen.writeEndObject();
}
catch (Exception ex) {
if (ex instanceof IOException ioException) {
throw ioException;
}
throw new JsonMappingException(jgen, "Object serialize error", ex);
}
}
/**
* Serialize JSON content into the value type this serializer handles.
* @param value the source value
* @param jgen the JSON generator
* @param provider the serializer provider
* @throws IOException on error
*/
protected abstract void serializeObject(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException;
}
@@ -0,0 +1,393 @@
/*
* 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.jackson2.autoconfigure;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
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.jspecify.annotations.Nullable;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
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.jackson2.JsonComponentModule;
import org.springframework.boot.jackson2.JsonMixinModule;
import org.springframework.boot.jackson2.JsonMixinModuleEntries;
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.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* Auto configuration for Jackson. The following auto-configuration will get applied:
* <ul>
* <li>an {@link ObjectMapper} in case none is already configured.</li>
* <li>a {@link org.springframework.http.converter.json.Jackson2ObjectMapperBuilder} in
* case none is already configured.</li>
* <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans
* (including the defaulted ones).</li>
* </ul>
*
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Sebastien Deleuze
* @author Johannes Edmeier
* @author Phillip Webb
* @author Eddú Meléndez
* @author Ralf Ueberfuhr
* @since 4.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public final class Jackson2AutoConfiguration {
private static final Map<?, Boolean> FEATURE_DEFAULTS;
static {
Map<Object, Boolean> featureDefaults = new HashMap<>();
featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults);
}
@Bean
JsonComponentModule jackson2JsonComponentModule() {
return new JsonComponentModule();
}
@Configuration(proxyBeanMethods = false)
static class JacksonMixinConfiguration {
@Bean
static JsonMixinModuleEntries jackson2JsonMixinModuleEntries(ApplicationContext context) {
List<String> packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context)
: Collections.emptyList();
return JsonMixinModuleEntries.scan(context, packages);
}
@Bean
JsonMixinModule jackson2JsonMixinModule(ApplicationContext context, JsonMixinModuleEntries entries) {
JsonMixinModule jsonMixinModule = new JsonMixinModule();
jsonMixinModule.registerEntries(entries, context.getClassLoader());
return jsonMixinModule;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jackson2ObjectMapper(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ParameterNamesModule.class)
static class ParameterNamesModuleConfiguration {
@Bean
@ConditionalOnMissingBean
ParameterNamesModule jackson2ParameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(
ApplicationContext applicationContext, List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder = new org.springframework.http.converter.json.Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
private void customize(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(Jackson2Properties.class)
static class Jackson2ObjectMapperBuilderCustomizerConfiguration {
@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJackson2ObjectMapperBuilderCustomizer(
Jackson2Properties jacksonProperties, ObjectProvider<Module> modules) {
return new StandardJackson2ObjectMapperBuilderCustomizer(jacksonProperties, modules.stream().toList());
}
static final class StandardJackson2ObjectMapperBuilderCustomizer
implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
private final Jackson2Properties jacksonProperties;
private final Collection<Module> modules;
StandardJackson2ObjectMapperBuilderCustomizer(Jackson2Properties jacksonProperties,
Collection<Module> modules) {
this.jacksonProperties = jacksonProperties;
this.modules = modules;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
}
if (this.jacksonProperties.getTimeZone() != null) {
builder.timeZone(this.jacksonProperties.getTimeZone());
}
configureFeatures(builder, FEATURE_DEFAULTS);
configureVisibility(builder, this.jacksonProperties.getVisibility());
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureFeatures(builder, this.jacksonProperties.getDatatype().getEnum());
configureFeatures(builder, this.jacksonProperties.getDatatype().getJsonNode());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
}
private void configureFeatures(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder,
Map<?, Boolean> features) {
features.forEach((feature, value) -> {
if (value != null) {
if (value) {
builder.featuresToEnable(feature);
}
else {
builder.featuresToDisable(feature);
}
}
});
}
private void configureVisibility(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder,
Map<PropertyAccessor, JsonAutoDetect.Visibility> visibilities) {
visibilities.forEach(builder::visibility);
}
private void configureDateFormat(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
// We support a fully qualified class name extending DateFormat or a date
// pattern string value
String dateFormat = this.jacksonProperties.getDateFormat();
if (dateFormat != null) {
try {
Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null);
builder.dateFormat((DateFormat) BeanUtils.instantiateClass(dateFormatClass));
}
catch (ClassNotFoundException ex) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
// Since Jackson 2.6.3 we always need to set a TimeZone (see
// gh-4170). If none in our properties fallback to the Jackson's
// default
TimeZone timeZone = this.jacksonProperties.getTimeZone();
if (timeZone == null) {
timeZone = new ObjectMapper().getSerializationConfig().getTimeZone();
}
simpleDateFormat.setTimeZone(timeZone);
builder.dateFormat(simpleDateFormat);
}
}
}
private void configurePropertyNamingStrategy(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
// We support a fully qualified class name extending Jackson's
// PropertyNamingStrategy or a string value corresponding to the constant
// names in PropertyNamingStrategy which hold default provided
// implementations
String strategy = this.jacksonProperties.getPropertyNamingStrategy();
if (strategy != null) {
try {
configurePropertyNamingStrategyClass(builder, ClassUtils.forName(strategy, null));
}
catch (ClassNotFoundException ex) {
configurePropertyNamingStrategyField(builder, strategy);
}
}
}
private void configurePropertyNamingStrategyClass(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder,
Class<?> propertyNamingStrategyClass) {
builder.propertyNamingStrategy(
(PropertyNamingStrategy) BeanUtils.instantiateClass(propertyNamingStrategyClass));
}
private void configurePropertyNamingStrategyField(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder, String fieldName) {
// Find the field (this way we automatically support new constants
// that may be added by Jackson in the future)
Field field = findPropertyNamingStrategyField(fieldName);
Assert.state(field != null, () -> "Constant named '" + fieldName + "' not found");
try {
builder.propertyNamingStrategy((PropertyNamingStrategy) field.get(null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private Field findPropertyNamingStrategyField(String fieldName) {
Field field = ReflectionUtils.findField(com.fasterxml.jackson.databind.PropertyNamingStrategies.class,
fieldName, PropertyNamingStrategy.class);
Assert.state(field != null, () -> "Constant named '" + fieldName + "' not found");
return field;
}
private void configureModules(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
builder.modulesToInstall((modules) -> modules.addAll(this.modules));
}
private void configureLocale(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
Locale locale = this.jacksonProperties.getLocale();
if (locale != null) {
builder.locale(locale);
}
}
private void configureDefaultLeniency(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
Boolean defaultLeniency = this.jacksonProperties.getDefaultLeniency();
if (defaultLeniency != null) {
builder.postConfigurer((objectMapper) -> objectMapper.setDefaultLeniency(defaultLeniency));
}
}
private void configureConstructorDetector(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
org.springframework.boot.jackson2.autoconfigure.Jackson2Properties.ConstructorDetectorStrategy strategy = this.jacksonProperties
.getConstructorDetector();
if (strategy != null) {
builder.postConfigurer((objectMapper) -> {
switch (strategy) {
case USE_PROPERTIES_BASED ->
objectMapper.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED);
case USE_DELEGATING ->
objectMapper.setConstructorDetector(ConstructorDetector.USE_DELEGATING);
case EXPLICIT_ONLY ->
objectMapper.setConstructorDetector(ConstructorDetector.EXPLICIT_ONLY);
default -> objectMapper.setConstructorDetector(ConstructorDetector.DEFAULT);
}
});
}
}
}
}
static class Jackson2AutoConfigurationRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.PropertyNamingStrategy", classLoader)) {
registerPropertyNamingStrategyHints(hints.reflection());
}
}
/**
* Register hints for the {@code configurePropertyNamingStrategyField} method to
* use.
* @param hints reflection hints
*/
private void registerPropertyNamingStrategyHints(ReflectionHints hints) {
registerPropertyNamingStrategyHints(hints, PropertyNamingStrategies.class);
}
private void registerPropertyNamingStrategyHints(ReflectionHints hints, Class<?> type) {
Stream.of(type.getDeclaredFields())
.filter(this::isPropertyNamingStrategyField)
.forEach(hints::registerField);
}
private boolean isPropertyNamingStrategyField(Field candidate) {
return ReflectionUtils.isPublicStaticFinal(candidate)
&& candidate.getType().isAssignableFrom(PropertyNamingStrategy.class);
}
}
}
@@ -0,0 +1,37 @@
/*
* 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.jackson2.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer;
/**
* {@link BackgroundPreinitializer} for Jackson 2.
*
* @author Andy Wilkinson
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
final class Jackson2BackgroundPreinitializer implements BackgroundPreinitializer {
@Override
public void preinitialize() throws Exception {
new ObjectMapper();
}
}
@@ -0,0 +1,42 @@
/*
* 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.jackson2.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Callback interface that can be implemented by beans wishing to further customize the
* {@link ObjectMapper} through
* {@link org.springframework.http.converter.json.Jackson2ObjectMapperBuilder} to
* fine-tune its auto-configuration.
*
* @author Grzegorz Poznachowski
* @since 4.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@FunctionalInterface
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public interface Jackson2ObjectMapperBuilderCustomizer {
/**
* Customize the JacksonObjectMapperBuilder.
* @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
*/
void customize(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);
}
@@ -0,0 +1,272 @@
/*
* 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.jackson2.autoconfigure;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.EnumFeature;
import com.fasterxml.jackson.databind.cfg.JsonNodeFeature;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
/**
* Configuration properties to configure Jackson 2.
*
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Johannes Edmeier
* @author Eddú Meléndez
* @since 4.0.0
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@ConfigurationProperties("spring.jackson2")
public class Jackson2Properties {
/**
* Date format string or a fully-qualified date format class name. For instance,
* 'yyyy-MM-dd HH:mm:ss'.
*/
private @Nullable String dateFormat;
/**
* One of the constants on Jackson's PropertyNamingStrategies. Can also be a
* fully-qualified class name of a PropertyNamingStrategy implementation.
*/
private @Nullable String propertyNamingStrategy;
/**
* Jackson visibility thresholds that can be used to limit which methods (and fields)
* are auto-detected.
*/
private final Map<PropertyAccessor, JsonAutoDetect.Visibility> visibility = new EnumMap<>(PropertyAccessor.class);
/**
* Jackson on/off features that affect the way Java objects are serialized.
*/
private final Map<SerializationFeature, Boolean> serialization = new EnumMap<>(SerializationFeature.class);
/**
* Jackson on/off features that affect the way Java objects are deserialized.
*/
private final Map<DeserializationFeature, Boolean> deserialization = new EnumMap<>(DeserializationFeature.class);
/**
* Jackson general purpose on/off features.
*/
private final Map<MapperFeature, Boolean> mapper = new EnumMap<>(MapperFeature.class);
/**
* Jackson on/off features for parsers.
*/
private final Map<JsonParser.Feature, Boolean> parser = new EnumMap<>(JsonParser.Feature.class);
/**
* Jackson on/off features for generators.
*/
private final Map<JsonGenerator.Feature, Boolean> generator = new EnumMap<>(JsonGenerator.Feature.class);
/**
* Controls the inclusion of properties during serialization. Configured with one of
* the values in Jackson's JsonInclude.Include enumeration.
*/
private JsonInclude.@Nullable Include defaultPropertyInclusion;
/**
* Global default setting (if any) for leniency.
*/
private @Nullable Boolean defaultLeniency;
/**
* Strategy to use to auto-detect constructor, and in particular behavior with
* single-argument constructors.
*/
private @Nullable ConstructorDetectorStrategy constructorDetector;
/**
* Time zone used when formatting dates. For instance, "America/Los_Angeles" or
* "GMT+10".
*/
private @Nullable TimeZone timeZone = null;
/**
* Locale used for formatting.
*/
private @Nullable Locale locale;
private final Datatype datatype = new Datatype();
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public @Nullable String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(@Nullable String dateFormat) {
this.dateFormat = dateFormat;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public @Nullable String getPropertyNamingStrategy() {
return this.propertyNamingStrategy;
}
public void setPropertyNamingStrategy(@Nullable String propertyNamingStrategy) {
this.propertyNamingStrategy = propertyNamingStrategy;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<PropertyAccessor, JsonAutoDetect.Visibility> getVisibility() {
return this.visibility;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<SerializationFeature, Boolean> getSerialization() {
return this.serialization;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<DeserializationFeature, Boolean> getDeserialization() {
return this.deserialization;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<MapperFeature, Boolean> getMapper() {
return this.mapper;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<JsonParser.Feature, Boolean> getParser() {
return this.parser;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<JsonGenerator.Feature, Boolean> getGenerator() {
return this.generator;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public JsonInclude.@Nullable Include getDefaultPropertyInclusion() {
return this.defaultPropertyInclusion;
}
public void setDefaultPropertyInclusion(JsonInclude.@Nullable Include defaultPropertyInclusion) {
this.defaultPropertyInclusion = defaultPropertyInclusion;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public @Nullable Boolean getDefaultLeniency() {
return this.defaultLeniency;
}
public void setDefaultLeniency(@Nullable Boolean defaultLeniency) {
this.defaultLeniency = defaultLeniency;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public @Nullable ConstructorDetectorStrategy getConstructorDetector() {
return this.constructorDetector;
}
public void setConstructorDetector(@Nullable ConstructorDetectorStrategy constructorDetector) {
this.constructorDetector = constructorDetector;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public @Nullable TimeZone getTimeZone() {
return this.timeZone;
}
public void setTimeZone(@Nullable TimeZone timeZone) {
this.timeZone = timeZone;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public @Nullable Locale getLocale() {
return this.locale;
}
public void setLocale(@Nullable Locale locale) {
this.locale = locale;
}
public Datatype getDatatype() {
return this.datatype;
}
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
}
public static class Datatype {
/**
* Jackson on/off features for enums.
*/
private final Map<EnumFeature, Boolean> enumFeatures = new EnumMap<>(EnumFeature.class);
/**
* Jackson on/off features for JsonNodes.
*/
private final Map<JsonNodeFeature, Boolean> jsonNode = new EnumMap<>(JsonNodeFeature.class);
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<EnumFeature, Boolean> getEnum() {
return this.enumFeatures;
}
@DeprecatedConfigurationProperty(reason = "Deprecated in favor of Jackson 3")
public Map<JsonNodeFeature, Boolean> getJsonNode() {
return this.jsonNode;
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.jackson2.autoconfigure;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.test.autoconfigure.TestAutoConfiguration;
import org.springframework.boot.test.autoconfigure.json.ConditionalOnJsonTesters;
import org.springframework.boot.test.autoconfigure.json.JsonMarshalTesterRuntimeHints;
import org.springframework.boot.test.autoconfigure.json.JsonTesterFactoryBean;
import org.springframework.boot.test.json.GsonTester;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.context.annotation.Scope;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link GsonTester}.
*
* @author Phjllip Webb
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@TestAutoConfiguration(after = Jackson2AutoConfiguration.class)
@ConditionalOnJsonTesters
@SuppressWarnings("removal")
final class Jackson2TesterAutoConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnBean(JsonMapper.class)
@ImportRuntimeHints(Jackson2TesterRuntimeHints.class)
FactoryBean<JacksonTester<?>> jacksonTesterFactoryBean(JsonMapper mapper) {
return new JsonTesterFactoryBean<>(JacksonTester.class, mapper);
}
static class Jackson2TesterRuntimeHints extends JsonMarshalTesterRuntimeHints {
Jackson2TesterRuntimeHints() {
super(JacksonTester.class);
}
}
}
@@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Auto-configuration for Jackson 2.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.boot.jackson2.autoconfigure;
@@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Custom enhancements and support for Jackson 2.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.boot.jackson2;
@@ -0,0 +1,12 @@
{
"properties": [
{
"name": "spring.jackson2.constructor-detector",
"defaultValue": "default"
},
{
"name": "spring.jackson2.datatype.enum",
"description": "Jackson on/off features for enums."
}
]
}
@@ -0,0 +1,3 @@
# Background Preinitializers
org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer=\
org.springframework.boot.jackson2.autoconfigure.Jackson2BackgroundPreinitializer
@@ -0,0 +1,8 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration$Jackson2AutoConfigurationRuntimeHints
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.jackson2.JsonComponentModule$JsonComponentBeanFactoryInitializationAotProcessor
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.jackson2.JsonMixinModuleEntriesBeanRegistrationAotProcessor
@@ -0,0 +1 @@
org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration
@@ -0,0 +1 @@
org.springframework.boot.jackson.autoconfigure.Jackson2AutoConfiguration
@@ -0,0 +1 @@
org.springframework.boot.jackson.autoconfigure.Jackson2TesterAutoConfiguration
@@ -0,0 +1,2 @@
org.springframework.boot.jackson2.JsonComponent
com.fasterxml.jackson.databind.Module
@@ -0,0 +1,254 @@
/*
* 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.jackson2;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndAge;
import org.springframework.boot.jackson2.types.NameAndCareer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JsonComponentModule}.
*
* @author Phillip Webb
* @author Vladimir Tsanev
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class JsonComponentModuleTests {
private @Nullable AnnotationConfigApplicationContext context;
@AfterEach
void closeContext() {
if (this.context != null) {
getContext().close();
}
}
@Test
void moduleShouldRegisterSerializers() throws Exception {
load(OnlySerializer.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertSerialize(module);
}
@Test
void moduleShouldRegisterDeserializers() throws Exception {
load(OnlyDeserializer.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertDeserialize(module);
}
@Test
void moduleShouldRegisterInnerClasses() throws Exception {
load(NameAndAgeJsonComponent.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertSerialize(module);
assertDeserialize(module);
}
@Test
void moduleShouldAllowInnerAbstractClasses() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JsonComponentModule.class,
ComponentWithInnerAbstractClass.class);
JsonComponentModule module = context.getBean(JsonComponentModule.class);
assertSerialize(module);
context.close();
}
@Test
void moduleShouldRegisterKeySerializers() throws Exception {
load(OnlyKeySerializer.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertKeySerialize(module);
}
@Test
void moduleShouldRegisterKeyDeserializers() throws Exception {
load(OnlyKeyDeserializer.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertKeyDeserialize(module);
}
@Test
void moduleShouldRegisterInnerClassesForKeyHandlers() throws Exception {
load(NameAndAgeJsonKeyComponent.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertKeySerialize(module);
assertKeyDeserialize(module);
}
@Test
void moduleShouldRegisterOnlyForSpecifiedClasses() throws Exception {
load(NameAndCareerJsonComponent.class);
JsonComponentModule module = getContext().getBean(JsonComponentModule.class);
assertSerialize(module, new NameAndCareer("spring", "developer"), "{\"name\":\"spring\"}");
assertSerialize(module);
assertDeserializeForSpecifiedClasses(module);
}
@Test
void aotContributionRegistersReflectionHintsForSuitableInnerClasses() {
load(ComponentWithInnerAbstractClass.class);
ConfigurableListableBeanFactory beanFactory = getContext().getBeanFactory();
BeanFactoryInitializationAotContribution contribution = new org.springframework.boot.jackson2.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor()
.processAheadOfTime(beanFactory);
TestGenerationContext generationContext = new TestGenerationContext();
assertThat(contribution).isNotNull();
contribution.applyTo(generationContext, mock(BeanFactoryInitializationCode.class));
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
assertThat(RuntimeHintsPredicates.reflection().onType(ComponentWithInnerAbstractClass.class))
.accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(JsonComponentModuleTests.ComponentWithInnerAbstractClass.ConcreteSerializer.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(JsonComponentModuleTests.ComponentWithInnerAbstractClass.AbstractSerializer.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
.negate()).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(JsonComponentModuleTests.ComponentWithInnerAbstractClass.NotSuitable.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
.negate()).accepts(runtimeHints);
}
private void load(Class<?>... configs) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(configs);
context.register(JsonComponentModule.class);
context.refresh();
this.context = context;
}
private void assertSerialize(Module module, Name value, String expectedJson) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(value);
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
}
private void assertSerialize(Module module) throws Exception {
assertSerialize(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}");
}
private void assertDeserialize(Module module) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
NameAndAge nameAndAge = mapper.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class);
assertThat(nameAndAge.getName()).isEqualTo("spring");
assertThat(nameAndAge.getAge()).isEqualTo(100);
}
private void assertDeserializeForSpecifiedClasses(JsonComponentModule module) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
assertThatExceptionOfType(JsonMappingException.class)
.isThrownBy(() -> mapper.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class));
NameAndCareer nameAndCareer = mapper.readValue("{\"name\":\"spring\",\"career\":\"developer\"}",
NameAndCareer.class);
assertThat(nameAndCareer.getName()).isEqualTo("spring");
assertThat(nameAndCareer.getCareer()).isEqualTo("developer");
}
private void assertKeySerialize(Module module) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Map<NameAndAge, Boolean> map = new HashMap<>();
map.put(new NameAndAge("spring", 100), true);
String json = mapper.writeValueAsString(map);
assertThat(json).isEqualToIgnoringWhitespace("{\"spring is 100\": true}");
}
private void assertKeyDeserialize(Module module) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
TypeReference<Map<NameAndAge, Boolean>> typeRef = new TypeReference<>() {
};
Map<NameAndAge, Boolean> map = mapper.readValue("{\"spring is 100\": true}", typeRef);
assertThat(map).containsEntry(new NameAndAge("spring", 100), true);
}
private AnnotationConfigApplicationContext getContext() {
AnnotationConfigApplicationContext context = this.context;
assertThat(context).isNotNull();
return context;
}
@JsonComponent
static class OnlySerializer extends NameAndAgeJsonComponent.Serializer {
}
@JsonComponent
static class OnlyDeserializer extends NameAndAgeJsonComponent.Deserializer {
}
@JsonComponent
static class ComponentWithInnerAbstractClass {
abstract static class AbstractSerializer extends NameAndAgeJsonComponent.Serializer {
}
static class ConcreteSerializer extends AbstractSerializer {
}
static class NotSuitable {
}
}
@JsonComponent(scope = JsonComponent.Scope.KEYS)
static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {
}
@JsonComponent(scope = JsonComponent.Scope.KEYS, type = NameAndAge.class)
static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {
}
}
@@ -0,0 +1,149 @@
/*
* 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.jackson2;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.boot.jackson2.scan.a.RenameMixInClass;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndAge;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.ClassName;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link JsonMixinModuleEntriesBeanRegistrationAotProcessor}.
*
* @author Stephane Nicoll
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@CompileWithForkedClassLoader
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class JsonMixinModuleEntriesBeanRegistrationAotProcessorTests {
private final TestGenerationContext generationContext = new TestGenerationContext();
private final GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
@Test
void processAheadOfTimeShouldRegisterBindingHintsForMixins() {
registerEntries(RenameMixInClass.class);
processAheadOfTime();
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
assertThat(RuntimeHintsPredicates.reflection()
.onType(RenameMixInClass.class)
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
}
@Test
void processAheadOfTimeWhenPublicClassShouldRegisterClass() {
registerEntries(RenameMixInClass.class);
compile((freshContext, compiled) -> {
assertThat(freshContext.getBean(TestConfiguration.class).scanningInvoked).isFalse();
JsonMixinModuleEntries jsonMixinModuleEntries = freshContext.getBean(JsonMixinModuleEntries.class);
assertThat(jsonMixinModuleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.containsExactly(entry(Name.class, RenameMixInClass.class),
entry(NameAndAge.class, RenameMixInClass.class));
});
}
@Test
void processAheadOfTimeWhenNonAccessibleClassShouldRegisterClassName() {
Class<?> privateMixinClass = ClassUtils
.resolveClassName("org.springframework.boot.jackson2.scan.e.PrivateMixInClass", null);
registerEntries(privateMixinClass);
compile((freshContext, compiled) -> {
assertThat(freshContext.getBean(TestConfiguration.class).scanningInvoked).isFalse();
JsonMixinModuleEntries jsonMixinModuleEntries = freshContext.getBean(JsonMixinModuleEntries.class);
assertThat(jsonMixinModuleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.containsExactly(entry(Name.class.getName(), privateMixinClass.getName()),
entry(NameAndAge.class.getName(), privateMixinClass.getName()));
});
}
private ClassName processAheadOfTime() {
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext,
this.generationContext);
this.generationContext.writeGeneratedContent();
return className;
}
@SuppressWarnings("unchecked")
private void compile(BiConsumer<GenericApplicationContext, Compiled> result) {
ClassName className = processAheadOfTime();
TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
.getInstance(ApplicationContextInitializer.class, className.toString());
initializer.initialize(freshApplicationContext);
freshApplicationContext.refresh();
result.accept(freshApplicationContext, compiled);
});
}
private void registerEntries(Class<?>... basePackageClasses) {
List<String> packageNames = Arrays.stream(basePackageClasses).map(Class::getPackageName).toList();
this.applicationContext.registerBeanDefinition("configuration",
BeanDefinitionBuilder.rootBeanDefinition(TestConfiguration.class)
.addConstructorArgValue(packageNames)
.getBeanDefinition());
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
public boolean scanningInvoked;
private final Collection<String> packageNames;
TestConfiguration(Collection<String> packageNames) {
this.packageNames = packageNames;
}
@Bean
JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext applicationContext) {
this.scanningInvoked = true;
return JsonMixinModuleEntries.scan(applicationContext, this.packageNames);
}
}
}
@@ -0,0 +1,128 @@
/*
* 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.jackson2;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.jackson2.scan.a.RenameMixInClass;
import org.springframework.boot.jackson2.scan.b.RenameMixInAbstractClass;
import org.springframework.boot.jackson2.scan.c.RenameMixInInterface;
import org.springframework.boot.jackson2.scan.d.EmptyMixInClass;
import org.springframework.boot.jackson2.scan.f.EmptyMixIn;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndAge;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link JsonMixinModule}.
*
* @author Guirong Hu
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class JsonMixinModuleTests {
private @Nullable AnnotationConfigApplicationContext context;
@AfterEach
void closeContext() {
if (this.context != null) {
getContext().close();
}
}
@Test
void jsonWithModuleEmptyMixInWithEmptyTypesShouldFail() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> load(EmptyMixIn.class))
.withMessageContaining("Error creating bean with name 'jsonMixinModule'")
.withStackTraceContaining("@JsonMixin annotation on class "
+ "'org.springframework.boot.jackson2.scan.f.EmptyMixIn' does not specify any types");
}
@Test
void jsonWithModuleWithRenameMixInClassShouldBeMixedIn() throws Exception {
load(RenameMixInClass.class);
JsonMixinModule module = getContext().getBean(JsonMixinModule.class);
assertMixIn(module, new Name("spring"), "{\"username\":\"spring\"}");
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
}
@Test
void jsonWithModuleWithEmptyMixInClassShouldNotBeMixedIn() throws Exception {
load(EmptyMixInClass.class);
JsonMixinModule module = getContext().getBean(JsonMixinModule.class);
assertMixIn(module, new Name("spring"), "{\"name\":\"spring\"}");
assertMixIn(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}");
}
@Test
void jsonWithModuleWithRenameMixInAbstractClassShouldBeMixedIn() throws Exception {
load(RenameMixInAbstractClass.class);
JsonMixinModule module = getContext().getBean(JsonMixinModule.class);
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
}
@Test
void jsonWithModuleWithRenameMixInInterfaceShouldBeMixedIn() throws Exception {
load(RenameMixInInterface.class);
JsonMixinModule module = getContext().getBean(JsonMixinModule.class);
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
}
private void load(Class<?>... basePackageClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(JsonMixinModule.class, () -> createJsonMixinModule(context, basePackageClasses));
context.refresh();
this.context = context;
}
private JsonMixinModule createJsonMixinModule(AnnotationConfigApplicationContext context,
Class<?>... basePackageClasses) {
List<String> basePackages = Arrays.stream(basePackageClasses).map(ClassUtils::getPackageName).toList();
JsonMixinModuleEntries entries = JsonMixinModuleEntries.scan(context, basePackages);
JsonMixinModule jsonMixinModule = new JsonMixinModule();
jsonMixinModule.registerEntries(entries, context.getClassLoader());
return jsonMixinModule;
}
private void assertMixIn(Module module, Name value, String expectedJson) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(value);
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
}
private AnnotationConfigApplicationContext getContext() {
AnnotationConfigApplicationContext context = this.context;
assertThat(context).isNotNull();
return context;
}
}
@@ -0,0 +1,221 @@
/*
* 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.jackson2;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.NullNode;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JsonObjectDeserializer}.
*
* @author Phillip Webb
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class JsonObjectDeserializerTests {
private final TestJsonObjectDeserializer<Object> testDeserializer = new TestJsonObjectDeserializer<>();
@Test
void deserializeObjectShouldReadJson() throws Exception {
NameAndAgeJsonComponent.Deserializer deserializer = new NameAndAgeJsonComponent.Deserializer();
SimpleModule module = new SimpleModule();
module.addDeserializer(org.springframework.boot.jackson2.types.NameAndAge.class, deserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
org.springframework.boot.jackson2.types.NameAndAge nameAndAge = mapper
.readValue("{\"name\":\"spring\",\"age\":100}", org.springframework.boot.jackson2.types.NameAndAge.class);
assertThat(nameAndAge.getName()).isEqualTo("spring");
assertThat(nameAndAge.getAge()).isEqualTo(100);
}
@Test
void nullSafeValueWhenValueIsNullShouldReturnNull() {
String value = this.testDeserializer.testNullSafeValue(null, String.class);
assertThat(value).isNull();
}
@Test
void nullSafeValueWhenClassIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.testDeserializer.testNullSafeValue(mock(JsonNode.class), null))
.withMessageContaining("'type' must not be null");
}
@Test
void nullSafeValueWhenClassIsStringShouldReturnString() {
JsonNode node = mock(JsonNode.class);
given(node.textValue()).willReturn("abc");
String value = this.testDeserializer.testNullSafeValue(node, String.class);
assertThat(value).isEqualTo("abc");
}
@Test
void nullSafeValueWhenClassIsBooleanShouldReturnBoolean() {
JsonNode node = mock(JsonNode.class);
given(node.booleanValue()).willReturn(true);
Boolean value = this.testDeserializer.testNullSafeValue(node, Boolean.class);
assertThat(value).isTrue();
}
@Test
void nullSafeValueWhenClassIsLongShouldReturnLong() {
JsonNode node = mock(JsonNode.class);
given(node.longValue()).willReturn(10L);
Long value = this.testDeserializer.testNullSafeValue(node, Long.class);
assertThat(value).isEqualTo(10L);
}
@Test
void nullSafeValueWhenClassIsIntegerShouldReturnInteger() {
JsonNode node = mock(JsonNode.class);
given(node.intValue()).willReturn(10);
Integer value = this.testDeserializer.testNullSafeValue(node, Integer.class);
assertThat(value).isEqualTo(10);
}
@Test
void nullSafeValueWhenClassIsShortShouldReturnShort() {
JsonNode node = mock(JsonNode.class);
given(node.shortValue()).willReturn((short) 10);
Short value = this.testDeserializer.testNullSafeValue(node, Short.class);
assertThat(value).isEqualTo((short) 10);
}
@Test
void nullSafeValueWhenClassIsDoubleShouldReturnDouble() {
JsonNode node = mock(JsonNode.class);
given(node.doubleValue()).willReturn(1.1D);
Double value = this.testDeserializer.testNullSafeValue(node, Double.class);
assertThat(value).isEqualTo(1.1D);
}
@Test
void nullSafeValueWhenClassIsFloatShouldReturnFloat() {
JsonNode node = mock(JsonNode.class);
given(node.floatValue()).willReturn(1.1F);
Float value = this.testDeserializer.testNullSafeValue(node, Float.class);
assertThat(value).isEqualTo(1.1F);
}
@Test
void nullSafeValueWhenClassIsBigDecimalShouldReturnBigDecimal() {
JsonNode node = mock(JsonNode.class);
given(node.decimalValue()).willReturn(BigDecimal.TEN);
BigDecimal value = this.testDeserializer.testNullSafeValue(node, BigDecimal.class);
assertThat(value).isEqualTo(BigDecimal.TEN);
}
@Test
void nullSafeValueWhenClassIsBigIntegerShouldReturnBigInteger() {
JsonNode node = mock(JsonNode.class);
given(node.bigIntegerValue()).willReturn(BigInteger.TEN);
BigInteger value = this.testDeserializer.testNullSafeValue(node, BigInteger.class);
assertThat(value).isEqualTo(BigInteger.TEN);
}
@Test
void nullSafeValueWithMapperShouldTransformValue() {
JsonNode node = mock(JsonNode.class);
given(node.textValue()).willReturn("2023-12-01");
LocalDate result = this.testDeserializer.testNullSafeValue(node, String.class, LocalDate::parse);
assertThat(result).isEqualTo(LocalDate.of(2023, 12, 1));
}
@Test
void nullSafeValueWhenClassIsUnknownShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.testDeserializer.testNullSafeValue(mock(JsonNode.class), InputStream.class))
.withMessageContaining("Unsupported value type java.io.InputStream");
}
@Test
void getRequiredNodeWhenTreeIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.testDeserializer.testGetRequiredNode(null, "test"))
.withMessageContaining("'tree' must not be null");
}
@Test
void getRequiredNodeWhenNodeIsNullShouldThrowException() {
JsonNode tree = mock(JsonNode.class);
given(tree.get("test")).willReturn(null);
assertThatIllegalStateException().isThrownBy(() -> this.testDeserializer.testGetRequiredNode(tree, "test"))
.withMessageContaining("Missing JSON field 'test'");
}
@Test
void getRequiredNodeWhenNodeIsNullNodeShouldThrowException() {
JsonNode tree = mock(JsonNode.class);
given(tree.get("test")).willReturn(NullNode.instance);
assertThatIllegalStateException().isThrownBy(() -> this.testDeserializer.testGetRequiredNode(tree, "test"))
.withMessageContaining("Missing JSON field 'test'");
}
@Test
void getRequiredNodeWhenNodeIsFoundShouldReturnNode() {
JsonNode node = mock(JsonNode.class);
given(node.get("test")).willReturn(node);
assertThat(this.testDeserializer.testGetRequiredNode(node, "test")).isEqualTo(node);
}
static class TestJsonObjectDeserializer<T> extends JsonObjectDeserializer<T> {
@Override
@SuppressWarnings("unchecked")
protected T deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) {
return (T) new Object();
}
<D, R> @Nullable R testNullSafeValue(JsonNode jsonNode, Class<D> type, Function<D, R> mapper) {
return nullSafeValue(jsonNode, type, mapper);
}
@SuppressWarnings("NullAway") // Test null check
<D> D testNullSafeValue(@Nullable JsonNode jsonNode, @Nullable Class<D> type) {
return nullSafeValue(jsonNode, type);
}
@SuppressWarnings("NullAway") // Test null check
JsonNode testGetRequiredNode(@Nullable JsonNode tree, String fieldName) {
return getRequiredNode(tree, fieldName);
}
}
}
@@ -0,0 +1,46 @@
/*
* 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.jackson2;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JsonObjectSerializer}.
*
* @author Phillip Webb
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class JsonObjectSerializerTests {
@Test
void serializeObjectShouldWriteJson() throws Exception {
org.springframework.boot.jackson2.NameAndAgeJsonComponent.Serializer serializer = new org.springframework.boot.jackson2.NameAndAgeJsonComponent.Serializer();
SimpleModule module = new SimpleModule();
module.addSerializer(org.springframework.boot.jackson2.types.NameAndAge.class, serializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(new org.springframework.boot.jackson2.types.NameAndAge("spring", 100));
assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}");
}
}
@@ -0,0 +1,67 @@
/*
* 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.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson2.types.NameAndAge;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Sample {@link JsonComponent @JsonComponent} used for tests.
*
* @author Phillip Webb
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
@JsonComponent
public class NameAndAgeJsonComponent {
static class Serializer extends JsonObjectSerializer<NameAndAge> {
@Override
protected void serializeObject(NameAndAge value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStringField("name", value.getName());
jgen.writeNumberField("age", value.getAge());
}
}
static class Deserializer extends JsonObjectDeserializer<NameAndAge> {
@Override
protected NameAndAge deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) throws IOException {
String name = nullSafeValue(tree.get("name"), String.class);
Integer age = nullSafeValue(tree.get("age"), Integer.class);
assertThat(age).isNotNull();
return new NameAndAge(name, age);
}
}
}
@@ -0,0 +1,59 @@
/*
* 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.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson2.types.NameAndAge;
/**
* Sample {@link JsonComponent @JsonComponent} used for tests.
*
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
@JsonComponent(type = NameAndAge.class, scope = JsonComponent.Scope.KEYS)
public class NameAndAgeJsonKeyComponent {
static class Serializer extends JsonSerializer<NameAndAge> {
@Override
public void serialize(NameAndAge value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
jgen.writeFieldName(value.asKey());
}
}
static class Deserializer extends KeyDeserializer {
@Override
public NameAndAge deserializeKey(String key, DeserializationContext ctxt) throws IOException {
String[] keys = key.split("is");
return new NameAndAge(keys[0].trim(), Integer.parseInt(keys[1].trim()));
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndCareer;
/**
* Sample {@link JsonComponent @JsonComponent} used for tests.
*
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
@JsonComponent(type = NameAndCareer.class)
public class NameAndCareerJsonComponent {
static class Serializer extends JsonObjectSerializer<Name> {
@Override
protected void serializeObject(Name value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStringField("name", value.getName());
}
}
static class Deserializer extends JsonObjectDeserializer<Name> {
@Override
protected Name deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) throws IOException {
String name = nullSafeValue(tree.get("name"), String.class);
String career = nullSafeValue(tree.get("career"), String.class);
return new NameAndCareer(name, career);
}
}
}
@@ -0,0 +1,770 @@
/*
* 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.jackson2.autoconfigure;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
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.cfg.EnumFeature;
import com.fasterxml.jackson.databind.cfg.JsonNodeFeature;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.ReflectionHintsPredicates;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.jackson2.JsonComponent;
import org.springframework.boot.jackson2.JsonMixin;
import org.springframework.boot.jackson2.JsonMixinModule;
import org.springframework.boot.jackson2.JsonMixinModuleEntries;
import org.springframework.boot.jackson2.JsonObjectSerializer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
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.annotation.Order;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link Jackson2AutoConfiguration}.
*
* @author Dave Syer
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Sebastien Deleuze
* @author Johannes Edmeier
* @author Grzegorz Poznachowski
* @author Ralf Ueberfuhr
* @author Eddú Meléndez
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class Jackson2AutoConfigurationTests {
protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(Jackson2AutoConfiguration.class));
// TODO reinstate
// @Test
// void doubleModuleRegistration() {
// this.contextRunner.withUserConfiguration(DoubleModulesConfig.class)
// .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
// .run((context) -> {
// ObjectMapper mapper = context.getBean(ObjectMapper.class);
// assertThat(mapper.writeValueAsString(new Foo())).isEqualTo("{\"foo\":\"bar\"}");
// });
// }
@Test
void jsonMixinModuleShouldBeAutoConfiguredWithBasePackages() {
this.contextRunner.withUserConfiguration(MixinConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(JsonMixinModule.class).hasSingleBean(JsonMixinModuleEntries.class);
JsonMixinModuleEntries moduleEntries = context.getBean(JsonMixinModuleEntries.class);
assertThat(moduleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.contains(entry(Person.class, EmptyMixin.class));
});
}
@Test
void noCustomDateFormat() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getDateFormat()).isInstanceOf(StdDateFormat.class);
});
}
@Test
void customDateFormat() {
this.contextRunner.withPropertyValues("spring.jackson2.date-format:yyyyMMddHHmmss").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
DateFormat dateFormat = mapper.getDateFormat();
assertThat(dateFormat).isInstanceOf(SimpleDateFormat.class);
assertThat(((SimpleDateFormat) dateFormat).toPattern()).isEqualTo("yyyyMMddHHmmss");
});
}
@Test
void customDateFormatClass() {
this.contextRunner.withPropertyValues(
"spring.jackson2.date-format:org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfigurationTests.MyDateFormat")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class);
});
}
@Test
void noCustomPropertyNamingStrategy() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getPropertyNamingStrategy()).isNull();
});
}
@Test
void customPropertyNamingStrategyField() {
this.contextRunner.withPropertyValues("spring.jackson2.property-naming-strategy:SNAKE_CASE").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getPropertyNamingStrategy()).isInstanceOf(SnakeCaseStrategy.class);
});
}
@Test
void customPropertyNamingStrategyClass() {
this.contextRunner.withPropertyValues(
"spring.jackson2.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getPropertyNamingStrategy()).isInstanceOf(SnakeCaseStrategy.class);
});
}
@Test
void enableSerializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.serialization.indent_output:true").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(SerializationFeature.INDENT_OUTPUT.enabledByDefault()).isFalse();
assertThat(mapper.getSerializationConfig()
.hasSerializationFeatures(SerializationFeature.INDENT_OUTPUT.getMask())).isTrue();
});
}
@Test
void disableSerializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.serialization.write_dates_as_timestamps:false")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.enabledByDefault()).isTrue();
assertThat(mapper.getSerializationConfig()
.hasSerializationFeatures(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.getMask())).isFalse();
});
}
@Test
void enableDeserializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.deserialization.use_big_decimal_for_floats:true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.enabledByDefault()).isFalse();
assertThat(mapper.getDeserializationConfig()
.hasDeserializationFeatures(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.getMask())).isTrue();
});
}
@Test
void disableDeserializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.deserialization.fail-on-unknown-properties:false")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig()
.hasDeserializationFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.getMask())).isFalse();
});
}
@Test
void enableMapperFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.mapper.require_setters_for_getters:true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.enabledByDefault()).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS))
.isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS))
.isTrue();
});
}
@Test
void disableMapperFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.mapper.use_annotations:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(MapperFeature.USE_ANNOTATIONS.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.USE_ANNOTATIONS)).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(MapperFeature.USE_ANNOTATIONS)).isFalse();
});
}
@Test
void enableParserFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.parser.allow_single_quotes:true").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonParser.Feature.ALLOW_SINGLE_QUOTES.enabledByDefault()).isFalse();
assertThat(mapper.getFactory().isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES)).isTrue();
});
}
@Test
void disableParserFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.parser.auto_close_source:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonParser.Feature.AUTO_CLOSE_SOURCE.enabledByDefault()).isTrue();
assertThat(mapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)).isFalse();
});
}
@Test
void enableGeneratorFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.generator.strict_duplicate_detection:true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
JsonGenerator.Feature feature = JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION;
assertThat(feature.enabledByDefault()).isFalse();
assertThat(mapper.getFactory().isEnabled(feature)).isTrue();
});
}
@Test
void disableGeneratorFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.generator.auto_close_target:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonGenerator.Feature.AUTO_CLOSE_TARGET.enabledByDefault()).isTrue();
assertThat(mapper.getFactory().isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET)).isFalse();
});
}
@Test
void defaultObjectMapperBuilder() {
this.contextRunner.run((context) -> {
Jackson2ObjectMapperBuilder builder = context.getBean(Jackson2ObjectMapperBuilder.class);
ObjectMapper mapper = builder.build();
assertThat(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
assertThat(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES))
.isFalse();
});
}
@Test
void enableEnumFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.datatype.enum.write-enums-to-lowercase=true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(EnumFeature.WRITE_ENUMS_TO_LOWERCASE.enabledByDefault()).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE)).isTrue();
});
}
@Test
void disableJsonNodeFeature() {
this.contextRunner.withPropertyValues("spring.jackson2.datatype.json-node.write-null-properties:false")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonNodeFeature.WRITE_NULL_PROPERTIES.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(JsonNodeFeature.WRITE_NULL_PROPERTIES))
.isFalse();
});
}
@Test
void moduleBeansAndWellKnownModulesAreRegisteredWithTheObjectMapperBuilder() {
this.contextRunner.withUserConfiguration(ModuleConfig.class).run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(context.getBean(CustomModule.class).getOwners()).contains(objectMapper);
assertThat(((DefaultSerializerProvider) objectMapper.getSerializerProviderInstance())
.hasSerializerFor(Baz.class, null)).isTrue();
});
}
@Test
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("module-A", "module-B",
CustomModule.class.getName());
});
}
@Test
void defaultSerializationInclusion() {
this.contextRunner.run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(objectMapper.getSerializationConfig().getDefaultPropertyInclusion().getValueInclusion())
.isEqualTo(JsonInclude.Include.USE_DEFAULTS);
});
}
@Test
void customSerializationInclusion() {
this.contextRunner.withPropertyValues("spring.jackson2.default-property-inclusion:non_null").run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(objectMapper.getSerializationConfig().getDefaultPropertyInclusion().getValueInclusion())
.isEqualTo(JsonInclude.Include.NON_NULL);
});
}
@Test
void customTimeZoneFormattingADate() {
this.contextRunner.withPropertyValues("spring.jackson2.time-zone:GMT+10", "spring.jackson2.date-format:z")
.run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
Date date = new Date(1436966242231L);
assertThat(objectMapper.writeValueAsString(date)).isEqualTo("\"GMT+10:00\"");
});
}
@Test
void enableDefaultLeniency() {
this.contextRunner.withPropertyValues("spring.jackson2.default-leniency:true").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
Person person = mapper.readValue("{\"birthDate\": \"2010-12-30\"}", Person.class);
assertThat(person.getBirthDate()).isNotNull();
});
}
@Test
void disableDefaultLeniency() {
this.contextRunner.withPropertyValues("spring.jackson2.default-leniency:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThatExceptionOfType(InvalidFormatException.class)
.isThrownBy(() -> mapper.readValue("{\"birthDate\": \"2010-12-30\"}", Person.class))
.withMessageContaining("expected format")
.withMessageContaining("yyyyMMdd");
});
}
@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.jackson2.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.jackson2.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.jackson2.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.jackson2.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) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class);
});
}
@Test
void parameterNamesModuleIsAutoConfigured() {
assertParameterNamesModuleCreatorBinding(Mode.DEFAULT, Jackson2AutoConfiguration.class);
}
@Test
void customParameterNamesModuleCanBeConfigured() {
assertParameterNamesModuleCreatorBinding(Mode.DELEGATING, ParameterNamesModuleConfig.class,
Jackson2AutoConfiguration.class);
}
@Test
void writeDurationAsTimestampsDefault() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
Duration duration = Duration.ofHours(2);
assertThat(mapper.writeValueAsString(duration)).isEqualTo("\"PT2H\"");
});
}
@Test
void writeWithVisibility() {
this.contextRunner
.withPropertyValues("spring.jackson2.visibility.getter:none", "spring.jackson2.visibility.field:any")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
String json = mapper.writeValueAsString(new VisibilityBean());
assertThat(json).contains("property1");
assertThat(json).contains("property2");
assertThat(json).doesNotContain("property3");
});
}
@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);
});
}
@Test
void jsonComponentThatInjectsObjectMapperCausesBeanCurrentlyInCreationException() {
this.contextRunner.withUserConfiguration(CircularDependencySerializerConfiguration.class).run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().hasRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
});
}
@Test
void shouldRegisterPropertyNamingStrategyHints() {
shouldRegisterPropertyNamingStrategyHints(PropertyNamingStrategies.class, "LOWER_CAMEL_CASE",
"UPPER_CAMEL_CASE", "SNAKE_CASE", "UPPER_SNAKE_CASE", "LOWER_CASE", "KEBAB_CASE", "LOWER_DOT_CASE");
}
private void shouldRegisterPropertyNamingStrategyHints(Class<?> type, String... fieldNames) {
RuntimeHints hints = new RuntimeHints();
new org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.Jackson2AutoConfigurationRuntimeHints()
.registerHints(hints, getClass().getClassLoader());
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
Stream.of(fieldNames)
.map((name) -> reflection.onField(type, name))
.forEach((predicate) -> assertThat(predicate).accepts(hints));
}
private void assertParameterNamesModuleCreatorBinding(Mode expectedMode, Class<?>... configClasses) {
this.contextRunner.withUserConfiguration(configClasses).run((context) -> {
DeserializationConfig deserializationConfig = context.getBean(ObjectMapper.class)
.getDeserializationConfig();
AnnotationIntrospector annotationIntrospector = deserializationConfig.getAnnotationIntrospector()
.allIntrospectors()
.iterator()
.next();
assertThat(annotationIntrospector).hasFieldOrPropertyWithValue("creatorBinding", expectedMode);
});
}
static class MyDateFormat extends SimpleDateFormat {
MyDateFormat() {
super("yyyy-MM-dd HH:mm:ss");
}
}
@Configuration(proxyBeanMethods = false)
static class MockObjectMapperConfig {
@Bean
@Primary
ObjectMapper objectMapper() {
return mock(ObjectMapper.class);
}
}
@Configuration(proxyBeanMethods = false)
@Import(BazSerializer.class)
static class ModuleConfig {
@Bean
CustomModule jacksonModule() {
return new CustomModule();
}
}
@Configuration
static class DoubleModulesConfig {
@Bean
Module jacksonModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(Foo.class, new JsonSerializer<>() {
@Override
public void serialize(Foo value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeStringField("foo", "bar");
jgen.writeEndObject();
}
});
return module;
}
@Bean
@Primary
ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(jacksonModule());
return mapper;
}
}
@Configuration(proxyBeanMethods = false)
static class ParameterNamesModuleConfig {
@Bean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DELEGATING);
}
}
@Configuration(proxyBeanMethods = false)
static class ObjectMapperBuilderCustomConfig {
@Bean
Jackson2ObjectMapperBuilderCustomizer customDateFormat() {
return (builder) -> builder.dateFormat(new MyDateFormat());
}
}
@Configuration(proxyBeanMethods = false)
static class CustomModuleBuilderCustomizerConfig {
@Bean
@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")));
}
}
@Configuration(proxyBeanMethods = false)
static class ObjectMapperBuilderConsumerConfig {
@Nullable Jackson2ObjectMapperBuilder builderOne;
@Nullable 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 @Nullable String name;
private Foo() {
}
static Foo create() {
return new Foo();
}
public @Nullable String getName() {
return this.name;
}
public void setName(@Nullable String name) {
this.name = name;
}
}
static class Bar {
private @Nullable String propertyName;
@Nullable String getPropertyName() {
return this.propertyName;
}
void setPropertyName(@Nullable String propertyName) {
this.propertyName = propertyName;
}
}
@JsonComponent
static class BazSerializer extends JsonObjectSerializer<Baz> {
@Override
protected void serializeObject(Baz value, JsonGenerator jgen, SerializerProvider provider) {
}
}
static class Baz {
}
static class CustomModule extends SimpleModule {
private final Set<ObjectCodec> owners = new HashSet<>();
@Override
public void setupModule(SetupContext context) {
this.owners.add(context.getOwner());
}
Set<ObjectCodec> getOwners() {
return this.owners;
}
}
@SuppressWarnings("unused")
static class VisibilityBean {
private @Nullable String property1;
public @Nullable String property2;
@Nullable String getProperty3() {
return null;
}
}
static class Person {
@JsonFormat(pattern = "yyyyMMdd")
private @Nullable Date birthDate;
@Nullable Date getBirthDate() {
return this.birthDate;
}
void setBirthDate(@Nullable Date birthDate) {
this.birthDate = birthDate;
}
}
@JsonMixin(type = Person.class)
static class EmptyMixin {
}
@AutoConfigurationPackage
static class MixinConfiguration {
}
@JsonComponent
static class CircularDependencySerializer extends JsonSerializer<String> {
CircularDependencySerializer(ObjectMapper objectMapper) {
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
}
}
@Import(CircularDependencySerializer.class)
@Configuration(proxyBeanMethods = false)
static class CircularDependencySerializerConfiguration {
}
}
@@ -0,0 +1,33 @@
/*
* 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.jackson2.scan.a;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson2.JsonMixin;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndAge;
@JsonMixin(type = { Name.class, NameAndAge.class })
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public abstract class RenameMixInClass {
@JsonProperty("username")
public abstract String getName();
}
@@ -0,0 +1,32 @@
/*
* 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.jackson2.scan.b;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson2.JsonMixin;
import org.springframework.boot.jackson2.types.NameAndAge;
@JsonMixin(type = NameAndAge.class)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public abstract class RenameMixInAbstractClass {
@JsonProperty("username")
abstract String getName();
}
@@ -0,0 +1,32 @@
/*
* 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.jackson2.scan.c;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson2.JsonMixin;
import org.springframework.boot.jackson2.types.NameAndAge;
@JsonMixin(type = NameAndAge.class)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public interface RenameMixInInterface {
@JsonProperty("username")
String getName();
}
@@ -0,0 +1,28 @@
/*
* 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.jackson2.scan.d;
import org.springframework.boot.jackson2.JsonMixin;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndAge;
@JsonMixin(type = { Name.class, NameAndAge.class })
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public class EmptyMixInClass {
}
@@ -0,0 +1,35 @@
/*
* 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.jackson2.scan.e;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson2.JsonMixin;
import org.springframework.boot.jackson2.types.Name;
import org.springframework.boot.jackson2.types.NameAndAge;
@JsonMixin(type = { Name.class, NameAndAge.class })
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
class PrivateMixInClass {
@JsonProperty("username")
String getName() {
return null;
}
}
@@ -0,0 +1,31 @@
/*
* 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.jackson2.scan.f;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson2.JsonMixin;
@JsonMixin
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public interface EmptyMixIn {
@JsonProperty("username")
String getName();
}
@@ -0,0 +1,38 @@
/*
* 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.jackson2.types;
/**
* Sample object used for tests.
*
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
public class Name {
protected final String name;
public Name(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
@@ -0,0 +1,73 @@
/*
* 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.jackson2.types;
import org.springframework.util.ObjectUtils;
/**
* Sample object used for tests.
*
* @author Phillip Webb
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public final class NameAndAge extends Name {
private final int age;
public NameAndAge(String name, int age) {
super(name);
this.age = age;
}
public int getAge() {
return this.age;
}
public String asKey() {
return this.name + " is " + this.age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof NameAndAge other) {
boolean rtn = true;
rtn = rtn && ObjectUtils.nullSafeEquals(this.name, other.name);
rtn = rtn && ObjectUtils.nullSafeEquals(this.age, other.age);
return rtn;
}
return super.equals(obj);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(this.name);
result = prime * result + ObjectUtils.nullSafeHashCode(this.age);
return result;
}
}
@@ -0,0 +1,40 @@
/*
* 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.jackson2.types;
/**
* Sample object used for tests.
*
* @author Paul Aly
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
public class NameAndCareer extends Name {
private final String career;
public NameAndCareer(String name, String career) {
super(name);
this.career = career;
}
public String getCareer() {
return this.career;
}
}
+2
View File
@@ -33,10 +33,12 @@ dependencies {
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
optional(project(":module:spring-boot-reactor-netty"))
optional("io.rsocket:rsocket-transport-netty")
optional("org.springframework:spring-web")
optional("tools.jackson.dataformat:jackson-dataformat-cbor")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
@@ -16,6 +16,8 @@
package org.springframework.boot.rsocket.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import io.netty.buffer.PooledByteBufAllocator;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.dataformat.cbor.CBORMapper;
@@ -23,11 +25,15 @@ import tools.jackson.dataformat.cbor.CBORMapper;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
@@ -45,7 +51,8 @@ import org.springframework.web.util.pattern.PathPatternRouteMatcher;
* @author Brian Clozel
* @since 4.0.0
*/
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" })
@ConditionalOnClass({ io.rsocket.RSocket.class, RSocketStrategies.class, PooledByteBufAllocator.class })
public final class RSocketStrategiesAutoConfiguration {
@@ -63,38 +70,114 @@ public final class RSocketStrategiesAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CBORMapper.class)
@ConditionalOnBean(CBORMapper.class)
protected static class JacksonCborStrategyConfiguration {
@ConditionalOnProperty(name = "spring.rsocket.preferred-mapper", havingValue = "jackson", matchIfMissing = true)
static class JacksonStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR };
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CBORMapper.class)
@ConditionalOnBean(CBORMapper.class)
static class JacksonCborStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR };
@Bean
@Order(0)
RSocketStrategiesCustomizer jacksonCborRSocketStrategyCustomizer(CBORMapper cborMapper) {
return (strategy) -> {
strategy.decoder(new JacksonCborDecoder(cborMapper, SUPPORTED_TYPES));
strategy.encoder(new JacksonCborEncoder(cborMapper, SUPPORTED_TYPES));
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JsonMapper.class)
static class JacksonJsonStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON,
new MediaType("application", "*+json") };
@Bean
@Order(1)
@ConditionalOnBean(JsonMapper.class)
RSocketStrategiesCustomizer jacksonJsonRSocketStrategyCustomizer(JsonMapper jsonMapper) {
return (strategy) -> {
strategy.decoder(new JacksonJsonDecoder(jsonMapper, SUPPORTED_TYPES));
strategy.encoder(new JacksonJsonEncoder(jsonMapper, SUPPORTED_TYPES));
};
}
@Bean
@Order(0)
RSocketStrategiesCustomizer jacksonCborRSocketStrategyCustomizer(CBORMapper cborMapper) {
return (strategy) -> {
strategy.decoder(new JacksonCborDecoder(cborMapper, SUPPORTED_TYPES));
strategy.encoder(new JacksonCborEncoder(cborMapper, SUPPORTED_TYPES));
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JsonMapper.class)
@ConditionalOnBean(JsonMapper.class)
protected static class JacksonJsonStrategyConfiguration {
@Conditional(NoJacksonOrJackson2Preferred.class)
@SuppressWarnings("removal")
@Deprecated(since = "4.0.0", forRemoval = true)
static class Jackson2StrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON,
new MediaType("application", "*+json") };
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ObjectMapper.class, CBORFactory.class })
static class Jackson2CborStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR };
@Bean
@Order(0)
@ConditionalOnBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
RSocketStrategiesCustomizer jackson2CborRSocketStrategyCustomizer(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
return (strategy) -> {
ObjectMapper objectMapper = builder.createXmlMapper(false)
.factory(new com.fasterxml.jackson.dataformat.cbor.CBORFactory())
.build();
strategy.decoder(
new org.springframework.http.codec.cbor.Jackson2CborDecoder(objectMapper, SUPPORTED_TYPES));
strategy.encoder(
new org.springframework.http.codec.cbor.Jackson2CborEncoder(objectMapper, SUPPORTED_TYPES));
};
}
}
@ConditionalOnClass(ObjectMapper.class)
static class Jackson2JsonStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON,
new MediaType("application", "*+json") };
@Bean
@Order(1)
@ConditionalOnBean(ObjectMapper.class)
RSocketStrategiesCustomizer jackson2JsonRSocketStrategyCustomizer(ObjectMapper objectMapper) {
return (strategy) -> {
strategy.decoder(
new org.springframework.http.codec.json.Jackson2JsonDecoder(objectMapper, SUPPORTED_TYPES));
strategy.encoder(
new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper, SUPPORTED_TYPES));
};
}
}
}
static class NoJacksonOrJackson2Preferred extends AnyNestedCondition {
NoJacksonOrJackson2Preferred() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper")
static class NoJackson {
}
@ConditionalOnProperty(name = "spring.rsocket.preferred-mapper", havingValue = "jackson2")
static class Jackson2Preferred {
@Bean
@Order(1)
RSocketStrategiesCustomizer jacksonJsonRSocketStrategyCustomizer(JsonMapper jsonMapper) {
return (strategy) -> {
strategy.decoder(new JacksonJsonDecoder(jsonMapper, SUPPORTED_TYPES));
strategy.encoder(new JacksonJsonEncoder(jsonMapper, SUPPORTED_TYPES));
};
}
}
@@ -1,4 +1,28 @@
{
"properties": [
{
"name": "spring.rsocket.preferred-mapper",
"type": "java.lang.String",
"defaultValue": "jackson",
"description": "Preferred JSON and CBOR mapper to use. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)."
}
],
"hints": [
{
"name": "spring.rsocket.preferred-mapper",
"values": [
{
"value": "jackson"
},
{
"value": "jackson2"
}
],
"providers": [
{
"name": "any"
}
]
}
]
}
}
@@ -22,6 +22,7 @@ import tools.jackson.dataformat.cbor.CBORMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -82,6 +83,44 @@ class RSocketStrategiesAutoConfigurationTests {
});
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void shouldUseJackson2WhenPreferred() {
this.contextRunner
.withConfiguration(AutoConfigurations
.of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class))
.withPropertyValues("spring.rsocket.preferred-mapper=jackson2")
.run((context) -> {
RSocketStrategies strategies = context.getBean(RSocketStrategies.class);
assertThat(strategies.decoders())
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborDecoder.class)
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonDecoder.class);
assertThat(strategies.encoders())
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborEncoder.class)
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonEncoder.class);
});
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void shouldUseJackson2WhenJacksonIsAbsent() {
this.contextRunner
.withConfiguration(AutoConfigurations
.of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(JsonMapper.class, CBORMapper.class))
.run((context) -> {
RSocketStrategies strategies = context.getBean(RSocketStrategies.class);
assertThat(strategies.decoders())
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborDecoder.class)
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonDecoder.class);
assertThat(strategies.encoders())
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborEncoder.class)
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonEncoder.class);
});
}
@Configuration(proxyBeanMethods = false)
static class UserStrategies {
+1
View File
@@ -39,6 +39,7 @@ dependencies {
optional(project(":module:spring-boot-micrometer-observation"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-validation"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("org.springframework.security:spring-security-core")
testFixturesApi(testFixtures(project(":module:spring-boot-actuator")))
@@ -86,6 +86,9 @@ import org.springframework.web.reactive.DispatcherHandler;
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebFluxEndpointManagementContextConfiguration {
private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("removal")
@@ -148,20 +151,26 @@ public class WebFluxEndpointManagementContextConfiguration {
SingletonSupplier.of(endpointJsonMapper::getObject));
}
@Bean
@SuppressWarnings("removal")
@ConditionalOnBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor serverCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
ObjectProvider<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> endpointJsonMapper) {
return new ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
SingletonSupplier.of(endpointJsonMapper::getObject));
}
/**
* {@link BeanPostProcessor} to apply {@link EndpointJsonMapper} for
* {@link OperationResponseBody} to
* {@link org.springframework.http.codec.json.Jackson2JsonEncoder} instances.
* {@link OperationResponseBody} to {@link JacksonJsonEncoder} instances.
*/
static class ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor implements BeanPostProcessor {
private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
private final Supplier<EndpointJsonMapper> mapper;
private final Supplier<EndpointJsonMapper> endpointJsonMapper;
ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor(Supplier<EndpointJsonMapper> endpointJsonMapper) {
this.endpointJsonMapper = endpointJsonMapper;
ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor(Supplier<EndpointJsonMapper> mapper) {
this.mapper = mapper;
}
@Override
@@ -181,10 +190,56 @@ public class WebFluxEndpointManagementContextConfiguration {
}
private void process(Encoder<?> encoder) {
if (encoder instanceof JacksonJsonEncoder jacksonJsonEncoder) {
jacksonJsonEncoder.registerMappersForType(OperationResponseBody.class, (associations) -> {
JsonMapper jsonMapper = this.endpointJsonMapper.get().get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, jsonMapper));
if (encoder instanceof JacksonJsonEncoder jacksonEncoder) {
jacksonEncoder.registerMappersForType(OperationResponseBody.class, (associations) -> {
JsonMapper mapper = this.mapper.get().get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, mapper));
});
}
}
}
/**
* {@link BeanPostProcessor} to apply
* {@link org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper}
* for {@link OperationResponseBody} to
* {@link org.springframework.http.codec.json.Jackson2JsonEncoder} instances.
*
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
static class ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor implements BeanPostProcessor {
private final Supplier<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> mapper;
ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
Supplier<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> mapper) {
this.mapper = mapper;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ServerCodecConfigurer serverCodecConfigurer) {
process(serverCodecConfigurer);
}
return bean;
}
private void process(ServerCodecConfigurer configurer) {
for (HttpMessageWriter<?> writer : configurer.getWriters()) {
if (writer instanceof EncoderHttpMessageWriter<?> encoderHttpMessageWriter) {
process((encoderHttpMessageWriter).getEncoder());
}
}
}
private void process(Encoder<?> encoder) {
if (encoder instanceof org.springframework.http.codec.json.Jackson2JsonEncoder jacksonEncoder) {
jacksonEncoder.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
com.fasterxml.jackson.databind.ObjectMapper mapper = this.mapper.get().get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, mapper));
});
}
}
+1
View File
@@ -43,6 +43,7 @@ dependencies {
optional(project(":module:spring-boot-tomcat"))
optional(project(":module:spring-boot-validation"))
optional(project(":module:spring-boot-web-server"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("io.projectreactor:reactor-core")
testFixturesApi(testFixtures(project(":module:spring-boot-actuator")))
@@ -78,6 +78,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebMvcEndpointManagementContextConfiguration {
private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("removal")
@@ -157,33 +160,77 @@ public class WebMvcEndpointManagementContextConfiguration {
return new EndpointJsonMapperWebMvcConfigurer(endpointJsonMapper);
}
@Bean
@SuppressWarnings("removal")
@ConditionalOnBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static EndpointJackson2ObjectMapperWebMvcConfigurer endpointJackson2ObjectMapperWebMvcConfigurer(
org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper endpointJsonMapper) {
return new EndpointJackson2ObjectMapperWebMvcConfigurer(endpointJsonMapper);
}
/**
* {@link WebMvcConfigurer} to apply {@link EndpointJsonMapper} for
* {@link OperationResponseBody} to {@link JacksonJsonHttpMessageConverter} instances.
*/
static class EndpointJsonMapperWebMvcConfigurer implements WebMvcConfigurer {
private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
private final EndpointJsonMapper mapper;
private final EndpointJsonMapper endpointJsonMapper;
EndpointJsonMapperWebMvcConfigurer(EndpointJsonMapper endpointJsonMapper) {
this.endpointJsonMapper = endpointJsonMapper;
EndpointJsonMapperWebMvcConfigurer(EndpointJsonMapper mapper) {
this.mapper = mapper;
}
@Override
public void configureMessageConverters(ServerBuilder builder) {
builder.configureMessageConverters((converter) -> {
if (converter instanceof JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter) {
configure(jacksonJsonHttpMessageConverter);
if (converter instanceof JacksonJsonHttpMessageConverter jacksonConverter) {
configure(jacksonConverter);
}
});
}
private void configure(JacksonJsonHttpMessageConverter converter) {
converter.registerMappersForType(OperationResponseBody.class, (associations) -> {
JsonMapper jsonMapper = this.endpointJsonMapper.get();
JsonMapper jsonMapper = this.mapper.get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, jsonMapper));
});
}
}
/**
* {@link WebMvcConfigurer} to apply
* {@link org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper}
* for {@link OperationResponseBody} to
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
* instances.
*
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
static class EndpointJackson2ObjectMapperWebMvcConfigurer implements WebMvcConfigurer {
private final org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper mapper;
EndpointJackson2ObjectMapperWebMvcConfigurer(
org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public void configureMessageConverters(ServerBuilder builder) {
builder.configureMessageConverters((converter) -> {
if (converter instanceof org.springframework.http.converter.json.MappingJackson2HttpMessageConverter jacksonConverter) {
configure(jacksonConverter);
}
});
}
private void configure(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter converter) {
converter.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
com.fasterxml.jackson.databind.ObjectMapper jsonMapper = this.mapper.get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, jsonMapper));
});
}
@@ -17,6 +17,7 @@
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies"
}
@@ -30,6 +31,7 @@ dependencies {
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":module:spring-boot-tomcat"))
@@ -43,3 +45,7 @@ dependencies {
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
tasks.named("test") {
jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED"
}
@@ -19,18 +19,23 @@ package org.springframework.boot.websocket.autoconfigure.servlet;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.Nullable;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.boot.LazyInitializationExcludeFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.task.AsyncTaskExecutor;
@@ -53,7 +58,8 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
* @author Moritz Halbritter
* @since 4.0.0
*/
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebSocketMessageBrokerConfigurer.class)
public final class WebSocketMessagingAutoConfiguration {
@@ -62,13 +68,14 @@ public final class WebSocketMessagingAutoConfiguration {
@ConditionalOnBean({ DelegatingWebSocketMessageBrokerConfiguration.class, JsonMapper.class })
@ConditionalOnClass({ JsonMapper.class, AbstractMessageBrokerConfiguration.class })
@Order(0)
static class WebSocketMessageConverterConfiguration implements WebSocketMessageBrokerConfigurer {
static class SpringBootWebSocketMessageBrokerConfiguration implements WebSocketMessageBrokerConfigurer {
private final JsonMapper jsonMapper;
private final @Nullable AsyncTaskExecutor executor;
WebSocketMessageConverterConfiguration(JsonMapper jsonMapper, Map<String, AsyncTaskExecutor> taskExecutors) {
SpringBootWebSocketMessageBrokerConfiguration(JsonMapper jsonMapper,
Map<String, AsyncTaskExecutor> taskExecutors) {
this.jsonMapper = jsonMapper;
this.executor = determineAsyncTaskExecutor(taskExecutors);
}
@@ -83,12 +90,12 @@ public final class WebSocketMessagingAutoConfiguration {
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
messageConverters.add(new StringMessageConverter());
messageConverters.add(new ByteArrayMessageConverter());
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(this.jsonMapper);
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
converter.setContentTypeResolver(resolver);
messageConverters.add(new StringMessageConverter());
messageConverters.add(new ByteArrayMessageConverter());
messageConverters.add(converter);
return false;
}
@@ -114,4 +121,74 @@ public final class WebSocketMessagingAutoConfiguration {
}
@Order(1)
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.websocket.messaging.preferred-json-mapper", havingValue = "jackson",
matchIfMissing = true)
@ConditionalOnClass(JsonMapper.class)
static class JacksonWebSocketMessageConverterConfiguration implements WebSocketMessageBrokerConfigurer {
private final JsonMapper jsonMapper;
JacksonWebSocketMessageConverterConfiguration(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(this.jsonMapper);
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
converter.setContentTypeResolver(resolver);
messageConverters.add(converter);
return false;
}
}
@Order(1)
@Configuration(proxyBeanMethods = false)
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
@Conditional(NoJacksonOrJackson2Preferred.class)
@ConditionalOnClass(ObjectMapper.class)
static class Jackson2WebSocketMessageConverterConfiguration implements WebSocketMessageBrokerConfigurer {
private final ObjectMapper objectMapper;
Jackson2WebSocketMessageConverterConfiguration(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
org.springframework.messaging.converter.MappingJackson2MessageConverter converter = new org.springframework.messaging.converter.MappingJackson2MessageConverter(
this.objectMapper);
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
converter.setContentTypeResolver(resolver);
messageConverters.add(converter);
return false;
}
}
static class NoJacksonOrJackson2Preferred extends AnyNestedCondition {
NoJacksonOrJackson2Preferred() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper")
static class NoJackson {
}
@ConditionalOnProperty(name = "spring.websocket.messaging.preferred-json-mapper", havingValue = "jackson2")
static class Jackson2Preferred {
}
}
}
@@ -0,0 +1,28 @@
{
"properties": [
{
"name": "spring.websocket.messaging.preferred-json-mapper",
"type": "java.lang.String",
"defaultValue": "jackson",
"description": "Preferred JSON mapper to use for WebSocket messaging. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)."
}
],
"hints": [
{
"name": "spring.websocket.messaging.preferred-json-mapper",
"values": [
{
"value": "jackson"
},
{
"value": "jackson2"
}
],
"providers": [
{
"name": "any"
}
]
}
]
}
@@ -27,6 +27,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.tomcat.websocket.WsWebSocketContainer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
@@ -41,14 +42,18 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
import org.springframework.boot.tomcat.autoconfigure.WebSocketTomcatWebServerFactoryCustomizer;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.websocket.autoconfigure.servlet.WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration;
import org.springframework.boot.websocket.autoconfigure.servlet.WebSocketMessagingAutoConfiguration.JacksonWebSocketMessageConverterConfiguration;
import org.springframework.boot.websocket.autoconfigure.servlet.WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@@ -91,6 +96,7 @@ import static org.assertj.core.api.Assertions.fail;
* @author Andy Wilkinson
* @author Lasse Wulff
*/
@DirtiesUrlFactories
class WebSocketMessagingAutoConfigurationTests {
private final AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
@@ -148,7 +154,7 @@ class WebSocketMessagingAutoConfigurationTests {
void predefinedThreadExecutorIsSelectedForInboundChannel() {
AsyncTaskExecutor expectedExecutor = new SimpleAsyncTaskExecutor();
ChannelRegistration registration = new ChannelRegistration();
WebSocketMessageConverterConfiguration configuration = new WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration(
SpringBootWebSocketMessageBrokerConfiguration configuration = new WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration(
new JsonMapper(),
Map.of(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, expectedExecutor));
configuration.configureClientInboundChannel(registration);
@@ -159,7 +165,7 @@ class WebSocketMessagingAutoConfigurationTests {
void predefinedThreadExecutorIsSelectedForOutboundChannel() {
AsyncTaskExecutor expectedExecutor = new SimpleAsyncTaskExecutor();
ChannelRegistration registration = new ChannelRegistration();
WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration configuration = new WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration(
WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration configuration = new WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration(
new JsonMapper(),
Map.of(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, expectedExecutor));
configuration.configureClientOutboundChannel(registration);
@@ -176,19 +182,51 @@ class WebSocketMessagingAutoConfigurationTests {
.getBean(DelegatingWebSocketMessageBrokerConfiguration.class);
CustomHighWebSocketMessageBrokerConfigurer high = this.context
.getBean(CustomHighWebSocketMessageBrokerConfigurer.class);
WebSocketMessageConverterConfiguration autoConfiguration = this.context
.getBean(WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration.class);
SpringBootWebSocketMessageBrokerConfiguration commonAutoConfiguration = this.context
.getBean(WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration.class);
JacksonWebSocketMessageConverterConfiguration jsonMessageConverterAutoConfiguration = this.context
.getBean(WebSocketMessagingAutoConfiguration.JacksonWebSocketMessageConverterConfiguration.class);
WebSocketMessagingConfiguration configuration = this.context.getBean(WebSocketMessagingConfiguration.class);
CustomLowWebSocketMessageBrokerConfigurer low = this.context
.getBean(CustomLowWebSocketMessageBrokerConfigurer.class);
assertThat(delegatingConfiguration).extracting("configurers")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.containsExactly(high, autoConfiguration, configuration, low);
.containsExactly(high, commonAutoConfiguration, jsonMessageConverterAutoConfiguration, configuration, low);
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
void shouldUseJackson2WhenPreferred() {
TestPropertyValues.of("server.port:0", "spring.websocket.messaging.preferred-json-mapper=jackson2")
.applyTo(this.context);
this.context.register(WebSocketMessagingConfiguration.class, CustomLowWebSocketMessageBrokerConfigurer.class,
CustomHighWebSocketMessageBrokerConfigurer.class);
this.context.registerBean(ObjectMapper.class);
this.context.refresh();
assertThat(AssertableApplicationContext.get(() -> this.context)).hasSingleBean(
org.springframework.boot.websocket.autoconfigure.servlet.WebSocketMessagingAutoConfiguration.Jackson2WebSocketMessageConverterConfiguration.class)
.doesNotHaveBean(JacksonWebSocketMessageConverterConfiguration.class);
}
@Test
@Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal")
@ClassPathExclusions("jackson-*-3*")
void shouldUseJackson2WhenJacksonIsMissing() {
TestPropertyValues.of("server.port:0").applyTo(this.context);
this.context.register(WebSocketMessagingConfiguration.class, CustomLowWebSocketMessageBrokerConfigurer.class,
CustomHighWebSocketMessageBrokerConfigurer.class);
this.context.registerBean(ObjectMapper.class);
this.context.refresh();
assertThat(AssertableApplicationContext.get(() -> this.context)).hasSingleBean(
org.springframework.boot.websocket.autoconfigure.servlet.WebSocketMessagingAutoConfiguration.Jackson2WebSocketMessageConverterConfiguration.class)
.doesNotHaveBean(JacksonWebSocketMessageConverterConfiguration.class);
}
private List<MessageConverter> getCustomizedConverters() {
List<MessageConverter> customizedConverters = new ArrayList<>();
WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration configuration = new WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration(
WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration configuration = new WebSocketMessagingAutoConfiguration.SpringBootWebSocketMessageBrokerConfiguration(
new JsonMapper(), Collections.emptyMap());
configuration.configureMessageConverters(customizedConverters);
return customizedConverters;
@@ -2053,6 +2053,7 @@ bom {
"spring-boot-http-converter",
"spring-boot-integration",
"spring-boot-jackson",
"spring-boot-jackson2",
"spring-boot-jarmode-tools",
"spring-boot-jdbc",
"spring-boot-jdbc-test",
+3
View File
@@ -132,6 +132,7 @@ include "module:spring-boot-http-codec"
include "module:spring-boot-http-converter"
include "module:spring-boot-integration"
include "module:spring-boot-jackson"
include "module:spring-boot-jackson2"
include "module:spring-boot-jdbc"
include "module:spring-boot-jdbc-test"
include "module:spring-boot-jetty"
@@ -413,6 +414,8 @@ include ":smoke-test:spring-boot-smoke-test-graphql"
include ":smoke-test:spring-boot-smoke-test-hateoas"
include ":smoke-test:spring-boot-smoke-test-hibernate"
include ":smoke-test:spring-boot-smoke-test-integration"
include ":smoke-test:spring-boot-smoke-test-jackson2-mixed"
include ":smoke-test:spring-boot-smoke-test-jackson2-only"
include ":smoke-test:spring-boot-smoke-test-jetty"
include ":smoke-test:spring-boot-smoke-test-jetty-jsp"
include ":smoke-test:spring-boot-smoke-test-jetty-ssl"
@@ -0,0 +1,30 @@
/*
* 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.
*/
plugins {
id "java"
}
description = "Spring Boot Jackson 2 mixed with Jackson 3 smoke test"
dependencies {
implementation(project(":module:spring-boot-jackson2"))
implementation(project(":starter:spring-boot-starter-actuator"))
implementation(project(":starter:spring-boot-starter-webmvc"))
testImplementation(project(":starter:spring-boot-starter-test"))
testImplementation(project(":starter:spring-boot-starter-webmvc-test"))
}
@@ -0,0 +1,21 @@
/*
* 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 smoketest.jackson2.mixed;
public record Name(String first, String last) {
}
@@ -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 smoketest.jackson2.mixed;
import java.util.List;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
/**
* Names endpoint.
*
* @author Andy Wilkinson
*/
@Component
@Endpoint(id = "names")
public class NamesEndpoint {
@ReadOperation
List<Name> names() {
return List.of(new Name("Spring", "Boot"));
}
}
@@ -0,0 +1,32 @@
/*
* 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 smoketest.jackson2.mixed;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SampleController {
@GetMapping("/names")
List<Name> names() {
return List.of(new Name("Spring", "Boot"));
}
}
@@ -0,0 +1,29 @@
/*
* 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 smoketest.jackson2.mixed;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleJackson2MixedApplication {
public static void main(String[] args) {
SpringApplication.run(SampleJackson2MixedApplication.class, args);
}
}
@@ -0,0 +1,38 @@
/*
* 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 smoketest.jackson2.mixed;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import org.springframework.boot.jackson.JacksonComponent;
@JacksonComponent
public class SampleJacksonComponent {
public static class Serializer extends ValueSerializer<Name> {
@Override
public void serialize(Name value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {
gen.writeString("JACKSON:%s:%s".formatted(value.first(), value.last()));
}
}
}
@@ -0,0 +1,40 @@
/*
* 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 smoketest.jackson2.mixed;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson2.JsonComponent;
@JsonComponent
@SuppressWarnings("removal")
public class SampleJsonComponent {
public static class Serializer extends JsonSerializer<Name> {
@Override
public void serialize(Name value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString("JACKSON2:%s:%s".formatted(value.first(), value.last()));
}
}
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package smoketest.jackson2.mixed;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,2 @@
management.endpoints.web.exposure.include=*
spring.http.converters.preferred-json-mapper=jackson2

Some files were not shown because too many files have changed in this diff Show More