mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-01 23:09:01 +00:00
Add Jackson 2 modules and support
Closes gh-47688
This commit is contained in:
+1
@@ -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");
|
||||
|
||||
+2
@@ -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}
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
+207
@@ -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<ExampleObject> json;
|
||||
*
|
||||
* @Before
|
||||
* public void setup() {
|
||||
* ObjectMapper objectMapper = new ObjectMapper();
|
||||
* Jackson2Tester.initFields(this, objectMapper);
|
||||
* }
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -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);
|
||||
|
||||
+92
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+28
-4
@@ -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
|
||||
|
||||
|
||||
+1
@@ -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")
|
||||
|
||||
+1
-1
@@ -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"))
|
||||
|
||||
+55
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+4
-1
@@ -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();
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
+34
@@ -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);
|
||||
|
||||
}
|
||||
+21
-255
@@ -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.
|
||||
|
||||
+284
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+275
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
+62
-3
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+31
@@ -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"))
|
||||
|
||||
+49
-4
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+27
-3
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+44
-1
@@ -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")
|
||||
|
||||
+6
@@ -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 {
|
||||
|
||||
+5
-2
@@ -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";
|
||||
|
||||
+93
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+4
-1
@@ -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
-1
@@ -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"
|
||||
}
|
||||
|
||||
+97
-6
@@ -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 {
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+116
@@ -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">
|
||||
* @JsonComponent
|
||||
* public class CustomerJsonComponent {
|
||||
*
|
||||
* public static class Serializer extends JsonSerializer<Customer> {
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* }
|
||||
*
|
||||
* public static class Deserializer extends JsonDeserializer<Customer> {
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* }
|
||||
*
|
||||
* }
|
||||
*
|
||||
* </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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+210
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -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 {};
|
||||
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+176
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+158
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -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;
|
||||
|
||||
}
|
||||
+393
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -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);
|
||||
|
||||
}
|
||||
+272
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
+21
@@ -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;
|
||||
+12
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.springframework.boot.jackson.autoconfigure.Jackson2AutoConfiguration
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.springframework.boot.jackson.autoconfigure.Jackson2TesterAutoConfiguration
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.jackson2.JsonComponent
|
||||
com.fasterxml.jackson.databind.Module
|
||||
+254
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+149
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+128
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+221
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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}");
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+770
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+33
@@ -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();
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
|
||||
}
|
||||
+28
@@ -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 {
|
||||
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -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();
|
||||
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
+107
-24
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+25
-1
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -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 {
|
||||
|
||||
|
||||
@@ -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")))
|
||||
|
||||
+67
-12
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")))
|
||||
|
||||
+56
-9
@@ -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"
|
||||
}
|
||||
|
||||
+82
-5
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+45
-7
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
+21
@@ -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) {
|
||||
|
||||
}
|
||||
+39
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -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;
|
||||
+2
@@ -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
Reference in New Issue
Block a user