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
This commit is contained in:
Andy Wilkinson
2025-10-27 12:41:31 +00:00
parent 5fbdfad11e
commit bf8e0c45a8
7 changed files with 209 additions and 4 deletions
@@ -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<HttpMessageConverters> legacyConverters,
@@ -77,7 +76,7 @@ public final class HttpMessageConvertersAutoConfiguration {
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
@Order(0)
@SuppressWarnings("deprecation")
ServerHttpMessageConvertersCustomizer serverConvertersCustomizer(
ObjectProvider<HttpMessageConverters> legacyConverters,
@@ -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 <T> void defaultConvertersCustomizerHasOrderZero(Class<T> customizerType) {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> {
Map<String, T> 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)
@@ -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);
}
}
}
@@ -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 {
}
@@ -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);
}
}
}
@@ -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
@@ -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<ResourceHttpRequestHandler> handlerConsumer) {
Map<String, Object> 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);
}
}
}