Skip to content

Commit 50f64f9

Browse files
committed
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
1 parent c1e4375 commit 50f64f9

File tree

11 files changed

+417
-237
lines changed

11 files changed

+417
-237
lines changed

module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
4141
import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties;
4242
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties;
43+
import org.springframework.boot.http.converter.autoconfigure.ServerHttpMessageConvertersCustomizer;
4344
import org.springframework.context.annotation.Bean;
4445
import org.springframework.context.annotation.Configuration;
4546
import org.springframework.context.annotation.ImportRuntimeHints;
@@ -60,6 +61,8 @@
6061
import org.springframework.http.HttpStatus;
6162
import org.springframework.http.MediaType;
6263
import org.springframework.http.converter.HttpMessageConverter;
64+
import org.springframework.http.converter.HttpMessageConverters;
65+
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
6366
import org.springframework.util.Assert;
6467
import org.springframework.web.cors.CorsConfiguration;
6568
import org.springframework.web.servlet.HandlerMapping;
@@ -183,17 +186,21 @@ static class WebSocketConfiguration {
183186
@Bean
184187
@ConditionalOnMissingBean
185188
GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
186-
GraphQlProperties properties, ObjectProvider<HttpMessageConverter<?>> converters) {
187-
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
189+
GraphQlProperties properties, ObjectProvider<ServerHttpMessageConvertersCustomizer> customizers) {
190+
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(customizers),
188191
properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive());
189192
}
190193

191-
private HttpMessageConverter<Object> getJsonConverter(ObjectProvider<HttpMessageConverter<?>> converters) {
192-
return converters.orderedStream()
193-
.filter(this::canReadJsonMap)
194-
.findFirst()
195-
.map(this::asObjectHttpMessageConverter)
196-
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
194+
private HttpMessageConverter<Object> getJsonConverter(
195+
ObjectProvider<ServerHttpMessageConvertersCustomizer> customizers) {
196+
ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults();
197+
customizers.forEach((customizer) -> customizer.customize(serverBuilder));
198+
for (HttpMessageConverter<?> converter : serverBuilder.build()) {
199+
if (canReadJsonMap(converter)) {
200+
return asObjectHttpMessageConverter(converter);
201+
}
202+
}
203+
throw new IllegalStateException("No JSON converter");
197204
}
198205

199206
private boolean canReadJsonMap(HttpMessageConverter<?> candidate) {

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

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@
2020

2121
import org.jspecify.annotations.Nullable;
2222

23-
import org.springframework.http.MediaType;
2423
import org.springframework.http.converter.HttpMessageConverter;
2524
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
26-
import org.springframework.http.converter.StringHttpMessageConverter;
2725
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
2826

2927
@SuppressWarnings("deprecation")
@@ -48,32 +46,14 @@ public void customize(ClientBuilder builder) {
4846
else {
4947
builder.registerDefaults();
5048
this.converters.forEach((converter) -> {
51-
if (converter instanceof StringHttpMessageConverter) {
52-
builder.withStringConverter(converter);
53-
}
54-
else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
49+
if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
5550
builder.withKotlinSerializationJsonConverter(converter);
5651
}
57-
else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) {
58-
builder.withJsonConverter(converter);
59-
}
60-
else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) {
61-
builder.withXmlConverter(converter);
62-
}
6352
else {
6453
builder.addCustomConverter(converter);
6554
}
6655
});
6756
}
6857
}
6958

70-
private static boolean supportsMediaType(HttpMessageConverter<?> converter, MediaType mediaType) {
71-
for (MediaType supportedMediaType : converter.getSupportedMediaTypes()) {
72-
if (supportedMediaType.equalsTypeAndSubtype(mediaType)) {
73-
return true;
74-
}
75-
}
76-
return false;
77-
}
78-
7959
}

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

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@
2020

2121
import org.jspecify.annotations.Nullable;
2222

23-
import org.springframework.http.MediaType;
2423
import org.springframework.http.converter.HttpMessageConverter;
2524
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
26-
import org.springframework.http.converter.StringHttpMessageConverter;
2725
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
2826

2927
@SuppressWarnings("deprecation")
@@ -48,32 +46,14 @@ public void customize(ServerBuilder builder) {
4846
else {
4947
builder.registerDefaults();
5048
this.converters.forEach((converter) -> {
51-
if (converter instanceof StringHttpMessageConverter) {
52-
builder.withStringConverter(converter);
53-
}
54-
else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
49+
if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
5550
builder.withKotlinSerializationJsonConverter(converter);
5651
}
57-
else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) {
58-
builder.withJsonConverter(converter);
59-
}
60-
else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) {
61-
builder.withXmlConverter(converter);
62-
}
6352
else {
6453
builder.addCustomConverter(converter);
6554
}
6655
});
6756
}
6857
}
6958

70-
private static boolean supportsMediaType(HttpMessageConverter<?> converter, MediaType mediaType) {
71-
for (MediaType supportedMediaType : converter.getSupportedMediaTypes()) {
72-
if (supportedMediaType.equalsTypeAndSubtype(mediaType)) {
73-
return true;
74-
}
75-
}
76-
return false;
77-
}
78-
7959
}

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@
2727
import org.springframework.context.annotation.Bean;
2828
import org.springframework.context.annotation.Conditional;
2929
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
31+
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
3032
import org.springframework.http.converter.json.GsonHttpMessageConverter;
31-
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
3233

3334
/**
3435
* Configuration for HTTP Message converters that use Gson.
3536
*
3637
* @author Andy Wilkinson
3738
* @author Eddú Meléndez
39+
* @author Brian Clozel
3840
*/
3941
@Configuration(proxyBeanMethods = false)
4042
@ConditionalOnClass(Gson.class)
@@ -46,11 +48,30 @@ class GsonHttpMessageConvertersConfiguration {
4648
static class GsonHttpMessageConverterConfiguration {
4749

4850
@Bean
49-
@ConditionalOnMissingBean
50-
GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) {
51-
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
52-
converter.setGson(gson);
53-
return converter;
51+
@ConditionalOnMissingBean(GsonHttpMessageConverter.class)
52+
GsonHttpConvertersCustomizer gsonHttpMessageConvertersCustomizer(Gson gson) {
53+
return new GsonHttpConvertersCustomizer(gson);
54+
}
55+
56+
}
57+
58+
static class GsonHttpConvertersCustomizer
59+
implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer {
60+
61+
private final GsonHttpMessageConverter converter;
62+
63+
GsonHttpConvertersCustomizer(Gson gson) {
64+
this.converter = new GsonHttpMessageConverter(gson);
65+
}
66+
67+
@Override
68+
public void customize(ClientBuilder builder) {
69+
builder.withJsonConverter(this.converter);
70+
}
71+
72+
@Override
73+
public void customize(ServerBuilder builder) {
74+
builder.withJsonConverter(this.converter);
5475
}
5576

5677
}
@@ -80,13 +101,13 @@ private static class JacksonAndJsonbUnavailableCondition extends NoneNestedCondi
80101
super(ConfigurationPhase.REGISTER_BEAN);
81102
}
82103

83-
@ConditionalOnBean(JacksonJsonHttpMessageConverter.class)
104+
@ConditionalOnBean(JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer.class)
84105
static class JacksonAvailable {
85106

86107
}
87108

88109
@SuppressWarnings("removal")
89-
@ConditionalOnBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)
110+
@ConditionalOnBean(Jackson2HttpMessageConvertersConfiguration.Jackson2JsonMessageConvertersCustomizer.class)
90111
static class Jackson2Available {
91112

92113
}

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.springframework.context.annotation.Import;
3333
import org.springframework.core.annotation.Order;
3434
import org.springframework.http.converter.HttpMessageConverter;
35+
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
36+
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
3537
import org.springframework.http.converter.StringHttpMessageConverter;
3638

3739
/**
@@ -86,17 +88,36 @@ ServerHttpMessageConvertersCustomizer serverConvertersCustomizer(
8688
}
8789

8890
@Configuration(proxyBeanMethods = false)
89-
@ConditionalOnClass(StringHttpMessageConverter.class)
9091
@EnableConfigurationProperties(HttpMessageConvertersProperties.class)
9192
protected static class StringHttpMessageConverterConfiguration {
9293

9394
@Bean
94-
@ConditionalOnMissingBean
95-
StringHttpMessageConverter stringHttpMessageConverter(HttpMessageConvertersProperties properties) {
96-
StringHttpMessageConverter converter = new StringHttpMessageConverter(
97-
properties.getStringEncodingCharset());
98-
converter.setWriteAcceptCharset(false);
99-
return converter;
95+
@ConditionalOnMissingBean(StringHttpMessageConverter.class)
96+
StringHttpMessageConvertersCustomizer stringHttpMessageConvertersCustomizer(
97+
HttpMessageConvertersProperties properties) {
98+
return new StringHttpMessageConvertersCustomizer(properties);
99+
}
100+
101+
}
102+
103+
static class StringHttpMessageConvertersCustomizer
104+
implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer {
105+
106+
StringHttpMessageConverter converter;
107+
108+
StringHttpMessageConvertersCustomizer(HttpMessageConvertersProperties properties) {
109+
this.converter = new StringHttpMessageConverter(properties.getStringEncodingCharset());
110+
this.converter.setWriteAcceptCharset(false);
111+
}
112+
113+
@Override
114+
public void customize(ClientBuilder builder) {
115+
builder.withStringConverter(this.converter);
116+
}
117+
118+
@Override
119+
public void customize(ServerBuilder builder) {
120+
builder.withStringConverter(this.converter);
100121
}
101122

102123
}

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

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@
2424
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2626
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
27+
import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConvertersCustomizer;
2728
import org.springframework.context.annotation.Bean;
2829
import org.springframework.context.annotation.Conditional;
2930
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
32+
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
3033
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
31-
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
32-
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
3334

3435
/**
3536
* Configuration for HTTP message converters that use Jackson 2.
3637
*
3738
* @author Andy Wilkinson
39+
* @author Brian Clozel
3840
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
3941
*/
4042
@Configuration(proxyBeanMethods = false)
4143
@Deprecated(since = "4.0.0", forRemoval = true)
42-
@SuppressWarnings({ "deprecation", "removal" })
44+
@SuppressWarnings("removal")
4345
class Jackson2HttpMessageConvertersConfiguration {
4446

4547
@Configuration(proxyBeanMethods = false)
@@ -49,10 +51,9 @@ class Jackson2HttpMessageConvertersConfiguration {
4951
static class MappingJackson2HttpMessageConverterConfiguration {
5052

5153
@Bean
52-
@ConditionalOnMissingBean
53-
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(
54-
ObjectMapper objectMapper) {
55-
return new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(objectMapper);
54+
@ConditionalOnMissingBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)
55+
Jackson2JsonMessageConvertersCustomizer jackson2HttpMessageConvertersCustomizer(ObjectMapper objectMapper) {
56+
return new Jackson2JsonMessageConvertersCustomizer(objectMapper);
5657
}
5758

5859
}
@@ -63,10 +64,56 @@ org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mapp
6364
protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
6465

6566
@Bean
66-
@ConditionalOnMissingBean
67-
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(
67+
@ConditionalOnMissingBean(org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter.class)
68+
Jackson2XmlMessageConvertersCustomizer mappingJackson2XmlHttpMessageConverter(
6869
Jackson2ObjectMapperBuilder builder) {
69-
return new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build());
70+
return new Jackson2XmlMessageConvertersCustomizer(builder.createXmlMapper(true).build());
71+
}
72+
73+
}
74+
75+
static class Jackson2JsonMessageConvertersCustomizer
76+
implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer {
77+
78+
private final ObjectMapper objectMapper;
79+
80+
Jackson2JsonMessageConvertersCustomizer(ObjectMapper objectMapper) {
81+
this.objectMapper = objectMapper;
82+
}
83+
84+
@Override
85+
public void customize(ClientBuilder builder) {
86+
builder.withJsonConverter(
87+
new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(this.objectMapper));
88+
}
89+
90+
@Override
91+
public void customize(ServerBuilder builder) {
92+
builder.withJsonConverter(
93+
new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(this.objectMapper));
94+
}
95+
96+
}
97+
98+
static class Jackson2XmlMessageConvertersCustomizer
99+
implements ClientHttpMessageConvertersCustomizer, ServerHttpMessageConvertersCustomizer {
100+
101+
private final ObjectMapper objectMapper;
102+
103+
Jackson2XmlMessageConvertersCustomizer(ObjectMapper objectMapper) {
104+
this.objectMapper = objectMapper;
105+
}
106+
107+
@Override
108+
public void customize(ClientBuilder builder) {
109+
builder.withXmlConverter(new org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter(
110+
this.objectMapper));
111+
}
112+
113+
@Override
114+
public void customize(ServerBuilder builder) {
115+
builder.withXmlConverter(new org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter(
116+
this.objectMapper));
70117
}
71118

72119
}
@@ -83,7 +130,7 @@ static class Jackson2Preferred {
83130

84131
}
85132

86-
@ConditionalOnMissingBean(JacksonJsonHttpMessageConverter.class)
133+
@ConditionalOnMissingBean(JacksonJsonHttpMessageConvertersCustomizer.class)
87134
static class JacksonUnavailable {
88135

89136
}

0 commit comments

Comments
 (0)