From 9d1db6830f02b3d35dfca6ed2a166b0d19505003 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 29 Dec 2025 13:41:12 +0100 Subject: [PATCH] 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 --- ...sonHttpMessageConvertersConfiguration.java | 2 + ...on2HttpMessageConvertersConfiguration.java | 3 + ...sonHttpMessageConvertersConfiguration.java | 3 + ...onbHttpMessageConvertersConfiguration.java | 2 + ...ionHttpMessageConvertersConfiguration.java | 2 + ...ssageConvertersAutoConfigurationTests.java | 68 +++++++++++++++++-- 6 files changed, 76 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java index 34f1e551e65..5c9c7cb522a 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java @@ -27,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); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java index 151ef3237e1..c3ec3b21bcd 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java @@ -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) { diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java index 9207b1986c1..25107d944a9 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JacksonHttpMessageConvertersConfiguration.java @@ -25,6 +25,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); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java index fa6a182e7f3..b246446ba3a 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java @@ -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); diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java index 6129b0edbec..5c4ca821254 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/KotlinSerializationHttpMessageConvertersConfiguration.java @@ -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) { diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java index a6c9e258c50..a5a5adf5a74 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java @@ -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 { + + 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 { + + } + + } + }