From 50f64f947a3d76116c3a0d702f40d7670b83e858 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Dec 2025 15:17:31 +0100 Subject: [PATCH] Separate client and server HttpMessageConverters Prior to this commit, the `HttpMessageConverters` auto-configuration would pick up `HttpMessageConverter` beans from the context and broadly apply them to both server and client converters setup. This can cause several types of problems. First, specific configurations only meant for server setup will also be applied to the client side. For example, the Actuator JSOn configuration is only meant to be applied to the server infrastructure. Also, picking up converters from the context does not convey whether such converters are meant to override the default ones or should be configured as custom, in addition to the defaults. For example, a bean extending `JacksonJsonHttpMessageConverter` can be both meant to override the default with `builder.withJsonConverter` or meant as an additional converter with `builder.addCustomConverter`. This commit ensures that the auto-configurations contribute `ClientHttpMessageConvertersCustomizer` and `ServerHttpMessageConvertersCustomizer` beans instead of converter beans directly. Applications can still contribute such beans and those will be used. Fixes gh-48310 --- .../GraphQlWebMvcAutoConfiguration.java | 23 +- ...ClientHttpMessageConvertersCustomizer.java | 22 +- ...ServerHttpMessageConvertersCustomizer.java | 22 +- ...sonHttpMessageConvertersConfiguration.java | 37 ++- ...ttpMessageConvertersAutoConfiguration.java | 35 ++- ...on2HttpMessageConvertersConfiguration.java | 69 +++- ...sonHttpMessageConvertersConfiguration.java | 57 +++- ...onbHttpMessageConvertersConfiguration.java | 41 ++- ...ionHttpMessageConvertersConfiguration.java | 47 ++- ...ssageConvertersAutoConfigurationTests.java | 296 ++++++++++-------- ...sAutoConfigurationWithoutJacksonTests.java | 5 +- 11 files changed, 417 insertions(+), 237 deletions(-) diff --git a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java index be3bc347045..f1e75dfcc31 100644 --- a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java +++ b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration; import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties; import org.springframework.boot.graphql.autoconfigure.GraphQlProperties; +import org.springframework.boot.http.converter.autoconfigure.ServerHttpMessageConvertersCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; @@ -60,6 +61,8 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.util.Assert; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerMapping; @@ -183,17 +186,21 @@ public final class GraphQlWebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler, - GraphQlProperties properties, ObjectProvider> converters) { - return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters), + GraphQlProperties properties, ObjectProvider customizers) { + return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(customizers), properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive()); } - private HttpMessageConverter getJsonConverter(ObjectProvider> converters) { - return converters.orderedStream() - .filter(this::canReadJsonMap) - .findFirst() - .map(this::asObjectHttpMessageConverter) - .orElseThrow(() -> new IllegalStateException("No JSON converter")); + private HttpMessageConverter getJsonConverter( + ObjectProvider customizers) { + ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults(); + customizers.forEach((customizer) -> customizer.customize(serverBuilder)); + for (HttpMessageConverter converter : serverBuilder.build()) { + if (canReadJsonMap(converter)) { + return asObjectHttpMessageConverter(converter); + } + } + throw new IllegalStateException("No JSON converter"); } private boolean canReadJsonMap(HttpMessageConverter candidate) { diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java index ed1185a184c..db03383e16c 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java @@ -20,10 +20,8 @@ import java.util.Collection; import org.jspecify.annotations.Nullable; -import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; -import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; @SuppressWarnings("deprecation") @@ -48,18 +46,9 @@ class DefaultClientHttpMessageConvertersCustomizer implements ClientHttpMessageC else { builder.registerDefaults(); this.converters.forEach((converter) -> { - if (converter instanceof StringHttpMessageConverter) { - builder.withStringConverter(converter); - } - else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { + if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { builder.withKotlinSerializationJsonConverter(converter); } - else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) { - builder.withJsonConverter(converter); - } - else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) { - builder.withXmlConverter(converter); - } else { builder.addCustomConverter(converter); } @@ -67,13 +56,4 @@ class DefaultClientHttpMessageConvertersCustomizer implements ClientHttpMessageC } } - private static boolean supportsMediaType(HttpMessageConverter converter, MediaType mediaType) { - for (MediaType supportedMediaType : converter.getSupportedMediaTypes()) { - if (supportedMediaType.equalsTypeAndSubtype(mediaType)) { - return true; - } - } - return false; - } - } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java index 2f4eed708cb..cab0bcd6640 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java @@ -20,10 +20,8 @@ import java.util.Collection; import org.jspecify.annotations.Nullable; -import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; -import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; @SuppressWarnings("deprecation") @@ -48,18 +46,9 @@ class DefaultServerHttpMessageConvertersCustomizer implements ServerHttpMessageC else { builder.registerDefaults(); this.converters.forEach((converter) -> { - if (converter instanceof StringHttpMessageConverter) { - builder.withStringConverter(converter); - } - else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { + if (converter instanceof KotlinSerializationJsonHttpMessageConverter) { builder.withKotlinSerializationJsonConverter(converter); } - else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) { - builder.withJsonConverter(converter); - } - else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) { - builder.withXmlConverter(converter); - } else { builder.addCustomConverter(converter); } @@ -67,13 +56,4 @@ class DefaultServerHttpMessageConvertersCustomizer implements ServerHttpMessageC } } - private static boolean supportsMediaType(HttpMessageConverter converter, MediaType mediaType) { - for (MediaType supportedMediaType : converter.getSupportedMediaTypes()) { - if (supportedMediaType.equalsTypeAndSubtype(mediaType)) { - return true; - } - } - return false; - } - } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java index 81d6d7cdb44..34f1e551e65 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java @@ -27,14 +27,16 @@ 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; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.GsonHttpMessageConverter; -import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; /** * Configuration for HTTP Message converters that use Gson. * * @author Andy Wilkinson * @author EddĂș MelĂ©ndez + * @author Brian Clozel */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Gson.class) @@ -46,11 +48,30 @@ class GsonHttpMessageConvertersConfiguration { static class GsonHttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean - GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) { - GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); - converter.setGson(gson); - return converter; + @ConditionalOnMissingBean(GsonHttpMessageConverter.class) + GsonHttpConvertersCustomizer gsonHttpMessageConvertersCustomizer(Gson gson) { + return new GsonHttpConvertersCustomizer(gson); + } + + } + + static class GsonHttpConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final GsonHttpMessageConverter converter; + + GsonHttpConvertersCustomizer(Gson gson) { + this.converter = new GsonHttpMessageConverter(gson); + } + + @Override + public void customize(ClientBuilder builder) { + builder.withJsonConverter(this.converter); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withJsonConverter(this.converter); } } @@ -80,13 +101,13 @@ class GsonHttpMessageConvertersConfiguration { super(ConfigurationPhase.REGISTER_BEAN); } - @ConditionalOnBean(JacksonJsonHttpMessageConverter.class) + @ConditionalOnBean(JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer.class) static class JacksonAvailable { } @SuppressWarnings("removal") - @ConditionalOnBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class) + @ConditionalOnBean(Jackson2HttpMessageConvertersConfiguration.Jackson2JsonMessageConvertersCustomizer.class) static class Jackson2Available { } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java index f645079bb22..6b58a03eec7 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java @@ -32,6 +32,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.Order; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.StringHttpMessageConverter; /** @@ -86,17 +88,36 @@ public final class HttpMessageConvertersAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(StringHttpMessageConverter.class) @EnableConfigurationProperties(HttpMessageConvertersProperties.class) protected static class StringHttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean - StringHttpMessageConverter stringHttpMessageConverter(HttpMessageConvertersProperties properties) { - StringHttpMessageConverter converter = new StringHttpMessageConverter( - properties.getStringEncodingCharset()); - converter.setWriteAcceptCharset(false); - return converter; + @ConditionalOnMissingBean(StringHttpMessageConverter.class) + StringHttpMessageConvertersCustomizer stringHttpMessageConvertersCustomizer( + HttpMessageConvertersProperties properties) { + return new StringHttpMessageConvertersCustomizer(properties); + } + + } + + static class StringHttpMessageConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + StringHttpMessageConverter converter; + + StringHttpMessageConvertersCustomizer(HttpMessageConvertersProperties properties) { + this.converter = new StringHttpMessageConverter(properties.getStringEncodingCharset()); + this.converter.setWriteAcceptCharset(false); + } + + @Override + public void customize(ClientBuilder builder) { + builder.withStringConverter(this.converter); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withStringConverter(this.converter); } } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java index 7164f80bf26..151ef3237e1 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java @@ -24,22 +24,24 @@ 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.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; 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 + * @author Brian Clozel * @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" }) +@SuppressWarnings("removal") class Jackson2HttpMessageConvertersConfiguration { @Configuration(proxyBeanMethods = false) @@ -49,10 +51,9 @@ class Jackson2HttpMessageConvertersConfiguration { static class MappingJackson2HttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean - org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( - ObjectMapper objectMapper) { - return new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(objectMapper); + @ConditionalOnMissingBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class) + Jackson2JsonMessageConvertersCustomizer jackson2HttpMessageConvertersCustomizer(ObjectMapper objectMapper) { + return new Jackson2JsonMessageConvertersCustomizer(objectMapper); } } @@ -63,10 +64,56 @@ class Jackson2HttpMessageConvertersConfiguration { protected static class MappingJackson2XmlHttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean - public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter( + @ConditionalOnMissingBean(org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter.class) + Jackson2XmlMessageConvertersCustomizer mappingJackson2XmlHttpMessageConverter( Jackson2ObjectMapperBuilder builder) { - return new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()); + return new Jackson2XmlMessageConvertersCustomizer(builder.createXmlMapper(true).build()); + } + + } + + static class Jackson2JsonMessageConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final ObjectMapper objectMapper; + + Jackson2JsonMessageConvertersCustomizer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void customize(ClientBuilder builder) { + builder.withJsonConverter( + new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(this.objectMapper)); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withJsonConverter( + new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(this.objectMapper)); + } + + } + + static class Jackson2XmlMessageConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final ObjectMapper objectMapper; + + Jackson2XmlMessageConvertersCustomizer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void customize(ClientBuilder builder) { + builder.withXmlConverter(new org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter( + this.objectMapper)); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withXmlConverter(new org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter( + this.objectMapper)); } } @@ -83,7 +130,7 @@ class Jackson2HttpMessageConvertersConfiguration { } - @ConditionalOnMissingBean(JacksonJsonHttpMessageConverter.class) + @ConditionalOnMissingBean(JacksonJsonHttpMessageConvertersCustomizer.class) static class JacksonUnavailable { } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java index 0856daf2269..9207b1986c1 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java @@ -25,6 +25,8 @@ 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.Configuration; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; @@ -32,6 +34,7 @@ import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; * Configuration for HTTP message converters that use Jackson. * * @author Andy Wilkinson + * @author Brian Clozel */ @Configuration(proxyBeanMethods = false) class JacksonHttpMessageConvertersConfiguration { @@ -44,11 +47,11 @@ class JacksonHttpMessageConvertersConfiguration { static class JacksonJsonHttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean( + @ConditionalOnMissingBean(value = JacksonJsonHttpMessageConverter.class, ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJacksonJsonHttpMessageConverter" }) - JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter(JsonMapper jsonMapper) { - return new JacksonJsonHttpMessageConverter(jsonMapper); + JacksonJsonHttpMessageConvertersCustomizer jacksonJsonHttpMessageConvertersCustomizer(JsonMapper jsonMapper) { + return new JacksonJsonHttpMessageConvertersCustomizer(jsonMapper); } } @@ -59,9 +62,51 @@ class JacksonHttpMessageConvertersConfiguration { protected static class JacksonXmlHttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean - public JacksonXmlHttpMessageConverter jacksonXmlHttpMessageConverter(XmlMapper xmlMapper) { - return new JacksonXmlHttpMessageConverter(xmlMapper); + @ConditionalOnMissingBean(JacksonXmlHttpMessageConverter.class) + JacksonXmlHttpMessageConvertersCustomizer jacksonXmlHttpMessageConvertersCustomizer(XmlMapper xmlMapper) { + return new JacksonXmlHttpMessageConvertersCustomizer(xmlMapper); + } + + } + + static class JacksonJsonHttpMessageConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final JsonMapper jsonMapper; + + JacksonJsonHttpMessageConvertersCustomizer(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; + } + + @Override + public void customize(ClientBuilder builder) { + builder.withJsonConverter(new JacksonJsonHttpMessageConverter(this.jsonMapper)); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withJsonConverter(new JacksonJsonHttpMessageConverter(this.jsonMapper)); + } + + } + + static class JacksonXmlHttpMessageConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final XmlMapper xmlMapper; + + JacksonXmlHttpMessageConvertersCustomizer(XmlMapper xmlMapper) { + this.xmlMapper = xmlMapper; + } + + @Override + public void customize(ClientBuilder builder) { + builder.withXmlConverter(new JacksonXmlHttpMessageConverter(this.xmlMapper)); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withXmlConverter(new JacksonXmlHttpMessageConverter(this.xmlMapper)); } } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java index be2616b0e7e..fa6a182e7f3 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java @@ -23,11 +23,13 @@ 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.http.converter.autoconfigure.GsonHttpMessageConvertersConfiguration.GsonHttpConvertersCustomizer; +import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer; 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.HttpMessageConverters.ClientBuilder; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.JsonbHttpMessageConverter; /** @@ -45,11 +47,30 @@ class JsonbHttpMessageConvertersConfiguration { static class JsonbHttpMessageConverterConfiguration { @Bean - @ConditionalOnMissingBean - JsonbHttpMessageConverter jsonbHttpMessageConverter(Jsonb jsonb) { - JsonbHttpMessageConverter converter = new JsonbHttpMessageConverter(); - converter.setJsonb(jsonb); - return converter; + @ConditionalOnMissingBean(JsonbHttpMessageConverter.class) + JsonbHttpMessageConvertersCustomizer jsonbHttpMessageConvertersCustomizer(Jsonb jsonb) { + return new JsonbHttpMessageConvertersCustomizer(jsonb); + } + + } + + static class JsonbHttpMessageConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final JsonbHttpMessageConverter converter; + + JsonbHttpMessageConvertersCustomizer(Jsonb jsonb) { + this.converter = new JsonbHttpMessageConverter(jsonb); + } + + @Override + public void customize(ClientBuilder builder) { + builder.withJsonConverter(this.converter); + } + + @Override + public void customize(ServerBuilder builder) { + builder.withJsonConverter(this.converter); } } @@ -67,9 +88,9 @@ class JsonbHttpMessageConvertersConfiguration { } @SuppressWarnings("removal") - @ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, - org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, - GsonHttpMessageConverter.class }) + @ConditionalOnMissingBean({ JacksonJsonHttpMessageConvertersCustomizer.class, + Jackson2HttpMessageConvertersConfiguration.Jackson2JsonMessageConvertersCustomizer.class, + GsonHttpConvertersCustomizer.class }) static class JacksonAndGsonMissing { } diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java index e85ff586fd1..6129b0edbec 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java @@ -19,15 +19,16 @@ package org.springframework.boot.http.converter.autoconfigure; import kotlinx.serialization.Serializable; import kotlinx.serialization.json.Json; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; +import org.springframework.util.ClassUtils; /** * Configuration for HTTP message converters that use Kotlin Serialization. @@ -41,24 +42,36 @@ import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessag class KotlinSerializationHttpMessageConvertersConfiguration { @Bean - @ConditionalOnMissingBean - KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json, - ObjectProvider> converters) { - return supportsApplicationJson(converters) ? new KotlinSerializationJsonHttpMessageConverter(json) - : new KotlinSerializationJsonHttpMessageConverter(json, (type) -> true); + @ConditionalOnMissingBean(KotlinSerializationJsonHttpMessageConverter.class) + KotlinSerializationJsonConvertersCustomizer kotlinSerializationJsonConvertersCustomizer(Json json, + ResourceLoader resourceLoader) { + return new KotlinSerializationJsonConvertersCustomizer(json, resourceLoader); } - private boolean supportsApplicationJson(ObjectProvider> converters) { - return converters.orderedStream().filter(this::supportsApplicationJson).findFirst().isPresent(); - } + static class KotlinSerializationJsonConvertersCustomizer + implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer { + + private final KotlinSerializationJsonHttpMessageConverter converter; + + KotlinSerializationJsonConvertersCustomizer(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); + this.converter = hasAnyJsonSupport ? new KotlinSerializationJsonHttpMessageConverter(json) + : new KotlinSerializationJsonHttpMessageConverter(json, (type) -> true); + } - private boolean supportsApplicationJson(HttpMessageConverter converter) { - for (MediaType mediaType : converter.getSupportedMediaTypes()) { - if (!mediaType.equals(MediaType.ALL) && mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) { - return true; - } + @Override + public void customize(ClientBuilder builder) { + builder.withKotlinSerializationJsonConverter(this.converter); } - return false; + + @Override + public void customize(ServerBuilder builder) { + builder.withKotlinSerializationJsonConverter(this.converter); + } + } } diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java index ce02ad958f9..a6c9e258c50 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.http.converter.autoconfigure; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -29,23 +30,27 @@ import org.junit.jupiter.api.Test; import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.xml.XmlMapper; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; 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.http.converter.autoconfigure.GsonHttpMessageConvertersConfiguration.GsonHttpConvertersCustomizer; +import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer; +import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonXmlHttpMessageConvertersCustomizer; +import org.springframework.boot.http.converter.autoconfigure.JsonbHttpMessageConvertersConfiguration.JsonbHttpMessageConvertersCustomizer; +import org.springframework.boot.http.converter.autoconfigure.KotlinSerializationHttpMessageConvertersConfiguration.KotlinSerializationJsonConvertersCustomizer; 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; -import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.rest.webmvc.alps.AlpsJacksonJsonHttpMessageConverter; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.mediatype.hal.forms.HalFormsHttpMessageConverter; import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -73,6 +78,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Moritz Halbritter * @author Sebastien Deleuze * @author Dmitry Sulman + * @author Brian Clozel */ class HttpMessageConvertersAutoConfigurationTests { @@ -81,35 +87,59 @@ class HttpMessageConvertersAutoConfigurationTests { @Test void jacksonNotAvailable() { - this.contextRunner.run((context) -> { - assertThat(context).doesNotHaveBean(JsonMapper.class); - assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JacksonXmlHttpMessageConverter.class); - }); + this.contextRunner.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName())) + .run((context) -> { + assertThat(context).doesNotHaveBean(JsonMapper.class); + assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConvertersCustomizer.class); + assertThat(context).doesNotHaveBean(JacksonXmlHttpMessageConvertersCustomizer.class); + }); } @Test void jacksonDefaultConverter() { - this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class) - .run(assertConverter(JacksonJsonHttpMessageConverter.class, "jacksonJsonHttpMessageConverter")); + this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class).run((context) -> { + assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class); + assertConverterIsRegistered(context, JacksonJsonHttpMessageConverter.class); + }); } @Test void jacksonConverterWithBuilder() { - this.contextRunner.withUserConfiguration(JacksonJsonMapperBuilderConfig.class) - .run(assertConverter(JacksonJsonHttpMessageConverter.class, "jacksonJsonHttpMessageConverter")); + this.contextRunner.withUserConfiguration(JacksonJsonMapperBuilderConfig.class).run((context) -> { + assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class); + assertConverterIsRegistered(context, JacksonJsonHttpMessageConverter.class); + }); } @Test void jacksonXmlConverterWithBuilder() { - this.contextRunner.withUserConfiguration(JacksonXmlMapperBuilderConfig.class) - .run(assertConverter(JacksonXmlHttpMessageConverter.class, "jacksonXmlHttpMessageConverter")); + this.contextRunner.withUserConfiguration(JacksonXmlMapperBuilderConfig.class).run((context) -> { + assertThat(context).hasSingleBean(JacksonXmlHttpMessageConvertersCustomizer.class); + assertConverterIsRegistered(context, JacksonXmlHttpMessageConverter.class); + }); } @Test void jacksonCustomConverter() { this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class, JacksonConverterConfig.class) - .run(assertConverter(JacksonJsonHttpMessageConverter.class, "customJacksonMessageConverter")); + .run((context) -> { + assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConvertersCustomizer.class); + HttpMessageConverters serverConverters = getServerConverters(context); + assertThat(serverConverters) + .contains(context.getBean("customJacksonMessageConverter", JacksonJsonHttpMessageConverter.class)); + }); + } + + @Test + void jacksonServerAndClientConvertersShouldBeDifferent() { + this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class).run((context) -> { + assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class); + JacksonJsonHttpMessageConverter serverConverter = findConverter(getServerConverters(context), + JacksonJsonHttpMessageConverter.class); + JacksonJsonHttpMessageConverter clientConverter = findConverter(getClientConverters(context), + JacksonJsonHttpMessageConverter.class); + assertThat(serverConverter).isNotEqualTo(clientConverter); + }); } @Test @@ -118,8 +148,8 @@ class HttpMessageConvertersAutoConfigurationTests { void jackson2DefaultConverter() { this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class) .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) - .run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, - "mappingJackson2HttpMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)); } @Test @@ -127,8 +157,8 @@ class HttpMessageConvertersAutoConfigurationTests { @SuppressWarnings("removal") void jackson2ConverterWithBuilder() { this.contextRunner.withUserConfiguration(Jackson2ObjectMapperBuilderConfig.class) - .run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, - "mappingJackson2HttpMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)); } @Test @@ -136,38 +166,55 @@ class HttpMessageConvertersAutoConfigurationTests { @SuppressWarnings("removal") void jackson2CustomConverter() { this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class, Jackson2ConverterConfig.class) - .run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, - "customJacksonMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void jackson2ServerAndClientConvertersShouldBeDifferent() { + this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run((context) -> { + assertThat(context).hasSingleBean( + Jackson2HttpMessageConvertersConfiguration.Jackson2JsonMessageConvertersCustomizer.class); + HttpMessageConverter serverConverter = findConverter(getServerConverters(context), + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class); + HttpMessageConverter clientConverter = findConverter(getClientConverters(context), + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class); + assertThat(serverConverter).isNotEqualTo(clientConverter); + }); } @Test void gsonNotAvailable() { this.contextRunner.run((context) -> { assertThat(context).doesNotHaveBean(Gson.class); - assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); + assertConverterIsNotRegistered(context, GsonHttpMessageConverter.class); }); } @Test void gsonDefaultConverter() { this.contextRunner.withBean(Gson.class) - .run(assertConverter(GsonHttpMessageConverter.class, "gsonHttpMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, GsonHttpMessageConverter.class)); } @Test void gsonCustomConverter() { this.contextRunner.withUserConfiguration(GsonConverterConfig.class) .withBean(Gson.class) - .run(assertConverter(GsonHttpMessageConverter.class, "customGsonMessageConverter")); + .run((context) -> assertThat(getServerConverters(context)) + .contains(context.getBean("customGsonMessageConverter", GsonHttpMessageConverter.class))); } @Test void gsonCanBePreferred() { allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:gson").run((context) -> { - assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter"); - assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class); + assertConverterIsRegistered(context, GsonHttpMessageConverter.class); + assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class); + assertConverterIsNotRegistered(context, JacksonJsonHttpMessageConverter.class); }); } @@ -175,30 +222,30 @@ class HttpMessageConvertersAutoConfigurationTests { void jsonbNotAvailable() { this.contextRunner.run((context) -> { assertThat(context).doesNotHaveBean(Jsonb.class); - assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); + assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class); }); } @Test void jsonbDefaultConverter() { this.contextRunner.withBean(Jsonb.class, JsonbBuilder::create) - .run(assertConverter(JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, JsonbHttpMessageConverter.class)); } @Test void jsonbCustomConverter() { this.contextRunner.withUserConfiguration(JsonbConverterConfig.class) .withBean(Jsonb.class, JsonbBuilder::create) - .run(assertConverter(JsonbHttpMessageConverter.class, "customJsonbMessageConverter")); + .run((context) -> assertThat(getServerConverters(context)) + .contains(context.getBean("customJsonbMessageConverter", JsonbHttpMessageConverter.class))); } @Test void jsonbCanBePreferred() { allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:jsonb").run((context) -> { - assertConverterBeanExists(context, JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter"); - assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class); + assertConverterIsRegistered(context, JsonbHttpMessageConverter.class); + assertConverterIsNotRegistered(context, GsonHttpMessageConverter.class); + assertConverterIsNotRegistered(context, JacksonJsonHttpMessageConverter.class); }); } @@ -206,7 +253,7 @@ class HttpMessageConvertersAutoConfigurationTests { void kotlinSerializationNotAvailable() { this.contextRunner.run((context) -> { assertThat(context).doesNotHaveBean(Json.class); - assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class); + assertThat(context).doesNotHaveBean(KotlinSerializationJsonConvertersCustomizer.class); }); } @@ -214,18 +261,14 @@ class HttpMessageConvertersAutoConfigurationTests { void kotlinSerializationCustomConverter() { this.contextRunner.withUserConfiguration(KotlinSerializationConverterConfig.class) .withBean(Json.class, () -> Json.Default) - .run(assertConverter(KotlinSerializationJsonHttpMessageConverter.class, - "customKotlinSerializationJsonHttpMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, KotlinSerializationJsonHttpMessageConverter.class)); } @Test void kotlinSerializationOrderedAheadOfJsonConverter() { allOptionsRunner().run((context) -> { - assertConverterBeanExists(context, KotlinSerializationJsonHttpMessageConverter.class, - "kotlinSerializationJsonHttpMessageConverter"); - assertConverterBeanRegisteredWithHttpMessageConverters(context, - KotlinSerializationJsonHttpMessageConverter.class); - assertConvertersBeanRegisteredWithHttpMessageConverters(context, + assertConverterIsRegistered(context, KotlinSerializationJsonHttpMessageConverter.class); + assertConvertersRegisteredWithHttpMessageConverters(context, List.of(KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class)); }); } @@ -233,30 +276,27 @@ class HttpMessageConvertersAutoConfigurationTests { @Test void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() { allOptionsRunner().run((context) -> { - KotlinSerializationJsonHttpMessageConverter converter = context - .getBean(KotlinSerializationJsonHttpMessageConverter.class); + KotlinSerializationJsonHttpMessageConverter converter = findConverter(getServerConverters(context), + KotlinSerializationJsonHttpMessageConverter.class); assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isFalse(); }); } - @Test - void kotlinSerializationUsesUnrestrictedPredicateWhenNoOtherJsonConverterIsAvailable() { - this.contextRunner.withBean(Json.class, () -> Json.Default).run((context) -> { - KotlinSerializationJsonHttpMessageConverter converter = context - .getBean(KotlinSerializationJsonHttpMessageConverter.class); - assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isTrue(); - }); - } - @Test void stringDefaultConverter() { - this.contextRunner.run(assertConverter(StringHttpMessageConverter.class, "stringHttpMessageConverter")); + this.contextRunner.run((context) -> assertConverterIsRegistered(context, StringHttpMessageConverter.class)); } @Test void stringCustomConverter() { - this.contextRunner.withUserConfiguration(StringConverterConfig.class) - .run(assertConverter(StringHttpMessageConverter.class, "customStringMessageConverter")); + this.contextRunner.withUserConfiguration(StringConverterConfig.class).run((context) -> { + assertThat(getClientConverters(context)) + .filteredOn((converter) -> converter instanceof StringHttpMessageConverter) + .hasSize(2); + assertThat(getServerConverters(context)) + .filteredOn((converter) -> converter instanceof StringHttpMessageConverter) + .hasSize(2); + }); } @Test @@ -264,10 +304,9 @@ class HttpMessageConvertersAutoConfigurationTests { this.contextRunner .withUserConfiguration(JacksonJsonMapperBuilderConfig.class, TypeConstrainedConverterConfiguration.class) .run((context) -> { - BeanDefinition beanDefinition = ((GenericApplicationContext) context.getSourceApplicationContext()) - .getBeanDefinition("jacksonJsonHttpMessageConverter"); - assertThat(beanDefinition.getFactoryBeanName()) - .isEqualTo(JacksonJsonHttpMessageConverterConfiguration.class.getName()); + assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class); + assertConvertersRegisteredWithHttpMessageConverters(context, List + .of(TypeConstrainedJacksonJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class)); }); } @@ -276,21 +315,20 @@ class HttpMessageConvertersAutoConfigurationTests { this.contextRunner .withUserConfiguration(JacksonJsonMapperBuilderConfig.class, RepositoryRestMvcConfiguration.class) .run((context) -> { - BeanDefinition beanDefinition = ((GenericApplicationContext) context.getSourceApplicationContext()) - .getBeanDefinition("jacksonJsonHttpMessageConverter"); - assertThat(beanDefinition.getFactoryBeanName()) - .isEqualTo(JacksonJsonHttpMessageConverterConfiguration.class.getName()); + assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class); + assertConvertersRegisteredWithHttpMessageConverters(context, List.of(HalFormsHttpMessageConverter.class, + AlpsJacksonJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class)); }); } @Test void jacksonIsPreferredByDefault() { allOptionsRunner().run((context) -> { - assertConverterBeanExists(context, JacksonJsonHttpMessageConverter.class, - "jacksonJsonHttpMessageConverter"); - assertConverterBeanRegisteredWithHttpMessageConverters(context, JacksonJsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); + assertBeanExists(context, JacksonJsonHttpMessageConvertersCustomizer.class, + "jacksonJsonHttpMessageConvertersCustomizer"); + assertConverterIsRegistered(context, JacksonJsonHttpMessageConverter.class); + assertThat(context).doesNotHaveBean(GsonHttpConvertersCustomizer.class); + assertThat(context).doesNotHaveBean(JsonbHttpMessageConvertersCustomizer.class); }); } @@ -300,13 +338,10 @@ class HttpMessageConvertersAutoConfigurationTests { 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, + assertConverterIsRegistered(context, org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class); - assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); + assertConverterIsNotRegistered(context, GsonHttpMessageConverter.class); + assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class); }); } @@ -316,9 +351,8 @@ class HttpMessageConvertersAutoConfigurationTests { .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(), ObjectMapper.class.getPackage().getName())) .run((context) -> { - assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter"); - assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class); - assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); + assertConverterIsRegistered(context, GsonHttpMessageConverter.class); + assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class); }); } @@ -327,15 +361,15 @@ class HttpMessageConvertersAutoConfigurationTests { allOptionsRunner() .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(), ObjectMapper.class.getPackage().getName(), Gson.class.getPackage().getName())) - .run(assertConverter(JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter")); + .run((context) -> assertConverterIsRegistered(context, JsonbHttpMessageConverter.class)); } @Test void whenServletWebApplicationHttpMessageConvertersIsConfigured() { new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(ServerHttpMessageConvertersCustomizer.class) - .hasSingleBean(ClientHttpMessageConvertersCustomizer.class)); + .run((context) -> assertThat(context).hasSingleBean(DefaultClientHttpMessageConvertersCustomizer.class) + .hasSingleBean(DefaultClientHttpMessageConvertersCustomizer.class)); } @Test @@ -351,9 +385,9 @@ class HttpMessageConvertersAutoConfigurationTests { new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .run((context) -> { - assertThat(context).hasSingleBean(StringHttpMessageConverter.class); - assertThat(context.getBean(StringHttpMessageConverter.class).getDefaultCharset()) - .isEqualTo(StandardCharsets.UTF_8); + StringHttpMessageConverter converter = findConverter(getServerConverters(context), + StringHttpMessageConverter.class); + assertThat(converter.getDefaultCharset()).isEqualTo(StandardCharsets.UTF_8); }); } @@ -363,9 +397,9 @@ class HttpMessageConvertersAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .withPropertyValues("spring.http.converters.string-encoding-charset=UTF-16") .run((context) -> { - assertThat(context).hasSingleBean(StringHttpMessageConverter.class); - assertThat(context.getBean(StringHttpMessageConverter.class).getDefaultCharset()) - .isEqualTo(StandardCharsets.UTF_16); + StringHttpMessageConverter serverConverter = findConverter(getServerConverters(context), + StringHttpMessageConverter.class); + assertThat(serverConverter.getDefaultCharset()).isEqualTo(StandardCharsets.UTF_16); }); } @@ -398,53 +432,65 @@ class HttpMessageConvertersAutoConfigurationTests { .withBean(Json.class, () -> Json.Default); } - private ContextConsumer assertConverter( - Class> converterType, String beanName) { - return (context) -> { - assertConverterBeanExists(context, converterType, beanName); - assertConverterBeanRegisteredWithHttpMessageConverters(context, converterType); - }; + private void assertConverterIsRegistered(AssertableApplicationContext context, + Class> converterType) { + assertThat(getClientConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) + .hasSize(1); + assertThat(getServerConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) + .hasSize(1); } - private void assertConverterBeanExists(AssertableApplicationContext context, Class type, String beanName) { - assertThat(context).hasSingleBean(type); + private void assertConverterIsNotRegistered(AssertableApplicationContext context, + Class> converterType) { + assertThat(getClientConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) + .isEmpty(); + assertThat(getServerConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass())) + .isEmpty(); + } + + private void assertBeanExists(AssertableApplicationContext context, Class type, String beanName) { + assertThat(context).getBean(beanName).isInstanceOf(type); assertThat(context).hasBean(beanName); } - private void assertConverterBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context, - Class> type) { - HttpMessageConverter converter = context.getBean(type); - ClientHttpMessageConvertersCustomizer clientCustomizer = context - .getBean(ClientHttpMessageConvertersCustomizer.class); + private HttpMessageConverters getClientConverters(ApplicationContext context) { ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults(); - clientCustomizer.customize(clientBuilder); - HttpMessageConverters clientConverters = clientBuilder.build(); - assertThat(clientConverters).contains(converter); - assertThat(clientConverters).filteredOn((c) -> type.isAssignableFrom(c.getClass())).hasSize(1); + context.getBeansOfType(ClientHttpMessageConvertersCustomizer.class) + .values() + .forEach((customizer) -> customizer.customize(clientBuilder)); + return clientBuilder.build(); + } - ServerHttpMessageConvertersCustomizer serverCustomizer = context - .getBean(ServerHttpMessageConvertersCustomizer.class); + private HttpMessageConverters getServerConverters(ApplicationContext context) { ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults(); - serverCustomizer.customize(serverBuilder); - HttpMessageConverters serverConverters = serverBuilder.build(); - assertThat(serverConverters).contains(converter); - assertThat(serverConverters).filteredOn((c) -> type.isAssignableFrom(c.getClass())).hasSize(1); + context.getBeansOfType(ServerHttpMessageConvertersCustomizer.class) + .values() + .forEach((customizer) -> customizer.customize(serverBuilder)); + return serverBuilder.build(); } - private void assertConvertersBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context, - List>> types) { - List> converterInstances = types.stream().map(context::getBean).toList(); - ClientHttpMessageConvertersCustomizer clientCustomizer = context - .getBean(ClientHttpMessageConvertersCustomizer.class); - ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults(); - clientCustomizer.customize(clientBuilder); - assertThat(clientBuilder.build()).containsSubsequence(converterInstances); + @SuppressWarnings("unchecked") + private > T findConverter(HttpMessageConverters converters, + Class> type) { + for (HttpMessageConverter converter : converters) { + if (type.isAssignableFrom(converter.getClass())) { + return (T) converter; + } + } + throw new IllegalStateException("Could not find converter of type " + type); + } - ServerHttpMessageConvertersCustomizer serverCustomizer = context - .getBean(ServerHttpMessageConvertersCustomizer.class); - ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults(); - serverCustomizer.customize(serverBuilder); - assertThat(serverBuilder.build()).containsSubsequence(converterInstances); + private void assertConvertersRegisteredWithHttpMessageConverters(AssertableApplicationContext context, + List>> types) { + HttpMessageConverters clientConverters = getClientConverters(context); + List> clientConverterTypes = new ArrayList<>(); + clientConverters.forEach((converter) -> clientConverterTypes.add(converter.getClass())); + assertThat(clientConverterTypes).containsSubsequence(types); + + HttpMessageConverters serverConverters = getServerConverters(context); + List> serverConverterTypes = new ArrayList<>(); + serverConverters.forEach((converter) -> serverConverterTypes.add(converter.getClass())); + assertThat(serverConverterTypes).containsSubsequence(types); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java index 10a19a37775..dd43e5067d6 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java @@ -38,9 +38,8 @@ class HttpMessageConvertersAutoConfigurationWithoutJacksonTests { @Test void autoConfigurationWorksWithSpringHateoasButWithoutJackson() { - this.contextRunner - .run((context) -> assertThat(context).hasSingleBean(ClientHttpMessageConvertersCustomizer.class) - .hasSingleBean(ServerHttpMessageConvertersCustomizer.class)); + this.contextRunner.run((context) -> assertThat(context).hasBean("clientConvertersCustomizer") + .hasBean("serverConvertersCustomizer")); } }