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")); } }