Browse Source

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
pull/48366/head
Brian Clozel 2 weeks ago
parent
commit
50f64f947a
  1. 23
      module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java
  2. 22
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java
  3. 22
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java
  4. 37
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java
  5. 35
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java
  6. 69
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java
  7. 57
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java
  8. 41
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java
  9. 47
      module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java
  10. 296
      module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java
  11. 5
      module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java

23
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 @@ -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; @@ -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 { @@ -183,17 +186,21 @@ public final class GraphQlWebMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, ObjectProvider<HttpMessageConverter<?>> converters) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
GraphQlProperties properties, ObjectProvider<ServerHttpMessageConvertersCustomizer> customizers) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(customizers),
properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive());
}
private HttpMessageConverter<Object> getJsonConverter(ObjectProvider<HttpMessageConverter<?>> converters) {
return converters.orderedStream()
.filter(this::canReadJsonMap)
.findFirst()
.map(this::asObjectHttpMessageConverter)
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
private HttpMessageConverter<Object> getJsonConverter(
ObjectProvider<ServerHttpMessageConvertersCustomizer> 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) {

22
module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java

@ -20,10 +20,8 @@ import java.util.Collection; @@ -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 @@ -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 @@ -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;
}
}

22
module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java

@ -20,10 +20,8 @@ import java.util.Collection; @@ -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 @@ -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 @@ -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;
}
}

37
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; @@ -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 { @@ -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 { @@ -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 {
}

35
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; @@ -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 { @@ -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);
}
}

69
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; @@ -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 { @@ -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 { @@ -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 { @@ -83,7 +130,7 @@ class Jackson2HttpMessageConvertersConfiguration {
}
@ConditionalOnMissingBean(JacksonJsonHttpMessageConverter.class)
@ConditionalOnMissingBean(JacksonJsonHttpMessageConvertersCustomizer.class)
static class JacksonUnavailable {
}

57
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 @@ -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; @@ -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 { @@ -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 { @@ -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));
}
}

41
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; @@ -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 { @@ -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 { @@ -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 {
}

47
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; @@ -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 @@ -41,24 +42,36 @@ import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessag
class KotlinSerializationHttpMessageConvertersConfiguration {
@Bean
@ConditionalOnMissingBean
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json,
ObjectProvider<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> 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);
}
}
}

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

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package org.springframework.boot.http.converter.autoconfigure;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -29,23 +30,27 @@ import org.junit.jupiter.api.Test; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -398,53 +432,65 @@ class HttpMessageConvertersAutoConfigurationTests {
.withBean(Json.class, () -> Json.Default);
}
private ContextConsumer<AssertableApplicationContext> assertConverter(
Class<? extends HttpMessageConverter<?>> converterType, String beanName) {
return (context) -> {
assertConverterBeanExists(context, converterType, beanName);
assertConverterBeanRegisteredWithHttpMessageConverters(context, converterType);
};
private void assertConverterIsRegistered(AssertableApplicationContext context,
Class<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<Class<? extends HttpMessageConverter<?>>> types) {
List<? extends HttpMessageConverter<?>> 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 extends HttpMessageConverter<?>> T findConverter(HttpMessageConverters converters,
Class<? extends HttpMessageConverter<?>> 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<Class<? extends HttpMessageConverter<?>>> types) {
HttpMessageConverters clientConverters = getClientConverters(context);
List<Class<?>> clientConverterTypes = new ArrayList<>();
clientConverters.forEach((converter) -> clientConverterTypes.add(converter.getClass()));
assertThat(clientConverterTypes).containsSubsequence(types);
HttpMessageConverters serverConverters = getServerConverters(context);
List<Class<?>> serverConverterTypes = new ArrayList<>();
serverConverters.forEach((converter) -> serverConverterTypes.add(converter.getClass()));
assertThat(serverConverterTypes).containsSubsequence(types);
}
@Configuration(proxyBeanMethods = false)

5
module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java

@ -38,9 +38,8 @@ class HttpMessageConvertersAutoConfigurationWithoutJacksonTests { @@ -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"));
}
}

Loading…
Cancel
Save