Fix message converter customizers order

Prior to this commit, gh-48310 separated client and server message
converter configurations by switching from message converter instances
as beans in the application context, to server/client customizers that
are applied to the `HttpMessageConverters` instances while being built.

This change did not order the new ClientHttpMessageConvertersCustomizer
or ServerHttpMessageConvertersCustomizer, letting those being at the
"lowest precedence" default. As customizers, this means they are applied
last and custom instances cannot take over.

This commit ensures that such customizers provided by Spring Boot are
now ordered at "0" to let applications ones take over.

Fixes gh-48635
This commit is contained in:
Brian Clozel
2025-12-29 14:39:43 +01:00
committed by Stéphane Nicoll
parent 5d1d5173ab
commit 9d1db6830f
6 changed files with 76 additions and 4 deletions
@@ -27,6 +27,7 @@ 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.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
@@ -48,6 +49,7 @@ class GsonHttpMessageConvertersConfiguration {
static class GsonHttpMessageConverterConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(GsonHttpMessageConverter.class)
GsonHttpConvertersCustomizer gsonHttpMessageConvertersCustomizer(Gson gson) {
return new GsonHttpConvertersCustomizer(gson);
@@ -28,6 +28,7 @@ import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageC
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@@ -51,6 +52,7 @@ class Jackson2HttpMessageConvertersConfiguration {
static class MappingJackson2HttpMessageConverterConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class)
Jackson2JsonMessageConvertersCustomizer jackson2HttpMessageConvertersCustomizer(ObjectMapper objectMapper) {
return new Jackson2JsonMessageConvertersCustomizer(objectMapper);
@@ -64,6 +66,7 @@ class Jackson2HttpMessageConvertersConfiguration {
protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter.class)
Jackson2XmlMessageConvertersCustomizer mappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder builder) {
@@ -25,6 +25,7 @@ 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.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
@@ -47,6 +48,7 @@ class JacksonHttpMessageConvertersConfiguration {
static class JacksonJsonHttpMessageConverterConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(value = JacksonJsonHttpMessageConverter.class,
ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter",
"org.springframework.data.rest.webmvc.alps.AlpsJacksonJsonHttpMessageConverter" })
@@ -62,6 +64,7 @@ class JacksonHttpMessageConvertersConfiguration {
protected static class JacksonXmlHttpMessageConverterConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(JacksonXmlHttpMessageConverter.class)
JacksonXmlHttpMessageConvertersCustomizer jacksonXmlHttpMessageConvertersCustomizer(XmlMapper xmlMapper) {
return new JacksonXmlHttpMessageConvertersCustomizer(xmlMapper);
@@ -28,6 +28,7 @@ import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageC
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
@@ -47,6 +48,7 @@ class JsonbHttpMessageConvertersConfiguration {
static class JsonbHttpMessageConverterConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(JsonbHttpMessageConverter.class)
JsonbHttpMessageConvertersCustomizer jsonbHttpMessageConvertersCustomizer(Jsonb jsonb) {
return new JsonbHttpMessageConvertersCustomizer(jsonb);
@@ -24,6 +24,7 @@ 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.core.annotation.Order;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
@@ -42,6 +43,7 @@ import org.springframework.util.ClassUtils;
class KotlinSerializationHttpMessageConvertersConfiguration {
@Bean
@Order(0)
@ConditionalOnMissingBean(KotlinSerializationJsonHttpMessageConverter.class)
KotlinSerializationJsonConvertersCustomizer kotlinSerializationJsonConvertersCustomizer(Json json,
ResourceLoader resourceLoader) {
@@ -16,6 +16,7 @@
package org.springframework.boot.http.converter.autoconfigure;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -52,11 +53,16 @@ import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguratio
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsHttpMessageConverter;
import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
@@ -103,6 +109,14 @@ class HttpMessageConvertersAutoConfigurationTests {
});
}
@Test
void jacksonServerCustomizer() {
this.contextRunner.withUserConfiguration(CustomJsonConverterConfig.class).run((context) -> {
assertConverterIsNotRegistered(context, JacksonJsonHttpMessageConverter.class);
assertConverterIsRegistered(context, CustomConverter.class);
});
}
@Test
void jacksonConverterWithBuilder() {
this.contextRunner.withUserConfiguration(JacksonJsonMapperBuilderConfig.class).run((context) -> {
@@ -455,16 +469,16 @@ class HttpMessageConvertersAutoConfigurationTests {
private HttpMessageConverters getClientConverters(ApplicationContext context) {
ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults();
context.getBeansOfType(ClientHttpMessageConvertersCustomizer.class)
.values()
context.getBeanProvider(ClientHttpMessageConvertersCustomizer.class)
.orderedStream()
.forEach((customizer) -> customizer.customize(clientBuilder));
return clientBuilder.build();
}
private HttpMessageConverters getServerConverters(ApplicationContext context) {
ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults();
context.getBeansOfType(ServerHttpMessageConvertersCustomizer.class)
.values()
context.getBeanProvider(ServerHttpMessageConvertersCustomizer.class)
.orderedStream()
.forEach((customizer) -> customizer.customize(serverBuilder));
return serverBuilder.build();
}
@@ -503,6 +517,26 @@ class HttpMessageConvertersAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomJsonConverterConfig {
@Bean
JsonMapper jsonMapper() {
return new JsonMapper();
}
@Bean
ServerHttpMessageConvertersCustomizer jsonServerCustomizer() {
return (configurer) -> configurer.withJsonConverter(new CustomConverter(MediaType.APPLICATION_JSON));
}
@Bean
ClientHttpMessageConvertersCustomizer jsonClientCustomizer() {
return (configurer) -> configurer.withJsonConverter(new CustomConverter(MediaType.APPLICATION_JSON));
}
}
@Configuration(proxyBeanMethods = false)
static class JacksonJsonMapperBuilderConfig {
@@ -640,4 +674,30 @@ class HttpMessageConvertersAutoConfigurationTests {
}
@SuppressWarnings("NullAway")
static class CustomConverter extends AbstractHttpMessageConverter<Object> {
CustomConverter(MediaType supportedMediaType) {
super(supportedMediaType);
}
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected void writeInternal(Object o, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
}
}
}