Deprecate HttpMessageConverters for Framework's

Prior to this commit, Spring Boot had an  `HttpMessageConverters` class
that allowed, to configure message converter instances for MVC server
applications and traditional Spring HTTP clients.

As of Spring Framework 7.0, Framework ships its own
`HttpMessageConverters` class, aligning with the existing codecs
configuration on the WebFlux side. As a result, a few methods taking
`List<HttpMessageConverter>` as arguments were deprecated in favor of
the new arrangement.

This commit adapts to the Framework changes by deprecating Boot's
`HttpMessageConverters` in favor of Framework's. This splits the client
and server configuration as they are meant to be managed separately.
Applications can still contribute `HttpMessageConverters` (Boot's
variant) beans but the type itself is now deprecated.
Instead, applications should now contribute
`ClientHttpMessageConvertersCustomizer` and
`ServerHttpMessageConvertersCustomizer` beans to customize message
converters.

Closes gh-46411
This commit is contained in:
Brian Clozel 2025-10-02 15:22:13 +02:00
parent 418e057afc
commit 92ee73df30
25 changed files with 361 additions and 170 deletions

View File

@ -97,13 +97,11 @@ By default, strings are encoded in `UTF-8`.
Any javadoc:org.springframework.http.converter.HttpMessageConverter[] bean that is present in the context is added to the list of converters.
You can also override default converters in the same way.
If you need to add or customize converters, you can use Spring Boot's javadoc:org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters[] class, as shown in the following listing:
If you need to add or customize converters, you can declare one or more javadoc:org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer[] or
javadoc:org.springframework.boot.http.converter.autoconfigure.ServerHttpMessageConvertersCustomizer[] as beans, as shown in the following listing:
include-code::MyHttpMessageConvertersConfiguration[]
For further control, you can also sub-class javadoc:org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters[] and override its `postProcessConverters` and/or `postProcessPartConverters` methods.
This can be useful when you want to re-order or remove some of the converters that Spring MVC configures by default.
[[web.servlet.spring-mvc.message-codes]]

View File

@ -16,19 +16,17 @@
package org.springframework.boot.docs.web.servlet.springmvc.messageconverters;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
return new HttpMessageConverters(additional, another);
public ClientHttpMessageConvertersCustomizer myClientConvertersCustomizer() {
return (clientBuilder) -> clientBuilder.customMessageConverter(new AdditionalHttpMessageConverter())
.customMessageConverter(new AnotherHttpMessageConverter());
}
}

View File

@ -16,19 +16,21 @@
package org.springframework.boot.docs.web.servlet.springmvc.messageconverters
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.converter.HttpMessageConverters
@Configuration(proxyBeanMethods = false)
class MyHttpMessageConvertersConfiguration {
@Bean
fun customConverters(): HttpMessageConverters {
val additional: HttpMessageConverter<*> = AdditionalHttpMessageConverter()
val another: HttpMessageConverter<*> = AnotherHttpMessageConverter()
return HttpMessageConverters(additional, another)
fun myClientConvertersCustomizer(): ClientHttpMessageConvertersCustomizer {
return ClientHttpMessageConvertersCustomizer { clientBuilder: HttpMessageConverters.ClientBuilder ->
clientBuilder
.customMessageConverter(AdditionalHttpMessageConverter())
.customMessageConverter(AnotherHttpMessageConverter())
}
}
}

View File

@ -40,7 +40,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties;
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
@ -177,21 +176,20 @@ public final class GraphQlWebMvcAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HttpMessageConverters.class, ServerContainer.class, WebSocketHandler.class })
@ConditionalOnClass({ HttpMessageConverter.class, ServerContainer.class, WebSocketHandler.class })
@ConditionalOnProperty("spring.graphql.websocket.path")
static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, HttpMessageConverters converters) {
GraphQlProperties properties, ObjectProvider<HttpMessageConverter<?>> converters) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive());
}
private HttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
return converters.getConverters()
.stream()
private HttpMessageConverter<Object> getJsonConverter(ObjectProvider<HttpMessageConverter<?>> converters) {
return converters.orderedStream()
.filter(this::canReadJsonMap)
.findFirst()
.map(this::asObjectHttpMessageConverter)

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.http.converter.autoconfigure;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
/**
* Callback interface that can be used to customize a {@link HttpMessageConverters} for
* client usage.
*
* @author Brian Clozel
* @since 4.0
*/
@FunctionalInterface
public interface ClientHttpMessageConvertersCustomizer {
/**
* Callback to customize a {@link HttpMessageConverters.ClientBuilder} instance.
* @param builder the builder to customize
*/
void customize(ClientBuilder builder);
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.http.converter.autoconfigure;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
@SuppressWarnings("deprecation")
class DefaultClientHttpMessageConvertersCustomizer implements ClientHttpMessageConvertersCustomizer {
private final @Nullable HttpMessageConverters legacyConverters;
private final Collection<HttpMessageConverter<?>> converters;
DefaultClientHttpMessageConvertersCustomizer(@Nullable HttpMessageConverters legacyConverters,
Collection<HttpMessageConverter<?>> converters) {
this.legacyConverters = legacyConverters;
this.converters = converters;
}
@Override
public void customize(ClientBuilder builder) {
if (this.legacyConverters != null) {
this.legacyConverters.forEach(builder::customMessageConverter);
}
else {
builder.registerDefaults();
this.converters.forEach((converter) -> {
if (converter instanceof StringHttpMessageConverter) {
builder.stringMessageConverter(converter);
}
else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
builder.customMessageConverter(converter);
}
else if (converter.getSupportedMediaTypes().contains(MediaType.APPLICATION_JSON)) {
builder.jsonMessageConverter(converter);
}
else {
builder.customMessageConverter(converter);
}
});
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.http.converter.autoconfigure;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
@SuppressWarnings("deprecation")
class DefaultServerHttpMessageConvertersCustomizer implements ServerHttpMessageConvertersCustomizer {
private final @Nullable HttpMessageConverters legacyConverters;
private final Collection<HttpMessageConverter<?>> converters;
DefaultServerHttpMessageConvertersCustomizer(@Nullable HttpMessageConverters legacyConverters,
Collection<HttpMessageConverter<?>> converters) {
this.legacyConverters = legacyConverters;
this.converters = converters;
}
@Override
public void customize(ServerBuilder builder) {
if (this.legacyConverters != null) {
this.legacyConverters.forEach(builder::customMessageConverter);
}
else {
builder.registerDefaults();
this.converters.forEach((converter) -> {
if (converter instanceof StringHttpMessageConverter) {
builder.stringMessageConverter(converter);
}
else if (converter instanceof KotlinSerializationJsonHttpMessageConverter) {
builder.customMessageConverter(converter);
}
else if (converter.getSupportedMediaTypes().contains(MediaType.APPLICATION_JSON)) {
builder.jsonMessageConverter(converter);
}
else {
builder.customMessageConverter(converter);
}
});
}
}
}

View File

@ -53,7 +53,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupp
* @see #HttpMessageConverters(HttpMessageConverter...)
* @see #HttpMessageConverters(Collection)
* @see #getConverters()
* @deprecated since 4.0 in favor of {@link ClientHttpMessageConvertersCustomizer} and
* {@link ServerHttpMessageConvertersCustomizer}.
*/
@Deprecated(since = "4.0")
public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> {
private static final List<Class<?>> NON_REPLACING_CONVERTERS;

View File

@ -30,6 +30,8 @@ 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;
@ -46,6 +48,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Dmitry Sulman
* @author Brian Clozel
* @since 4.0.0
*/
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
@ -61,9 +64,23 @@ public final class HttpMessageConvertersAutoConfiguration {
static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
@Bean
@ConditionalOnMissingBean
HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().toList());
@Order(Ordered.LOWEST_PRECEDENCE)
@SuppressWarnings("deprecation")
ClientHttpMessageConvertersCustomizer clientConvertersCustomizer(
ObjectProvider<HttpMessageConverters> legacyConverters,
ObjectProvider<HttpMessageConverter<?>> converters) {
return new DefaultClientHttpMessageConvertersCustomizer(legacyConverters.getIfAvailable(),
converters.orderedStream().toList());
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
@SuppressWarnings("deprecation")
ServerHttpMessageConvertersCustomizer serverConvertersCustomizer(
ObjectProvider<HttpMessageConverters> legacyConverters,
ObjectProvider<HttpMessageConverter<?>> converters) {
return new DefaultServerHttpMessageConvertersCustomizer(legacyConverters.getIfAvailable(),
converters.orderedStream().toList());
}
@Configuration(proxyBeanMethods = false)

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.http.converter.autoconfigure;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
/**
* Callback interface that can be used to customize a {@link HttpMessageConverters} for
* server usage.
*
* @author Brian Clozel
* @since 4.0
*/
@FunctionalInterface
public interface ServerHttpMessageConvertersCustomizer {
/**
* Callback to customize a {@link ServerBuilder} instance.
* @param builder the builder to customize
*/
void customize(ServerBuilder builder);
}

View File

@ -44,6 +44,9 @@ import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguratio
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter;
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.StringHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
@ -262,14 +265,16 @@ class HttpMessageConvertersAutoConfigurationTests {
void whenServletWebApplicationHttpMessageConvertersIsConfigured() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(HttpMessageConverters.class));
.run((context) -> assertThat(context).hasSingleBean(ServerHttpMessageConvertersCustomizer.class)
.hasSingleBean(ClientHttpMessageConvertersCustomizer.class));
}
@Test
void whenReactiveWebApplicationHttpMessageConvertersIsNotConfigured() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(HttpMessageConverters.class));
.run((context) -> assertThat(context).doesNotHaveBean(ServerHttpMessageConvertersCustomizer.class)
.doesNotHaveBean(ClientHttpMessageConvertersCustomizer.class));
}
@Test
@ -318,15 +323,33 @@ class HttpMessageConvertersAutoConfigurationTests {
private void assertConverterBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context,
Class<? extends HttpMessageConverter<?>> type) {
HttpMessageConverter<?> converter = context.getBean(type);
HttpMessageConverters converters = context.getBean(HttpMessageConverters.class);
assertThat(converters.getConverters()).contains(converter);
ClientHttpMessageConvertersCustomizer clientCustomizer = context
.getBean(ClientHttpMessageConvertersCustomizer.class);
ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults();
clientCustomizer.customize(clientBuilder);
assertThat(clientBuilder.build()).contains(converter);
ServerHttpMessageConvertersCustomizer serverCustomizer = context
.getBean(ServerHttpMessageConvertersCustomizer.class);
ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults();
serverCustomizer.customize(serverBuilder);
assertThat(serverBuilder.build()).contains(converter);
}
private void assertConvertersBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context,
List<Class<? extends HttpMessageConverter<?>>> types) {
List<? extends HttpMessageConverter<?>> converterInstances = types.stream().map(context::getBean).toList();
HttpMessageConverters converters = context.getBean(HttpMessageConverters.class);
assertThat(converters.getConverters()).containsSubsequence(converterInstances);
ClientHttpMessageConvertersCustomizer clientCustomizer = context
.getBean(ClientHttpMessageConvertersCustomizer.class);
ClientBuilder clientBuilder = HttpMessageConverters.forClient().registerDefaults();
clientCustomizer.customize(clientBuilder);
assertThat(clientBuilder.build()).containsSubsequence(converterInstances);
ServerHttpMessageConvertersCustomizer serverCustomizer = context
.getBean(ServerHttpMessageConvertersCustomizer.class);
ServerBuilder serverBuilder = HttpMessageConverters.forServer().registerDefaults();
serverCustomizer.customize(serverBuilder);
assertThat(serverBuilder.build()).containsSubsequence(converterInstances);
}
@Configuration(proxyBeanMethods = false)

View File

@ -38,7 +38,9 @@ class HttpMessageConvertersAutoConfigurationWithoutJacksonTests {
@Test
void autoConfigurationWorksWithSpringHateoasButWithoutJackson() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(HttpMessageConverters.class));
this.contextRunner
.run((context) -> assertThat(context).hasSingleBean(ClientHttpMessageConvertersCustomizer.class)
.hasSingleBean(ServerHttpMessageConvertersCustomizer.class));
}
}

View File

@ -44,6 +44,7 @@ import static org.mockito.Mockito.mock;
* @author Dave Syer
* @author Phillip Webb
*/
@SuppressWarnings("deprecation")
class HttpMessageConvertersTests {
@Test

View File

@ -189,7 +189,7 @@ public class RestTemplateBuilder {
* @return a new builder instance
* @see #additionalMessageConverters(HttpMessageConverter...)
*/
public RestTemplateBuilder messageConverters(Collection<? extends HttpMessageConverter<?>> messageConverters) {
public RestTemplateBuilder messageConverters(Iterable<? extends HttpMessageConverter<?>> messageConverters) {
Assert.notNull(messageConverters, "'messageConverters' must not be null");
return new RestTemplateBuilder(this.requestFactorySettings, this.detectRequestFactory, this.rootUri,
copiedSetOf(messageConverters), this.interceptors, this.requestFactoryBuilder, this.uriTemplateHandler,
@ -726,8 +726,10 @@ public class RestTemplateBuilder {
return copiedSetOf(Arrays.asList(items));
}
private <T> Set<T> copiedSetOf(Collection<? extends T> collection) {
return Collections.unmodifiableSet(new LinkedHashSet<>(collection));
private <T> Set<T> copiedSetOf(Iterable<? extends T> collection) {
LinkedHashSet<T> set = new LinkedHashSet<>();
collection.forEach(set::add);
return Collections.unmodifiableSet(set);
}
private static <T> List<T> copiedListOf(T[] items) {

View File

@ -19,9 +19,7 @@ package org.springframework.boot.restclient.autoconfigure;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
@ -31,33 +29,26 @@ import org.springframework.web.client.RestClient;
* {@link RestClientCustomizer} to apply {@link HttpMessageConverter
* HttpMessageConverters}.
*
* @author Phillip Webb
* @author Brian Clozel
* @since 4.0.0
*/
public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer {
private final @Nullable Iterable<? extends HttpMessageConverter<?>> messageConverters;
private final List<ClientHttpMessageConvertersCustomizer> customizers;
public HttpMessageConvertersRestClientCustomizer(HttpMessageConverter<?>... messageConverters) {
Assert.notNull(messageConverters, "'messageConverters' must not be null");
this.messageConverters = Arrays.asList(messageConverters);
public HttpMessageConvertersRestClientCustomizer(ClientHttpMessageConvertersCustomizer... customizers) {
this(Arrays.asList(customizers));
}
HttpMessageConvertersRestClientCustomizer(@Nullable HttpMessageConverters messageConverters) {
this.messageConverters = messageConverters;
public HttpMessageConvertersRestClientCustomizer(List<ClientHttpMessageConvertersCustomizer> customizers) {
Assert.notNull(customizers, "customizers must not be null");
this.customizers = customizers;
}
@SuppressWarnings("removal")
@Override
public void customize(RestClient.Builder restClientBuilder) {
restClientBuilder.messageConverters(this::configureMessageConverters);
}
private void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
if (this.messageConverters != null) {
messageConverters.clear();
this.messageConverters.forEach(messageConverters::add);
}
restClientBuilder.configureMessageConverters(
(builder) -> this.customizers.forEach((customizer) -> customizer.customize(builder)));
}
}

View File

@ -29,7 +29,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
@ -38,6 +38,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.client.RestClient;
@ -102,11 +103,11 @@ public final class RestClientAutoConfiguration {
static class HttpMessageConvertersConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(ClientHttpMessageConvertersCustomizer.class)
@Order(Ordered.LOWEST_PRECEDENCE)
HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer(
ObjectProvider<HttpMessageConverters> messageConverters) {
return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique());
ObjectProvider<ClientHttpMessageConvertersCustomizer> customizerProvider) {
return new HttpMessageConvertersRestClientCustomizer(customizerProvider.orderedStream().toList());
}
}

View File

@ -24,13 +24,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateCustomizer;
import org.springframework.boot.restclient.RestTemplateRequestCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.web.client.RestTemplate;
/**
@ -51,13 +52,13 @@ public final class RestTemplateAutoConfiguration {
RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
ObjectProvider<HttpMessageConverters> messageConverters,
ObjectProvider<ClientHttpMessageConvertersCustomizer> convertersCustomizers,
ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers,
ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) {
RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer();
configurer.setRequestFactoryBuilder(clientHttpRequestFactoryBuilder.getIfAvailable());
configurer.setRequestFactorySettings(clientHttpRequestFactorySettings.getIfAvailable());
configurer.setHttpMessageConverters(messageConverters.getIfUnique());
configurer.setHttpMessageConvertersCustomizers(convertersCustomizers.orderedStream().toList());
configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().toList());
configurer.setRestTemplateRequestCustomizers(restTemplateRequestCustomizers.orderedStream().toList());
return configurer;

View File

@ -24,10 +24,12 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateCustomizer;
import org.springframework.boot.restclient.RestTemplateRequestCustomizer;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.util.ObjectUtils;
/**
@ -46,7 +48,7 @@ public final class RestTemplateBuilderConfigurer {
private @Nullable ClientHttpRequestFactorySettings requestFactorySettings;
private @Nullable HttpMessageConverters httpMessageConverters;
private @Nullable List<ClientHttpMessageConvertersCustomizer> httpMessageConvertersCustomizers;
private @Nullable List<RestTemplateCustomizer> restTemplateCustomizers;
@ -60,8 +62,9 @@ public final class RestTemplateBuilderConfigurer {
this.requestFactorySettings = requestFactorySettings;
}
void setHttpMessageConverters(@Nullable HttpMessageConverters httpMessageConverters) {
this.httpMessageConverters = httpMessageConverters;
void setHttpMessageConvertersCustomizers(
@Nullable List<ClientHttpMessageConvertersCustomizer> httpMessageConvertersCustomizers) {
this.httpMessageConvertersCustomizers = httpMessageConvertersCustomizers;
}
void setRestTemplateCustomizers(@Nullable List<RestTemplateCustomizer> restTemplateCustomizers) {
@ -86,8 +89,10 @@ public final class RestTemplateBuilderConfigurer {
if (this.requestFactorySettings != null) {
builder = builder.requestFactorySettings(this.requestFactorySettings);
}
if (this.httpMessageConverters != null) {
builder = builder.messageConverters(this.httpMessageConverters.getConverters());
if (this.httpMessageConvertersCustomizers != null) {
ClientBuilder clientBuilder = HttpMessageConverters.forClient();
this.httpMessageConvertersCustomizers.forEach((customizer) -> customizer.customize(clientBuilder));
builder = builder.messageConverters(clientBuilder.build());
}
builder = addCustomizers(builder, this.restTemplateCustomizers, RestTemplateBuilder::customizers);
builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers);

View File

@ -16,62 +16,35 @@
package org.springframework.boot.restclient.autoconfigure;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpMessageConvertersRestClientCustomizer}
*
* @author Phillip Webb
* @author Brian Clozel
*/
class HttpMessageConvertersRestClientCustomizerTests {
@Test
void createWhenNullMessageConvertersArrayThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new HttpMessageConvertersRestClientCustomizer((HttpMessageConverter<?>[]) null))
.withMessage("'messageConverters' must not be null");
}
@Test
void createWhenNullMessageConvertersDoesNotCustomize() {
HttpMessageConverter<?> c0 = mock();
assertThat(apply(new HttpMessageConvertersRestClientCustomizer((HttpMessageConverters) null), c0))
.containsExactly(c0);
}
@Test
void customizeConfiguresMessageConverters() {
HttpMessageConverter<?> c0 = mock();
HttpMessageConverter<?> c1 = mock();
HttpMessageConverter<?> c2 = mock();
assertThat(apply(new HttpMessageConvertersRestClientCustomizer(c1, c2), c0)).containsExactly(c1, c2);
}
ClientHttpMessageConvertersCustomizer customizer = (clientBuilder) -> clientBuilder.customMessageConverter(c0)
.customMessageConverter(c1);
@SuppressWarnings({ "unchecked", "removal" })
private List<HttpMessageConverter<?>> apply(HttpMessageConvertersRestClientCustomizer customizer,
HttpMessageConverter<?>... converters) {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(Arrays.asList(converters));
RestClient.Builder restClientBuilder = mock();
ArgumentCaptor<Consumer<List<HttpMessageConverter<?>>>> captor = ArgumentCaptor.forClass(Consumer.class);
given(restClientBuilder.messageConverters(captor.capture())).willReturn(restClientBuilder);
customizer.customize(restClientBuilder);
captor.getValue().accept(messageConverters);
return messageConverters;
RestClient.Builder builder = RestClient.builder();
new HttpMessageConvertersRestClientCustomizer(customizer).customize(builder);
assertThat(builder.build()).extracting("messageConverters")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.containsSubsequence(c0, c1);
}
}

View File

@ -31,7 +31,6 @@ import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.boot.ssl.SslBundle;
@ -42,8 +41,8 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
@ -71,7 +70,6 @@ class RestClientAutoConfigurationTests {
@Test
void shouldSupplyBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
assertThat(context).hasSingleBean(RestClient.Builder.class);
});
@ -162,21 +160,6 @@ class RestClientAutoConfigurationTests {
});
}
@Test
@SuppressWarnings("unchecked")
void restClientWhenMessageConvertersDefinedShouldHaveMessageConverters() {
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.withUserConfiguration(RestClientConfig.class)
.run((context) -> {
RestClient restClient = context.getBean(RestClient.class);
List<HttpMessageConverter<?>> expectedConverters = context.getBean(HttpMessageConverters.class)
.getConverters();
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
.getField(restClient, "messageConverters");
assertThat(actualConverters).containsExactlyElementsOf(expectedConverters);
});
}
@Test
@SuppressWarnings("unchecked")
void restClientWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
@ -257,15 +240,13 @@ class RestClientAutoConfigurationTests {
.run((context) -> {
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class)
.hasSingleBean(ClientHttpRequestFactorySettings.class)
.hasSingleBean(ClientHttpRequestFactoryBuilder.class)
.hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
.hasSingleBean(ClientHttpRequestFactoryBuilder.class);
RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class);
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactoryBuilder",
context.getBean(ClientHttpRequestFactoryBuilder.class));
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactorySettings",
context.getBean(ClientHttpRequestFactorySettings.class));
assertThat(configurer).hasFieldOrPropertyWithValue("customizers", List.of(customizer1, customizer2,
context.getBean(HttpMessageConvertersRestClientCustomizer.class)));
assertThat(configurer).hasFieldOrPropertyWithValue("customizers", List.of(customizer1, customizer2));
});
}
@ -284,7 +265,6 @@ class RestClientAutoConfigurationTests {
void whenServletWebApplicationRestClientIsConfigured() {
new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
assertThat(context).hasSingleBean(RestClient.Builder.class);
});
@ -297,7 +277,6 @@ class RestClientAutoConfigurationTests {
.withConfiguration(
AutoConfigurations.of(RestClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
assertThat(context).hasSingleBean(RestClient.Builder.class);
});
@ -396,7 +375,7 @@ class RestClientAutoConfigurationTests {
}
static class CustomHttpMessageConverter extends StringHttpMessageConverter {
static class CustomHttpMessageConverter extends ByteArrayHttpMessageConverter {
}

View File

@ -17,14 +17,12 @@
package org.springframework.boot.restclient.autoconfigure;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
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.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateCustomizer;
@ -36,10 +34,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.web.client.RestTemplate;
@ -84,19 +81,6 @@ class RestTemplateAutoConfigurationTests {
.isTrue());
}
@Test
void restTemplateWhenMessageConvertersDefinedShouldHaveMessageConverters() {
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.withUserConfiguration(RestTemplateConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(RestTemplate.class);
RestTemplate restTemplate = context.getBean(RestTemplate.class);
List<HttpMessageConverter<?>> converters = context.getBean(HttpMessageConverters.class).getConverters();
assertThat(restTemplate.getMessageConverters()).containsExactlyElementsOf(converters);
assertThat(restTemplate.getRequestFactory()).isInstanceOf(HttpComponentsClientHttpRequestFactory.class);
});
}
@Test
void restTemplateWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
this.contextRunner.withUserConfiguration(RestTemplateConfig.class).run((context) -> {
@ -297,7 +281,7 @@ class RestTemplateAutoConfigurationTests {
}
static class CustomHttpMessageConverter extends StringHttpMessageConverter {
static class CustomHttpMessageConverter extends ByteArrayHttpMessageConverter {
}

View File

@ -16,12 +16,16 @@
package org.springframework.boot.webmvc.test.autoconfigure;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ServerHttpMessageConvertersCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
@ -36,11 +40,14 @@ class MockMvcTesterConfiguration {
@Bean
@ConditionalOnMissingBean
MockMvcTester mockMvcTester(MockMvc mockMvc, ObjectProvider<HttpMessageConverters> httpMessageConverters) {
MockMvcTester mockMvcTester(MockMvc mockMvc,
ObjectProvider<ServerHttpMessageConvertersCustomizer> customizersProvider) {
MockMvcTester mockMvcTester = MockMvcTester.create(mockMvc);
HttpMessageConverters converters = httpMessageConverters.getIfAvailable();
if (converters != null) {
mockMvcTester = mockMvcTester.withHttpMessageConverters(converters);
List<ServerHttpMessageConvertersCustomizer> customizers = customizersProvider.orderedStream().toList();
if (!customizers.isEmpty()) {
ServerBuilder serverBuilder = HttpMessageConverters.forServer();
customizersProvider.orderedStream().forEach((customizer) -> customizer.customize(serverBuilder));
mockMvcTester = mockMvcTester.withHttpMessageConverters(serverBuilder.build());
}
return mockMvcTester;
}

View File

@ -54,7 +54,7 @@ import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.ServerHttpMessageConvertersCustomizer;
import org.springframework.boot.servlet.filter.OrderedFormContentFilter;
import org.springframework.boot.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.servlet.filter.OrderedRequestContextFilter;
@ -78,7 +78,7 @@ import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.lang.Contract;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
@ -200,12 +200,12 @@ public final class WebMvcAutoConfiguration {
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
private final ObjectProvider<DispatcherServletPath> dispatcherServletPath;
private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations;
private final ObjectProvider<ServerHttpMessageConvertersCustomizer> httpMessageConvertersCustomizerProvider;
private final @Nullable ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private @Nullable ServletContext servletContext;
@ -217,7 +217,8 @@ public final class WebMvcAutoConfiguration {
private final ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler;
WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ListableBeanFactory beanFactory,
ObjectProvider<ServerHttpMessageConvertersCustomizer> httpMessageConvertersCustomizerProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations,
@ -227,7 +228,7 @@ public final class WebMvcAutoConfiguration {
this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.httpMessageConvertersCustomizerProvider = httpMessageConvertersCustomizerProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
@ -242,10 +243,8 @@ public final class WebMvcAutoConfiguration {
}
@Override
@SuppressWarnings("removal")
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.messageConvertersProvider
.ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
public void configureMessageConverters(ServerBuilder builder) {
this.httpMessageConvertersCustomizerProvider.forEach((customizer) -> customizer.customize(builder));
}
@Override

View File

@ -58,7 +58,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.DispatcherServlet;
@ -174,13 +174,12 @@ public class WebMvcEndpointManagementContextConfiguration {
}
@Override
@SuppressWarnings("removal")
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
public void configureMessageConverters(ServerBuilder builder) {
builder.configureMessageConverters((converter) -> {
if (converter instanceof JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter) {
configure(jacksonJsonHttpMessageConverter);
}
}
});
}
private void configure(JacksonJsonHttpMessageConverter converter) {

View File

@ -46,7 +46,6 @@ 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.HttpMessageConverters;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.servlet.filter.OrderedFormContentFilter;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
@ -185,8 +184,7 @@ class WebMvcAutoConfigurationTests {
void handlerAdaptersCreated() {
this.contextRunner.run((context) -> {
assertThat(context).getBeans(HandlerAdapter.class).hasSize(4);
assertThat(context.getBean(RequestMappingHandlerAdapter.class).getMessageConverters()).isNotEmpty()
.isEqualTo(context.getBean(HttpMessageConverters.class).getConverters());
assertThat(context.getBean(RequestMappingHandlerAdapter.class).getMessageConverters()).isNotEmpty();
});
}