Browse Source

Revisit Kotlin Serialization integration

This commit removes the "kotlin-serialization" option from the
`spring.http.converters.preferred-json-mapper` and configures the kotlin
serialization http message converter ahead of the preferred JSON
converter.

This effectively makes Kotlin Serialization a converter that is
considered first for JSON support, and then Jackson/Jsonb/Gson is
considered as fallback.

Closes gh-47178
pull/47203/head
Brian Clozel 3 months ago
parent
commit
a6cf0365ce
  1. 18
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java
  2. 35
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java
  3. 48
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java
  4. 3
      module/spring-boot-http-converter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
  5. 34
      module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java

18
module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java

@ -42,7 +42,7 @@ class GsonHttpMessageConvertersConfiguration { @@ -42,7 +42,7 @@ class GsonHttpMessageConvertersConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(Gson.class)
@Conditional(PreferGsonOrOtherJsonLibraryUnavailableCondition.class)
@Conditional(PreferGsonOrJacksonAndJsonbUnavailableCondition.class)
static class GsonHttpMessageConverterConfiguration {
@Bean
@ -55,9 +55,9 @@ class GsonHttpMessageConvertersConfiguration { @@ -55,9 +55,9 @@ class GsonHttpMessageConvertersConfiguration {
}
private static class PreferGsonOrOtherJsonLibraryUnavailableCondition extends AnyNestedCondition {
private static class PreferGsonOrJacksonAndJsonbUnavailableCondition extends AnyNestedCondition {
PreferGsonOrOtherJsonLibraryUnavailableCondition() {
PreferGsonOrJacksonAndJsonbUnavailableCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ -67,16 +67,16 @@ class GsonHttpMessageConvertersConfiguration { @@ -67,16 +67,16 @@ class GsonHttpMessageConvertersConfiguration {
}
@Conditional(OtherJsonLibrariesUnavailableCondition.class)
@Conditional(JacksonAndJsonbUnavailableCondition.class)
static class JacksonJsonbUnavailable {
}
}
private static class OtherJsonLibrariesUnavailableCondition extends NoneNestedConditions {
private static class JacksonAndJsonbUnavailableCondition extends NoneNestedConditions {
OtherJsonLibrariesUnavailableCondition() {
JacksonAndJsonbUnavailableCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ -91,12 +91,6 @@ class GsonHttpMessageConvertersConfiguration { @@ -91,12 +91,6 @@ class GsonHttpMessageConvertersConfiguration {
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "kotlin-serialization")
static class KotlinSerializationPreferred {
}
}
}

35
module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java

@ -23,7 +23,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -23,7 +23,6 @@ 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.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -42,7 +41,7 @@ class JsonbHttpMessageConvertersConfiguration { @@ -42,7 +41,7 @@ class JsonbHttpMessageConvertersConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(Jsonb.class)
@Conditional(PreferJsonbOrOtherJsonLibrariesMissingCondition.class)
@Conditional(PreferJsonbOrMissingJacksonAndGsonCondition.class)
static class JsonbHttpMessageConverterConfiguration {
@Bean
@ -55,9 +54,9 @@ class JsonbHttpMessageConvertersConfiguration { @@ -55,9 +54,9 @@ class JsonbHttpMessageConvertersConfiguration {
}
private static class PreferJsonbOrOtherJsonLibrariesMissingCondition extends AnyNestedCondition {
private static class PreferJsonbOrMissingJacksonAndGsonCondition extends AnyNestedCondition {
PreferJsonbOrOtherJsonLibrariesMissingCondition() {
PreferJsonbOrMissingJacksonAndGsonCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ -67,32 +66,8 @@ class JsonbHttpMessageConvertersConfiguration { @@ -67,32 +66,8 @@ class JsonbHttpMessageConvertersConfiguration {
}
@Conditional(OtherJsonLibrariesMissingMissingCondition.class)
static class OtherJsonLibrariesMissingMissing {
}
}
private static class OtherJsonLibrariesMissingMissingCondition extends NoneNestedConditions {
OtherJsonLibrariesMissingMissingCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnBean(JacksonJsonHttpMessageConverter.class)
static class JacksonAvailable {
}
@ConditionalOnBean(GsonHttpMessageConverter.class)
static class GsonAvailable {
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "kotlin-serialization")
static class KotlinPreferred {
@ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, GsonHttpMessageConverter.class })
static class JacksonAndGsonMissing {
}

48
module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java

@ -16,61 +16,33 @@ @@ -16,61 +16,33 @@
package org.springframework.boot.http.converter.autoconfigure;
import kotlinx.serialization.Serializable;
import kotlinx.serialization.json.Json;
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.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
/**
* Configuration for HTTP message converters that use Kotlin Serialization.
*
* @author Brian Clozel
* @author Dmitry Sulman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Json.class)
@ConditionalOnClass({ Serializable.class, Json.class })
@ConditionalOnBean(Json.class)
class KotlinSerializationHttpMessageConvertersConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(Json.class)
@Conditional(PreferKotlinSerializationOrOtherJsonLibrariesUnavailableCondition.class)
static class KotlinSerializationHttpMessageConverterConfiguration {
@Bean
@ConditionalOnMissingBean
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json) {
return new KotlinSerializationJsonHttpMessageConverter(json);
}
}
private static class PreferKotlinSerializationOrOtherJsonLibrariesUnavailableCondition extends AnyNestedCondition {
PreferKotlinSerializationOrOtherJsonLibrariesUnavailableCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "kotlin-serialization")
static class KotlinSerializationPreferred {
}
@ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, JsonbHttpMessageConverter.class,
GsonHttpMessageConverter.class })
static class OtherJsonLibrariesUnavailable {
}
@Bean
@ConditionalOnMissingBean
@Order(-10) // configured ahead of JSON mappers
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json) {
return new KotlinSerializationJsonHttpMessageConverter(json);
}
}

3
module/spring-boot-http-converter/src/main/resources/META-INF/additional-spring-configuration-metadata.json

@ -19,9 +19,6 @@ @@ -19,9 +19,6 @@
},
{
"value": "jsonb"
},
{
"value": "kotlin-serialization"
}
],
"providers": [

34
module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package org.springframework.boot.http.converter.autoconfigure;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.google.gson.Gson;
import jakarta.json.bind.Jsonb;
@ -131,7 +132,6 @@ class HttpMessageConvertersAutoConfigurationTests { @@ -131,7 +132,6 @@ class HttpMessageConvertersAutoConfigurationTests {
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}
@ -163,7 +163,6 @@ class HttpMessageConvertersAutoConfigurationTests { @@ -163,7 +163,6 @@ class HttpMessageConvertersAutoConfigurationTests {
assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}
@ -184,17 +183,15 @@ class HttpMessageConvertersAutoConfigurationTests { @@ -184,17 +183,15 @@ class HttpMessageConvertersAutoConfigurationTests {
}
@Test
void kotlinSerializationCanBePreferred() {
allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:kotlin-serialization")
.run((context) -> {
assertConverterBeanExists(context, KotlinSerializationJsonHttpMessageConverter.class,
"kotlinSerializationJsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context,
KotlinSerializationJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
});
void kotlinSerializationOrderedAheadOfJsonConverter() {
allOptionsRunner().run((context) -> {
assertConverterBeanExists(context, KotlinSerializationJsonHttpMessageConverter.class,
"kotlinSerializationJsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context,
KotlinSerializationJsonHttpMessageConverter.class);
assertConvertersBeanRegisteredWithHttpMessageConverters(context,
List.of(KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class));
});
}
@Test
@ -240,7 +237,6 @@ class HttpMessageConvertersAutoConfigurationTests { @@ -240,7 +237,6 @@ class HttpMessageConvertersAutoConfigurationTests {
assertConverterBeanRegisteredWithHttpMessageConverters(context, JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}
@ -251,7 +247,6 @@ class HttpMessageConvertersAutoConfigurationTests { @@ -251,7 +247,6 @@ class HttpMessageConvertersAutoConfigurationTests {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}
@ -327,6 +322,15 @@ class HttpMessageConvertersAutoConfigurationTests { @@ -327,6 +322,15 @@ class HttpMessageConvertersAutoConfigurationTests {
assertThat(converters.getConverters()).contains(converter);
}
private void assertConvertersBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context,
List<Class<? extends HttpMessageConverter<?>>> types) {
List<? extends HttpMessageConverter<?>> converterInstances = types.stream().map(context::getBean).toList();
HttpMessageConverters converters = context.getBean(HttpMessageConverters.class);
assertThat(converters.getConverters()).containsSubsequence(converterInstances);
}
@Configuration(proxyBeanMethods = false)
static class JacksonJsonMapperConfig {

Loading…
Cancel
Save