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
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration; import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties; import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties;
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.annotation.ImportRuntimeHints;
@ -60,6 +61,8 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; 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.util.Assert;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.HandlerMapping;
@ -183,17 +186,21 @@ public final class GraphQlWebMvcAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler, GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, ObjectProvider<HttpMessageConverter<?>> converters) { GraphQlProperties properties, ObjectProvider<ServerHttpMessageConvertersCustomizer> customizers) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters), return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(customizers),
properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive()); properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive());
} }
private HttpMessageConverter<Object> getJsonConverter(ObjectProvider<HttpMessageConverter<?>> converters) { private HttpMessageConverter<Object> getJsonConverter(
return converters.orderedStream() ObjectProvider<ServerHttpMessageConvertersCustomizer> customizers) {
.filter(this::canReadJsonMap) ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults();
.findFirst() customizers.forEach((customizer) -> customizer.customize(serverBuilder));
.map(this::asObjectHttpMessageConverter) for (HttpMessageConverter<?> converter : serverBuilder.build()) {
.orElseThrow(() -> new IllegalStateException("No JSON converter")); if (canReadJsonMap(converter)) {
return asObjectHttpMessageConverter(converter);
}
}
throw new IllegalStateException("No JSON converter");
} }
private boolean canReadJsonMap(HttpMessageConverter<?> candidate) { 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;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -48,18 +46,9 @@ class DefaultClientHttpMessageConvertersCustomizer implements ClientHttpMessageC
else { else {
builder.registerDefaults(); builder.registerDefaults();
this.converters.forEach((converter) -> { this.converters.forEach((converter) -> {
if (converter instanceof StringHttpMessageConverter) { if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
builder.withStringConverter(converter);
}
else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
builder.withKotlinSerializationJsonConverter(converter); builder.withKotlinSerializationJsonConverter(converter);
} }
else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) {
builder.withJsonConverter(converter);
}
else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) {
builder.withXmlConverter(converter);
}
else { else {
builder.addCustomConverter(converter); 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;
}
} }

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;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -48,18 +46,9 @@ class DefaultServerHttpMessageConvertersCustomizer implements ServerHttpMessageC
else { else {
builder.registerDefaults(); builder.registerDefaults();
this.converters.forEach((converter) -> { this.converters.forEach((converter) -> {
if (converter instanceof StringHttpMessageConverter) { if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
builder.withStringConverter(converter);
}
else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
builder.withKotlinSerializationJsonConverter(converter); builder.withKotlinSerializationJsonConverter(converter);
} }
else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) {
builder.withJsonConverter(converter);
}
else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) {
builder.withXmlConverter(converter);
}
else { else {
builder.addCustomConverter(converter); 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;
}
} }

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;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; 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.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
/** /**
* Configuration for HTTP Message converters that use Gson. * Configuration for HTTP Message converters that use Gson.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Brian Clozel
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Gson.class) @ConditionalOnClass(Gson.class)
@ -46,11 +48,30 @@ class GsonHttpMessageConvertersConfiguration {
static class GsonHttpMessageConverterConfiguration { static class GsonHttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(GsonHttpMessageConverter.class)
GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) { GsonHttpConvertersCustomizer gsonHttpMessageConvertersCustomizer(Gson gson) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); return new GsonHttpConvertersCustomizer(gson);
converter.setGson(gson); }
return converter;
}
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); super(ConfigurationPhase.REGISTER_BEAN);
} }
@ConditionalOnBean(JacksonJsonHttpMessageConverter.class) @ConditionalOnBean(JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer.class)
static class JacksonAvailable { static class JacksonAvailable {
} }
@SuppressWarnings("removal") @SuppressWarnings("removal")
@ConditionalOnBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class) @ConditionalOnBean(Jackson2HttpMessageConvertersConfiguration.Jackson2JsonMessageConvertersCustomizer.class)
static class Jackson2Available { 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;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverter; 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; import org.springframework.http.converter.StringHttpMessageConverter;
/** /**
@ -86,17 +88,36 @@ public final class HttpMessageConvertersAutoConfiguration {
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(StringHttpMessageConverter.class)
@EnableConfigurationProperties(HttpMessageConvertersProperties.class) @EnableConfigurationProperties(HttpMessageConvertersProperties.class)
protected static class StringHttpMessageConverterConfiguration { protected static class StringHttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(StringHttpMessageConverter.class)
StringHttpMessageConverter stringHttpMessageConverter(HttpMessageConvertersProperties properties) { StringHttpMessageConvertersCustomizer stringHttpMessageConvertersCustomizer(
StringHttpMessageConverter converter = new StringHttpMessageConverter( HttpMessageConvertersProperties properties) {
properties.getStringEncodingCharset()); return new StringHttpMessageConvertersCustomizer(properties);
converter.setWriteAcceptCharset(false); }
return converter;
}
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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; 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.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
/** /**
* Configuration for HTTP message converters that use Jackson 2. * Configuration for HTTP message converters that use Jackson 2.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Brian Clozel
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3. * @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@Deprecated(since = "4.0.0", forRemoval = true) @Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings({ "deprecation", "removal" }) @SuppressWarnings("removal")
class Jackson2HttpMessageConvertersConfiguration { class Jackson2HttpMessageConvertersConfiguration {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ -49,10 +51,9 @@ class Jackson2HttpMessageConvertersConfiguration {
static class MappingJackson2HttpMessageConverterConfiguration { static class MappingJackson2HttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( Jackson2JsonMessageConvertersCustomizer jackson2HttpMessageConvertersCustomizer(ObjectMapper objectMapper) {
ObjectMapper objectMapper) { return new Jackson2JsonMessageConvertersCustomizer(objectMapper);
return new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(objectMapper);
} }
} }
@ -63,10 +64,56 @@ class Jackson2HttpMessageConvertersConfiguration {
protected static class MappingJackson2XmlHttpMessageConverterConfiguration { protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter.class)
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter( Jackson2XmlMessageConvertersCustomizer mappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder builder) { 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 { 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
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; 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. * Configuration for HTTP message converters that use Jackson.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Brian Clozel
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
class JacksonHttpMessageConvertersConfiguration { class JacksonHttpMessageConvertersConfiguration {
@ -44,11 +47,11 @@ class JacksonHttpMessageConvertersConfiguration {
static class JacksonJsonHttpMessageConverterConfiguration { static class JacksonJsonHttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean( @ConditionalOnMissingBean(value = JacksonJsonHttpMessageConverter.class,
ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter", ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter",
"org.springframework.data.rest.webmvc.alps.AlpsJacksonJsonHttpMessageConverter" }) "org.springframework.data.rest.webmvc.alps.AlpsJacksonJsonHttpMessageConverter" })
JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter(JsonMapper jsonMapper) { JacksonJsonHttpMessageConvertersCustomizer jacksonJsonHttpMessageConvertersCustomizer(JsonMapper jsonMapper) {
return new JacksonJsonHttpMessageConverter(jsonMapper); return new JacksonJsonHttpMessageConvertersCustomizer(jsonMapper);
} }
} }
@ -59,9 +62,51 @@ class JacksonHttpMessageConvertersConfiguration {
protected static class JacksonXmlHttpMessageConverterConfiguration { protected static class JacksonXmlHttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(JacksonXmlHttpMessageConverter.class)
public JacksonXmlHttpMessageConverter jacksonXmlHttpMessageConverter(XmlMapper xmlMapper) { JacksonXmlHttpMessageConvertersCustomizer jacksonXmlHttpMessageConvertersCustomizer(XmlMapper xmlMapper) {
return new JacksonXmlHttpMessageConverter(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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.json.JsonbHttpMessageConverter; import org.springframework.http.converter.json.JsonbHttpMessageConverter;
/** /**
@ -45,11 +47,30 @@ class JsonbHttpMessageConvertersConfiguration {
static class JsonbHttpMessageConverterConfiguration { static class JsonbHttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(JsonbHttpMessageConverter.class)
JsonbHttpMessageConverter jsonbHttpMessageConverter(Jsonb jsonb) { JsonbHttpMessageConvertersCustomizer jsonbHttpMessageConvertersCustomizer(Jsonb jsonb) {
JsonbHttpMessageConverter converter = new JsonbHttpMessageConverter(); return new JsonbHttpMessageConvertersCustomizer(jsonb);
converter.setJsonb(jsonb); }
return converter;
}
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") @SuppressWarnings("removal")
@ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, @ConditionalOnMissingBean({ JacksonJsonHttpMessageConvertersCustomizer.class,
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, Jackson2HttpMessageConvertersConfiguration.Jackson2JsonMessageConvertersCustomizer.class,
GsonHttpMessageConverter.class }) GsonHttpConvertersCustomizer.class })
static class JacksonAndGsonMissing { 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;
import kotlinx.serialization.Serializable; import kotlinx.serialization.Serializable;
import kotlinx.serialization.json.Json; import kotlinx.serialization.json.Json;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType; import org.springframework.core.io.ResourceLoader;
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.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.util.ClassUtils;
/** /**
* Configuration for HTTP message converters that use Kotlin Serialization. * Configuration for HTTP message converters that use Kotlin Serialization.
@ -41,24 +42,36 @@ import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessag
class KotlinSerializationHttpMessageConvertersConfiguration { class KotlinSerializationHttpMessageConvertersConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean(KotlinSerializationJsonHttpMessageConverter.class)
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json, KotlinSerializationJsonConvertersCustomizer kotlinSerializationJsonConvertersCustomizer(Json json,
ObjectProvider<HttpMessageConverter<?>> converters) { ResourceLoader resourceLoader) {
return supportsApplicationJson(converters) ? new KotlinSerializationJsonHttpMessageConverter(json) return new KotlinSerializationJsonConvertersCustomizer(json, resourceLoader);
: new KotlinSerializationJsonHttpMessageConverter(json, (type) -> true);
} }
private boolean supportsApplicationJson(ObjectProvider<HttpMessageConverter<?>> converters) { static class KotlinSerializationJsonConvertersCustomizer
return converters.orderedStream().filter(this::supportsApplicationJson).findFirst().isPresent(); 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) { @Override
for (MediaType mediaType : converter.getSupportedMediaTypes()) { public void customize(ClientBuilder builder) {
if (!mediaType.equals(MediaType.ALL) && mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) { builder.withKotlinSerializationJsonConverter(this.converter);
return true;
}
} }
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 @@
package org.springframework.boot.http.converter.autoconfigure; package org.springframework.boot.http.converter.autoconfigure;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -29,23 +30,27 @@ import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.json.JsonMapper;
import tools.jackson.dataformat.xml.XmlMapper; import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; 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.logging.LogLevel;
import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; 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.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsHttpMessageConverter;
import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter; import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
@ -73,6 +78,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @author Dmitry Sulman * @author Dmitry Sulman
* @author Brian Clozel
*/ */
class HttpMessageConvertersAutoConfigurationTests { class HttpMessageConvertersAutoConfigurationTests {
@ -81,35 +87,59 @@ class HttpMessageConvertersAutoConfigurationTests {
@Test @Test
void jacksonNotAvailable() { void jacksonNotAvailable() {
this.contextRunner.run((context) -> { this.contextRunner.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName()))
assertThat(context).doesNotHaveBean(JsonMapper.class); .run((context) -> {
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class); assertThat(context).doesNotHaveBean(JsonMapper.class);
assertThat(context).doesNotHaveBean(JacksonXmlHttpMessageConverter.class); assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConvertersCustomizer.class);
}); assertThat(context).doesNotHaveBean(JacksonXmlHttpMessageConvertersCustomizer.class);
});
} }
@Test @Test
void jacksonDefaultConverter() { void jacksonDefaultConverter() {
this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class) this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class).run((context) -> {
.run(assertConverter(JacksonJsonHttpMessageConverter.class, "jacksonJsonHttpMessageConverter")); assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class);
assertConverterIsRegistered(context, JacksonJsonHttpMessageConverter.class);
});
} }
@Test @Test
void jacksonConverterWithBuilder() { void jacksonConverterWithBuilder() {
this.contextRunner.withUserConfiguration(JacksonJsonMapperBuilderConfig.class) this.contextRunner.withUserConfiguration(JacksonJsonMapperBuilderConfig.class).run((context) -> {
.run(assertConverter(JacksonJsonHttpMessageConverter.class, "jacksonJsonHttpMessageConverter")); assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class);
assertConverterIsRegistered(context, JacksonJsonHttpMessageConverter.class);
});
} }
@Test @Test
void jacksonXmlConverterWithBuilder() { void jacksonXmlConverterWithBuilder() {
this.contextRunner.withUserConfiguration(JacksonXmlMapperBuilderConfig.class) this.contextRunner.withUserConfiguration(JacksonXmlMapperBuilderConfig.class).run((context) -> {
.run(assertConverter(JacksonXmlHttpMessageConverter.class, "jacksonXmlHttpMessageConverter")); assertThat(context).hasSingleBean(JacksonXmlHttpMessageConvertersCustomizer.class);
assertConverterIsRegistered(context, JacksonXmlHttpMessageConverter.class);
});
} }
@Test @Test
void jacksonCustomConverter() { void jacksonCustomConverter() {
this.contextRunner.withUserConfiguration(JacksonJsonMapperConfig.class, JacksonConverterConfig.class) 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 @Test
@ -118,8 +148,8 @@ class HttpMessageConvertersAutoConfigurationTests {
void jackson2DefaultConverter() { void jackson2DefaultConverter() {
this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class) this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class)
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, .run((context) -> assertConverterIsRegistered(context,
"mappingJackson2HttpMessageConverter")); org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class));
} }
@Test @Test
@ -127,8 +157,8 @@ class HttpMessageConvertersAutoConfigurationTests {
@SuppressWarnings("removal") @SuppressWarnings("removal")
void jackson2ConverterWithBuilder() { void jackson2ConverterWithBuilder() {
this.contextRunner.withUserConfiguration(Jackson2ObjectMapperBuilderConfig.class) this.contextRunner.withUserConfiguration(Jackson2ObjectMapperBuilderConfig.class)
.run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, .run((context) -> assertConverterIsRegistered(context,
"mappingJackson2HttpMessageConverter")); org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class));
} }
@Test @Test
@ -136,38 +166,55 @@ class HttpMessageConvertersAutoConfigurationTests {
@SuppressWarnings("removal") @SuppressWarnings("removal")
void jackson2CustomConverter() { void jackson2CustomConverter() {
this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class, Jackson2ConverterConfig.class) this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class, Jackson2ConverterConfig.class)
.run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, .run((context) -> assertConverterIsRegistered(context,
"customJacksonMessageConverter")); 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 @Test
void gsonNotAvailable() { void gsonNotAvailable() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(Gson.class); assertThat(context).doesNotHaveBean(Gson.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); assertConverterIsNotRegistered(context, GsonHttpMessageConverter.class);
}); });
} }
@Test @Test
void gsonDefaultConverter() { void gsonDefaultConverter() {
this.contextRunner.withBean(Gson.class) this.contextRunner.withBean(Gson.class)
.run(assertConverter(GsonHttpMessageConverter.class, "gsonHttpMessageConverter")); .run((context) -> assertConverterIsRegistered(context, GsonHttpMessageConverter.class));
} }
@Test @Test
void gsonCustomConverter() { void gsonCustomConverter() {
this.contextRunner.withUserConfiguration(GsonConverterConfig.class) this.contextRunner.withUserConfiguration(GsonConverterConfig.class)
.withBean(Gson.class) .withBean(Gson.class)
.run(assertConverter(GsonHttpMessageConverter.class, "customGsonMessageConverter")); .run((context) -> assertThat(getServerConverters(context))
.contains(context.getBean("customGsonMessageConverter", GsonHttpMessageConverter.class)));
} }
@Test @Test
void gsonCanBePreferred() { void gsonCanBePreferred() {
allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:gson").run((context) -> { allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:gson").run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter"); assertConverterIsRegistered(context, GsonHttpMessageConverter.class);
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class); assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); assertConverterIsNotRegistered(context, JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
}); });
} }
@ -175,30 +222,30 @@ class HttpMessageConvertersAutoConfigurationTests {
void jsonbNotAvailable() { void jsonbNotAvailable() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(Jsonb.class); assertThat(context).doesNotHaveBean(Jsonb.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class);
}); });
} }
@Test @Test
void jsonbDefaultConverter() { void jsonbDefaultConverter() {
this.contextRunner.withBean(Jsonb.class, JsonbBuilder::create) this.contextRunner.withBean(Jsonb.class, JsonbBuilder::create)
.run(assertConverter(JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter")); .run((context) -> assertConverterIsRegistered(context, JsonbHttpMessageConverter.class));
} }
@Test @Test
void jsonbCustomConverter() { void jsonbCustomConverter() {
this.contextRunner.withUserConfiguration(JsonbConverterConfig.class) this.contextRunner.withUserConfiguration(JsonbConverterConfig.class)
.withBean(Jsonb.class, JsonbBuilder::create) .withBean(Jsonb.class, JsonbBuilder::create)
.run(assertConverter(JsonbHttpMessageConverter.class, "customJsonbMessageConverter")); .run((context) -> assertThat(getServerConverters(context))
.contains(context.getBean("customJsonbMessageConverter", JsonbHttpMessageConverter.class)));
} }
@Test @Test
void jsonbCanBePreferred() { void jsonbCanBePreferred() {
allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:jsonb").run((context) -> { allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:jsonb").run((context) -> {
assertConverterBeanExists(context, JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter"); assertConverterIsRegistered(context, JsonbHttpMessageConverter.class);
assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class); assertConverterIsNotRegistered(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); assertConverterIsNotRegistered(context, JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
}); });
} }
@ -206,7 +253,7 @@ class HttpMessageConvertersAutoConfigurationTests {
void kotlinSerializationNotAvailable() { void kotlinSerializationNotAvailable() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(Json.class); assertThat(context).doesNotHaveBean(Json.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class); assertThat(context).doesNotHaveBean(KotlinSerializationJsonConvertersCustomizer.class);
}); });
} }
@ -214,18 +261,14 @@ class HttpMessageConvertersAutoConfigurationTests {
void kotlinSerializationCustomConverter() { void kotlinSerializationCustomConverter() {
this.contextRunner.withUserConfiguration(KotlinSerializationConverterConfig.class) this.contextRunner.withUserConfiguration(KotlinSerializationConverterConfig.class)
.withBean(Json.class, () -> Json.Default) .withBean(Json.class, () -> Json.Default)
.run(assertConverter(KotlinSerializationJsonHttpMessageConverter.class, .run((context) -> assertConverterIsRegistered(context, KotlinSerializationJsonHttpMessageConverter.class));
"customKotlinSerializationJsonHttpMessageConverter"));
} }
@Test @Test
void kotlinSerializationOrderedAheadOfJsonConverter() { void kotlinSerializationOrderedAheadOfJsonConverter() {
allOptionsRunner().run((context) -> { allOptionsRunner().run((context) -> {
assertConverterBeanExists(context, KotlinSerializationJsonHttpMessageConverter.class, assertConverterIsRegistered(context, KotlinSerializationJsonHttpMessageConverter.class);
"kotlinSerializationJsonHttpMessageConverter"); assertConvertersRegisteredWithHttpMessageConverters(context,
assertConverterBeanRegisteredWithHttpMessageConverters(context,
KotlinSerializationJsonHttpMessageConverter.class);
assertConvertersBeanRegisteredWithHttpMessageConverters(context,
List.of(KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class)); List.of(KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class));
}); });
} }
@ -233,30 +276,27 @@ class HttpMessageConvertersAutoConfigurationTests {
@Test @Test
void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() { void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() {
allOptionsRunner().run((context) -> { allOptionsRunner().run((context) -> {
KotlinSerializationJsonHttpMessageConverter converter = context KotlinSerializationJsonHttpMessageConverter converter = findConverter(getServerConverters(context),
.getBean(KotlinSerializationJsonHttpMessageConverter.class); KotlinSerializationJsonHttpMessageConverter.class);
assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isFalse(); 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 @Test
void stringDefaultConverter() { void stringDefaultConverter() {
this.contextRunner.run(assertConverter(StringHttpMessageConverter.class, "stringHttpMessageConverter")); this.contextRunner.run((context) -> assertConverterIsRegistered(context, StringHttpMessageConverter.class));
} }
@Test @Test
void stringCustomConverter() { void stringCustomConverter() {
this.contextRunner.withUserConfiguration(StringConverterConfig.class) this.contextRunner.withUserConfiguration(StringConverterConfig.class).run((context) -> {
.run(assertConverter(StringHttpMessageConverter.class, "customStringMessageConverter")); assertThat(getClientConverters(context))
.filteredOn((converter) -> converter instanceof StringHttpMessageConverter)
.hasSize(2);
assertThat(getServerConverters(context))
.filteredOn((converter) -> converter instanceof StringHttpMessageConverter)
.hasSize(2);
});
} }
@Test @Test
@ -264,10 +304,9 @@ class HttpMessageConvertersAutoConfigurationTests {
this.contextRunner this.contextRunner
.withUserConfiguration(JacksonJsonMapperBuilderConfig.class, TypeConstrainedConverterConfiguration.class) .withUserConfiguration(JacksonJsonMapperBuilderConfig.class, TypeConstrainedConverterConfiguration.class)
.run((context) -> { .run((context) -> {
BeanDefinition beanDefinition = ((GenericApplicationContext) context.getSourceApplicationContext()) assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class);
.getBeanDefinition("jacksonJsonHttpMessageConverter"); assertConvertersRegisteredWithHttpMessageConverters(context, List
assertThat(beanDefinition.getFactoryBeanName()) .of(TypeConstrainedJacksonJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class));
.isEqualTo(JacksonJsonHttpMessageConverterConfiguration.class.getName());
}); });
} }
@ -276,21 +315,20 @@ class HttpMessageConvertersAutoConfigurationTests {
this.contextRunner this.contextRunner
.withUserConfiguration(JacksonJsonMapperBuilderConfig.class, RepositoryRestMvcConfiguration.class) .withUserConfiguration(JacksonJsonMapperBuilderConfig.class, RepositoryRestMvcConfiguration.class)
.run((context) -> { .run((context) -> {
BeanDefinition beanDefinition = ((GenericApplicationContext) context.getSourceApplicationContext()) assertThat(context).hasSingleBean(JacksonJsonHttpMessageConvertersCustomizer.class);
.getBeanDefinition("jacksonJsonHttpMessageConverter"); assertConvertersRegisteredWithHttpMessageConverters(context, List.of(HalFormsHttpMessageConverter.class,
assertThat(beanDefinition.getFactoryBeanName()) AlpsJacksonJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class));
.isEqualTo(JacksonJsonHttpMessageConverterConfiguration.class.getName());
}); });
} }
@Test @Test
void jacksonIsPreferredByDefault() { void jacksonIsPreferredByDefault() {
allOptionsRunner().run((context) -> { allOptionsRunner().run((context) -> {
assertConverterBeanExists(context, JacksonJsonHttpMessageConverter.class, assertBeanExists(context, JacksonJsonHttpMessageConvertersCustomizer.class,
"jacksonJsonHttpMessageConverter"); "jacksonJsonHttpMessageConvertersCustomizer");
assertConverterBeanRegisteredWithHttpMessageConverters(context, JacksonJsonHttpMessageConverter.class); assertConverterIsRegistered(context, JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); assertThat(context).doesNotHaveBean(GsonHttpConvertersCustomizer.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); assertThat(context).doesNotHaveBean(JsonbHttpMessageConvertersCustomizer.class);
}); });
} }
@ -300,13 +338,10 @@ class HttpMessageConvertersAutoConfigurationTests {
allOptionsRunner().withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName())) allOptionsRunner().withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName()))
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run((context) -> { .run((context) -> {
assertConverterBeanExists(context, assertConverterIsRegistered(context,
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class,
"mappingJackson2HttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context,
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class); org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); assertConverterIsNotRegistered(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class);
}); });
} }
@ -316,9 +351,8 @@ class HttpMessageConvertersAutoConfigurationTests {
.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(), .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
ObjectMapper.class.getPackage().getName())) ObjectMapper.class.getPackage().getName()))
.run((context) -> { .run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter"); assertConverterIsRegistered(context, GsonHttpMessageConverter.class);
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class); assertConverterIsNotRegistered(context, JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
}); });
} }
@ -327,15 +361,15 @@ class HttpMessageConvertersAutoConfigurationTests {
allOptionsRunner() allOptionsRunner()
.withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(), .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
ObjectMapper.class.getPackage().getName(), Gson.class.getPackage().getName())) ObjectMapper.class.getPackage().getName(), Gson.class.getPackage().getName()))
.run(assertConverter(JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter")); .run((context) -> assertConverterIsRegistered(context, JsonbHttpMessageConverter.class));
} }
@Test @Test
void whenServletWebApplicationHttpMessageConvertersIsConfigured() { void whenServletWebApplicationHttpMessageConvertersIsConfigured() {
new WebApplicationContextRunner() new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(ServerHttpMessageConvertersCustomizer.class) .run((context) -> assertThat(context).hasSingleBean(DefaultClientHttpMessageConvertersCustomizer.class)
.hasSingleBean(ClientHttpMessageConvertersCustomizer.class)); .hasSingleBean(DefaultClientHttpMessageConvertersCustomizer.class));
} }
@Test @Test
@ -351,9 +385,9 @@ class HttpMessageConvertersAutoConfigurationTests {
new WebApplicationContextRunner() new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(StringHttpMessageConverter.class); StringHttpMessageConverter converter = findConverter(getServerConverters(context),
assertThat(context.getBean(StringHttpMessageConverter.class).getDefaultCharset()) StringHttpMessageConverter.class);
.isEqualTo(StandardCharsets.UTF_8); assertThat(converter.getDefaultCharset()).isEqualTo(StandardCharsets.UTF_8);
}); });
} }
@ -363,9 +397,9 @@ class HttpMessageConvertersAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.withPropertyValues("spring.http.converters.string-encoding-charset=UTF-16") .withPropertyValues("spring.http.converters.string-encoding-charset=UTF-16")
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(StringHttpMessageConverter.class); StringHttpMessageConverter serverConverter = findConverter(getServerConverters(context),
assertThat(context.getBean(StringHttpMessageConverter.class).getDefaultCharset()) StringHttpMessageConverter.class);
.isEqualTo(StandardCharsets.UTF_16); assertThat(serverConverter.getDefaultCharset()).isEqualTo(StandardCharsets.UTF_16);
}); });
} }
@ -398,53 +432,65 @@ class HttpMessageConvertersAutoConfigurationTests {
.withBean(Json.class, () -> Json.Default); .withBean(Json.class, () -> Json.Default);
} }
private ContextConsumer<AssertableApplicationContext> assertConverter( private void assertConverterIsRegistered(AssertableApplicationContext context,
Class<? extends HttpMessageConverter<?>> converterType, String beanName) { Class<? extends HttpMessageConverter<?>> converterType) {
return (context) -> { assertThat(getClientConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass()))
assertConverterBeanExists(context, converterType, beanName); .hasSize(1);
assertConverterBeanRegisteredWithHttpMessageConverters(context, converterType); assertThat(getServerConverters(context)).filteredOn((c) -> converterType.isAssignableFrom(c.getClass()))
}; .hasSize(1);
} }
private void assertConverterBeanExists(AssertableApplicationContext context, Class<?> type, String beanName) { private void assertConverterIsNotRegistered(AssertableApplicationContext context,
assertThat(context).hasSingleBean(type); 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); assertThat(context).hasBean(beanName);
} }
private void assertConverterBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context, private HttpMessageConverters getClientConverters(ApplicationContext context) {
Class<? extends HttpMessageConverter<?>> type) {
HttpMessageConverter<?> converter = context.getBean(type);
ClientHttpMessageConvertersCustomizer clientCustomizer = context
.getBean(ClientHttpMessageConvertersCustomizer.class);
ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults(); ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults();
clientCustomizer.customize(clientBuilder); context.getBeansOfType(ClientHttpMessageConvertersCustomizer.class)
HttpMessageConverters clientConverters = clientBuilder.build(); .values()
assertThat(clientConverters).contains(converter); .forEach((customizer) -> customizer.customize(clientBuilder));
assertThat(clientConverters).filteredOn((c) -> type.isAssignableFrom(c.getClass())).hasSize(1); return clientBuilder.build();
}
ServerHttpMessageConvertersCustomizer serverCustomizer = context private HttpMessageConverters getServerConverters(ApplicationContext context) {
.getBean(ServerHttpMessageConvertersCustomizer.class);
ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults(); ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults();
serverCustomizer.customize(serverBuilder); context.getBeansOfType(ServerHttpMessageConvertersCustomizer.class)
HttpMessageConverters serverConverters = serverBuilder.build(); .values()
assertThat(serverConverters).contains(converter); .forEach((customizer) -> customizer.customize(serverBuilder));
assertThat(serverConverters).filteredOn((c) -> type.isAssignableFrom(c.getClass())).hasSize(1); return serverBuilder.build();
} }
private void assertConvertersBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context, @SuppressWarnings("unchecked")
List<Class<? extends HttpMessageConverter<?>>> types) { private <T extends HttpMessageConverter<?>> T findConverter(HttpMessageConverters converters,
List<? extends HttpMessageConverter<?>> converterInstances = types.stream().map(context::getBean).toList(); Class<? extends HttpMessageConverter<?>> type) {
ClientHttpMessageConvertersCustomizer clientCustomizer = context for (HttpMessageConverter<?> converter : converters) {
.getBean(ClientHttpMessageConvertersCustomizer.class); if (type.isAssignableFrom(converter.getClass())) {
ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults(); return (T) converter;
clientCustomizer.customize(clientBuilder); }
assertThat(clientBuilder.build()).containsSubsequence(converterInstances); }
throw new IllegalStateException("Could not find converter of type " + type);
}
ServerHttpMessageConvertersCustomizer serverCustomizer = context private void assertConvertersRegisteredWithHttpMessageConverters(AssertableApplicationContext context,
.getBean(ServerHttpMessageConvertersCustomizer.class); List<Class<? extends HttpMessageConverter<?>>> types) {
ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults(); HttpMessageConverters clientConverters = getClientConverters(context);
serverCustomizer.customize(serverBuilder); List<Class<?>> clientConverterTypes = new ArrayList<>();
assertThat(serverBuilder.build()).containsSubsequence(converterInstances); 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) @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 {
@Test @Test
void autoConfigurationWorksWithSpringHateoasButWithoutJackson() { void autoConfigurationWorksWithSpringHateoasButWithoutJackson() {
this.contextRunner this.contextRunner.run((context) -> assertThat(context).hasBean("clientConvertersCustomizer")
.run((context) -> assertThat(context).hasSingleBean(ClientHttpMessageConvertersCustomizer.class) .hasBean("serverConvertersCustomizer"));
.hasSingleBean(ServerHttpMessageConvertersCustomizer.class));
} }
} }

Loading…
Cancel
Save