From bf8e0c45a8d84dc473404f033e07858aa3d0d52b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 27 Oct 2025 12:33:34 +0000 Subject: [PATCH] Allow customizations of default customizers to be overridden Previously, the default client and server HTTP message converter customizers were ordered with lowest precedence. This made it impossible to guarantee that another customizer would be able to go after the default customizers, preventing their customizations from being reliably overridden. This commit updates the definitions of the default customizers to order them at 0. This allows additional customizers to be ordered either before or after them. Usage of the customizers is now always ordered (previously Spring MVC's was not). See gh-47798 --- ...ttpMessageConvertersAutoConfiguration.java | 5 +- ...ssageConvertersAutoConfigurationTests.java | 23 ++++++++++ .../RestClientAutoConfigurationTests.java | 46 +++++++++++++++++++ .../RestTemplateAutoConfigurationTests.java | 46 +++++++++++++++++++ .../RestTestClientAutoConfigurationTests.java | 45 ++++++++++++++++++ .../WebMvcAutoConfiguration.java | 3 +- .../WebMvcAutoConfigurationTests.java | 45 ++++++++++++++++++ 7 files changed, 209 insertions(+), 4 deletions(-) diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java index bace77bb3f6..db2083a355a 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java @@ -30,7 +30,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -67,7 +66,7 @@ public final class HttpMessageConvertersAutoConfiguration { static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; @Bean - @Order(Ordered.LOWEST_PRECEDENCE) + @Order(0) @SuppressWarnings("deprecation") ClientHttpMessageConvertersCustomizer clientConvertersCustomizer( ObjectProvider legacyConverters, @@ -77,7 +76,7 @@ public final class HttpMessageConvertersAutoConfiguration { } @Bean - @Order(Ordered.LOWEST_PRECEDENCE) + @Order(0) @SuppressWarnings("deprecation") ServerHttpMessageConvertersCustomizer serverConvertersCustomizer( ObjectProvider legacyConverters, 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 e636dc87b8d..b22d73864d7 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 @@ -18,6 +18,7 @@ package org.springframework.boot.http.converter.autoconfigure; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; @@ -29,6 +30,7 @@ import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.xml.XmlMapper; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConverterConfiguration; @@ -348,6 +350,27 @@ class HttpMessageConvertersAutoConfigurationTests { }); } + @Test + void defaultServerConvertersCustomizerHasOrderZero() { + defaultConvertersCustomizerHasOrderZero(DefaultServerHttpMessageConvertersCustomizer.class); + } + + @Test + void defaultClientConvertersCustomizerHasOrderZero() { + defaultConvertersCustomizerHasOrderZero(DefaultClientHttpMessageConvertersCustomizer.class); + } + + private void defaultConvertersCustomizerHasOrderZero(Class customizerType) { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .run((context) -> { + Map customizers = context.getBeansOfType(customizerType); + assertThat(customizers).hasSize(1); + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory(); + customizers.keySet().forEach((beanName) -> assertThat(beanFactory.getOrder(beanName)).isZero()); + }); + } + private ApplicationContextRunner allOptionsRunner() { return this.contextRunner.withBean(Gson.class) .withBean(JsonMapper.class) diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java index 7816e0a2a68..1e08fbc2dda 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java @@ -22,6 +22,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; +import org.mockito.InOrder; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; @@ -31,6 +32,7 @@ import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; import org.springframework.boot.http.client.autoconfigure.imperative.ImperativeHttpClientAutoConfiguration; import org.springframework.boot.http.client.autoconfigure.reactive.ReactiveHttpClientAutoConfiguration; +import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.boot.ssl.SslBundle; @@ -40,9 +42,11 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.Builder; @@ -50,6 +54,7 @@ import org.springframework.web.client.RestClient.Builder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -296,6 +301,24 @@ class RestClientAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(RestClient.Builder.class)); } + @Test + void clientHttpMessageConverterCustomizersAreAppliedInOrder() { + this.contextRunner.withUserConfiguration(ClientHttpMessageConverterCustomizersConfiguration.class) + .run((context) -> { + context.getBean(RestClient.Builder.class).build(); + ClientHttpMessageConvertersCustomizer customizer1 = context.getBean("customizer1", + ClientHttpMessageConvertersCustomizer.class); + ClientHttpMessageConvertersCustomizer customizer2 = context.getBean("customizer2", + ClientHttpMessageConvertersCustomizer.class); + ClientHttpMessageConvertersCustomizer customizer3 = context.getBean("customizer3", + ClientHttpMessageConvertersCustomizer.class); + InOrder inOrder = inOrder(customizer1, customizer2, customizer3); + inOrder.verify(customizer3).customize(any(ClientBuilder.class)); + inOrder.verify(customizer1).customize(any(ClientBuilder.class)); + inOrder.verify(customizer2).customize(any(ClientBuilder.class)); + }); + } + @Configuration(proxyBeanMethods = false) static class RestClientCustomizerConfig { @@ -334,4 +357,27 @@ class RestClientAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ClientHttpMessageConverterCustomizersConfiguration { + + @Bean + @Order(-5) + ClientHttpMessageConvertersCustomizer customizer1() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(5) + ClientHttpMessageConvertersCustomizer customizer2() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(-10) + ClientHttpMessageConvertersCustomizer customizer3() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + } + } diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java index f488ed0e53b..c35411ab694 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java @@ -19,11 +19,13 @@ package org.springframework.boot.restclient.autoconfigure; import java.util.Collections; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; import org.springframework.boot.http.client.autoconfigure.imperative.ImperativeHttpClientAutoConfiguration; +import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.restclient.RestTemplateCustomizer; @@ -33,11 +35,13 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters.ClientBuilder; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.web.client.RestTemplate; @@ -47,6 +51,7 @@ import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -192,6 +197,24 @@ class RestTemplateAutoConfigurationTests { }); } + @Test + void clientHttpMessageConverterCustomizersAreAppliedInOrder() { + this.contextRunner.withUserConfiguration(ClientHttpMessageConverterCustomizersConfiguration.class) + .run((context) -> { + context.getBean(RestTemplateBuilder.class).build(); + ClientHttpMessageConvertersCustomizer customizer1 = context.getBean("customizer1", + ClientHttpMessageConvertersCustomizer.class); + ClientHttpMessageConvertersCustomizer customizer2 = context.getBean("customizer2", + ClientHttpMessageConvertersCustomizer.class); + ClientHttpMessageConvertersCustomizer customizer3 = context.getBean("customizer3", + ClientHttpMessageConvertersCustomizer.class); + InOrder inOrder = inOrder(customizer1, customizer2, customizer3); + inOrder.verify(customizer3).customize(any(ClientBuilder.class)); + inOrder.verify(customizer1).customize(any(ClientBuilder.class)); + inOrder.verify(customizer2).customize(any(ClientBuilder.class)); + }); + } + @Configuration(proxyBeanMethods = false) static class RestTemplateConfig { @@ -283,6 +306,29 @@ class RestTemplateAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ClientHttpMessageConverterCustomizersConfiguration { + + @Bean + @Order(-5) + ClientHttpMessageConvertersCustomizer customizer1() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(5) + ClientHttpMessageConvertersCustomizer customizer2() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(-10) + ClientHttpMessageConvertersCustomizer customizer3() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + } + static class CustomHttpMessageConverter extends ByteArrayHttpMessageConverter { } diff --git a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/RestTestClientAutoConfigurationTests.java b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/RestTestClientAutoConfigurationTests.java index 620b3c829f2..1282790372b 100644 --- a/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/RestTestClientAutoConfigurationTests.java +++ b/module/spring-boot-resttestclient/src/test/java/org/springframework/boot/resttestclient/autoconfigure/RestTestClientAutoConfigurationTests.java @@ -19,8 +19,10 @@ package org.springframework.boot.resttestclient.autoconfigure; import org.assertj.core.extractor.Extractors; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.http.server.LocalTestWebServer; @@ -28,6 +30,8 @@ import org.springframework.boot.test.http.server.LocalTestWebServer.Scheme; import org.springframework.boot.testsupport.classpath.resources.WithResource; 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.test.web.servlet.client.RestTestClient; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriBuilderFactory; @@ -35,6 +39,7 @@ import org.springframework.web.util.UriBuilderFactory; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -86,6 +91,23 @@ class RestTestClientAutoConfigurationTests { }); } + @Test + void clientHttpMessageConverterCustomizersAreAppliedInOrder() { + this.contextRunner.withUserConfiguration(ClientHttpMessageConverterCustomizersConfiguration.class) + .run((context) -> { + ClientHttpMessageConvertersCustomizer customizer1 = context.getBean("customizer1", + ClientHttpMessageConvertersCustomizer.class); + ClientHttpMessageConvertersCustomizer customizer2 = context.getBean("customizer2", + ClientHttpMessageConvertersCustomizer.class); + ClientHttpMessageConvertersCustomizer customizer3 = context.getBean("customizer3", + ClientHttpMessageConvertersCustomizer.class); + InOrder inOrder = inOrder(customizer1, customizer2, customizer3); + inOrder.verify(customizer3).customize(any(ClientBuilder.class)); + inOrder.verify(customizer1).customize(any(ClientBuilder.class)); + inOrder.verify(customizer2).customize(any(ClientBuilder.class)); + }); + } + @Configuration(proxyBeanMethods = false) static class RestTestClientCustomConfig { @@ -106,4 +128,27 @@ class RestTestClientAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ClientHttpMessageConverterCustomizersConfiguration { + + @Bean + @Order(-5) + ClientHttpMessageConvertersCustomizer customizer1() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(5) + ClientHttpMessageConvertersCustomizer customizer2() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(-10) + ClientHttpMessageConvertersCustomizer customizer3() { + return mock(ClientHttpMessageConvertersCustomizer.class); + } + + } + } diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index 53ad6523acb..28e042fb836 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -244,7 +244,8 @@ public final class WebMvcAutoConfiguration { @Override public void configureMessageConverters(ServerBuilder builder) { - this.httpMessageConvertersCustomizerProvider.forEach((customizer) -> customizer.customize(builder)); + this.httpMessageConvertersCustomizerProvider.orderedStream() + .forEach((customizer) -> customizer.customize(builder)); } @Override diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index 8f51b73bce7..c8384e7c9ac 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -42,12 +42,14 @@ import org.aspectj.lang.annotation.Aspect; import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.springframework.aop.support.AopUtils; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.http.converter.autoconfigure.ServerHttpMessageConvertersCustomizer; import org.springframework.boot.servlet.filter.OrderedFormContentFilter; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -83,6 +85,7 @@ import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; import org.springframework.http.server.RequestPath; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.util.ReflectionTestUtils; @@ -156,6 +159,8 @@ import org.springframework.web.util.UrlPathHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -1115,6 +1120,23 @@ class WebMvcAutoConfigurationTests { }); } + @Test + void serverHttpMessageConverterCustomizersAreAppliedInOrder() { + this.contextRunner.withUserConfiguration(ServerHttpMessageConverterCustomizersConfiguration.class) + .run((context) -> { + ServerHttpMessageConvertersCustomizer customizer1 = context.getBean("customizer1", + ServerHttpMessageConvertersCustomizer.class); + ServerHttpMessageConvertersCustomizer customizer2 = context.getBean("customizer2", + ServerHttpMessageConvertersCustomizer.class); + ServerHttpMessageConvertersCustomizer customizer3 = context.getBean("customizer3", + ServerHttpMessageConvertersCustomizer.class); + InOrder inOrder = inOrder(customizer1, customizer2, customizer3); + inOrder.verify(customizer3).customize(any(ServerBuilder.class)); + inOrder.verify(customizer1).customize(any(ServerBuilder.class)); + inOrder.verify(customizer2).customize(any(ServerBuilder.class)); + }); + } + private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context, Consumer handlerConsumer) { Map handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class)); @@ -1701,4 +1723,27 @@ class WebMvcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ServerHttpMessageConverterCustomizersConfiguration { + + @Bean + @Order(-5) + ServerHttpMessageConvertersCustomizer customizer1() { + return mock(ServerHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(5) + ServerHttpMessageConvertersCustomizer customizer2() { + return mock(ServerHttpMessageConvertersCustomizer.class); + } + + @Bean + @Order(-10) + ServerHttpMessageConvertersCustomizer customizer3() { + return mock(ServerHttpMessageConvertersCustomizer.class); + } + + } + }