Browse Source

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
pull/48124/head
Brian Clozel 1 month ago
parent
commit
d161aafcdf
  1. 1
      module/spring-boot-http-codec/build.gradle
  2. 28
      module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java
  3. 51
      module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java

1
module/spring-boot-http-codec/build.gradle

@ -32,6 +32,7 @@ dependencies { @@ -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
module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java

@ -17,6 +17,7 @@ @@ -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; @@ -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 { @@ -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
module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java

@ -17,8 +17,10 @@ @@ -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; @@ -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 { @@ -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 { @@ -159,6 +200,16 @@ class CodecsAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class KotlinxJsonConfiguration {
@Bean
Json kotlinxJson() {
return Json.Default;
}
}
@Configuration(proxyBeanMethods = false)
static class CodecCustomizerConfiguration {

Loading…
Cancel
Save