mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-04 08:19:50 +00:00
Restrict Kotlin serialization when alternative is available
This commit applies similar changes already contributed to the `HttpMessageConverter` stack for MVC applications. Since there was no auto-configuration for Kotlinx JSON Serialization on the reactive side, this commit adds a relevant `CodecCustomizer` that will use an available `Json` bean and use it to configure a codec that: * will only consider `@Serializable`-annotated types if another JSON library is available * will use a broader support with Kotlinx Serialization otherwise Fixes gh-48070
This commit is contained in:
@@ -32,6 +32,7 @@ dependencies {
|
||||
optional(project(":core:spring-boot-test"))
|
||||
optional(project(":module:spring-boot-jackson"))
|
||||
optional(project(":module:spring-boot-jackson2"))
|
||||
optional(project(":module:spring-boot-kotlinx-serialization-json"))
|
||||
optional("org.springframework:spring-webflux")
|
||||
|
||||
testImplementation(project(":core:spring-boot-test"))
|
||||
|
||||
+28
@@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.http.codec.autoconfigure;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import kotlinx.serialization.json.Json;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@@ -35,9 +36,13 @@ import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
|
||||
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@@ -93,6 +98,29 @@ public final class CodecsAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(Json.class)
|
||||
static class KotlinxSerializationJsonCodecConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(Json.class)
|
||||
CodecCustomizer kotlinxJsonCodecCustomizer(Json json, ResourceLoader resourceLoader) {
|
||||
ClassLoader classLoader = resourceLoader.getClassLoader();
|
||||
boolean hasAnyJsonSupport = ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader)
|
||||
|| ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|
||||
|| ClassUtils.isPresent("com.google.gson.Gson", classLoader);
|
||||
|
||||
return (configurer) -> {
|
||||
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
|
||||
defaults.kotlinSerializationJsonEncoder(hasAnyJsonSupport ? new KotlinSerializationJsonEncoder(json)
|
||||
: new KotlinSerializationJsonEncoder(json, (type) -> true));
|
||||
defaults.kotlinSerializationJsonDecoder(hasAnyJsonSupport ? new KotlinSerializationJsonDecoder(json)
|
||||
: new KotlinSerializationJsonDecoder(json, (type) -> true));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(HttpCodecsProperties.class)
|
||||
static class DefaultCodecsConfiguration {
|
||||
|
||||
+51
@@ -17,8 +17,10 @@
|
||||
package org.springframework.boot.http.codec.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import kotlinx.serialization.json.Json;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@@ -30,8 +32,13 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.CodecConfigurer.DefaultCodecs;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
|
||||
import org.springframework.http.codec.support.DefaultClientCodecConfigurer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -132,6 +139,40 @@ class CodecsAutoConfigurationTests {
|
||||
1048576));
|
||||
}
|
||||
|
||||
@Test
|
||||
void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() {
|
||||
this.contextRunner.withUserConfiguration(KotlinxJsonConfiguration.class).run((context) -> {
|
||||
KotlinSerializationJsonEncoder encoder = findEncoder(context, KotlinSerializationJsonEncoder.class);
|
||||
assertThat(encoder.canEncode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void kotlinSerializationUsesUnrestrictedPredicateWhenNoOtherJsonConverterIsAvailable() {
|
||||
FilteredClassLoader classLoader = new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
|
||||
ObjectMapper.class.getPackage().getName());
|
||||
this.contextRunner.withClassLoader(classLoader)
|
||||
.withUserConfiguration(KotlinxJsonConfiguration.class)
|
||||
.run((context) -> {
|
||||
KotlinSerializationJsonEncoder encoder = findEncoder(context, KotlinSerializationJsonEncoder.class);
|
||||
assertThat(encoder.canEncode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T findEncoder(AssertableApplicationContext context, Class<T> encoderClass) {
|
||||
ServerCodecConfigurer configurer = ServerCodecConfigurer.create();
|
||||
context.getBeansOfType(CodecCustomizer.class).values().forEach((codec) -> codec.customize(configurer));
|
||||
return (T) configurer.getWriters()
|
||||
.stream()
|
||||
.filter((writer) -> writer instanceof EncoderHttpMessageWriter<?>)
|
||||
.map((writer) -> (EncoderHttpMessageWriter<?>) writer)
|
||||
.map(EncoderHttpMessageWriter::getEncoder)
|
||||
.filter((encoder) -> encoderClass.isAssignableFrom(encoder.getClass()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private DefaultCodecs defaultCodecs(AssertableApplicationContext context) {
|
||||
CodecCustomizer customizer = context.getBean(CodecCustomizer.class);
|
||||
CodecConfigurer configurer = new DefaultClientCodecConfigurer();
|
||||
@@ -159,6 +200,16 @@ class CodecsAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class KotlinxJsonConfiguration {
|
||||
|
||||
@Bean
|
||||
Json kotlinxJson() {
|
||||
return Json.Default;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CodecCustomizerConfiguration {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user