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. 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. 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[] 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]] [[web.servlet.spring-mvc.message-codes]]

View File

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

View File

@ -16,19 +16,21 @@
package org.springframework.boot.docs.web.servlet.springmvc.messageconverters 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.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter import org.springframework.http.converter.HttpMessageConverters
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
class MyHttpMessageConvertersConfiguration { class MyHttpMessageConvertersConfiguration {
@Bean @Bean
fun customConverters(): HttpMessageConverters { fun myClientConvertersCustomizer(): ClientHttpMessageConvertersCustomizer {
val additional: HttpMessageConverter<*> = AdditionalHttpMessageConverter() return ClientHttpMessageConvertersCustomizer { clientBuilder: HttpMessageConverters.ClientBuilder ->
val another: HttpMessageConverter<*> = AnotherHttpMessageConverter() clientBuilder
return HttpMessageConverters(additional, another) .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.GraphQlAutoConfiguration;
import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties; import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties;
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.annotation.ImportRuntimeHints;
@ -177,21 +176,20 @@ public final class GraphQlWebMvcAutoConfiguration {
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HttpMessageConverters.class, ServerContainer.class, WebSocketHandler.class }) @ConditionalOnClass({ HttpMessageConverter.class, ServerContainer.class, WebSocketHandler.class })
@ConditionalOnProperty("spring.graphql.websocket.path") @ConditionalOnProperty("spring.graphql.websocket.path")
static class WebSocketConfiguration { static class WebSocketConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler, GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, HttpMessageConverters converters) { GraphQlProperties properties, ObjectProvider<HttpMessageConverter<?>> converters) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters), return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive()); properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive());
} }
private HttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) { private HttpMessageConverter<Object> getJsonConverter(ObjectProvider<HttpMessageConverter<?>> converters) {
return converters.getConverters() return converters.orderedStream()
.stream()
.filter(this::canReadJsonMap) .filter(this::canReadJsonMap)
.findFirst() .findFirst()
.map(this::asObjectHttpMessageConverter) .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(HttpMessageConverter...)
* @see #HttpMessageConverters(Collection) * @see #HttpMessageConverters(Collection)
* @see #getConverters() * @see #getConverters()
* @deprecated since 4.0 in favor of {@link ClientHttpMessageConvertersCustomizer} and
* {@link ServerHttpMessageConvertersCustomizer}.
*/ */
@Deprecated(since = "4.0")
public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> { public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> {
private static final List<Class<?>> NON_REPLACING_CONVERTERS; 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.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; 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.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
@ -46,6 +48,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Dmitry Sulman * @author Dmitry Sulman
* @author Brian Clozel
* @since 4.0.0 * @since 4.0.0
*/ */
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration", @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"; static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
@Bean @Bean
@ConditionalOnMissingBean @Order(Ordered.LOWEST_PRECEDENCE)
HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) { @SuppressWarnings("deprecation")
return new HttpMessageConverters(converters.orderedStream().toList()); 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) @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.RepresentationModel;
import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter; import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; 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.StringHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
@ -262,14 +265,16 @@ class HttpMessageConvertersAutoConfigurationTests {
void whenServletWebApplicationHttpMessageConvertersIsConfigured() { void whenServletWebApplicationHttpMessageConvertersIsConfigured() {
new WebApplicationContextRunner() new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(HttpMessageConverters.class)); .run((context) -> assertThat(context).hasSingleBean(ServerHttpMessageConvertersCustomizer.class)
.hasSingleBean(ClientHttpMessageConvertersCustomizer.class));
} }
@Test @Test
void whenReactiveWebApplicationHttpMessageConvertersIsNotConfigured() { void whenReactiveWebApplicationHttpMessageConvertersIsNotConfigured() {
new ReactiveWebApplicationContextRunner() new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(HttpMessageConverters.class)); .run((context) -> assertThat(context).doesNotHaveBean(ServerHttpMessageConvertersCustomizer.class)
.doesNotHaveBean(ClientHttpMessageConvertersCustomizer.class));
} }
@Test @Test
@ -318,15 +323,33 @@ class HttpMessageConvertersAutoConfigurationTests {
private void assertConverterBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context, private void assertConverterBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context,
Class<? extends HttpMessageConverter<?>> type) { Class<? extends HttpMessageConverter<?>> type) {
HttpMessageConverter<?> converter = context.getBean(type); HttpMessageConverter<?> converter = context.getBean(type);
HttpMessageConverters converters = context.getBean(HttpMessageConverters.class); ClientHttpMessageConvertersCustomizer clientCustomizer = context
assertThat(converters.getConverters()).contains(converter); .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, private void assertConvertersBeanRegisteredWithHttpMessageConverters(AssertableApplicationContext context,
List<Class<? extends HttpMessageConverter<?>>> types) { List<Class<? extends HttpMessageConverter<?>>> types) {
List<? extends HttpMessageConverter<?>> converterInstances = types.stream().map(context::getBean).toList(); List<? extends HttpMessageConverter<?>> converterInstances = types.stream().map(context::getBean).toList();
HttpMessageConverters converters = context.getBean(HttpMessageConverters.class); ClientHttpMessageConvertersCustomizer clientCustomizer = context
assertThat(converters.getConverters()).containsSubsequence(converterInstances); .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) @Configuration(proxyBeanMethods = false)

View File

@ -38,7 +38,9 @@ class HttpMessageConvertersAutoConfigurationWithoutJacksonTests {
@Test @Test
void autoConfigurationWorksWithSpringHateoasButWithoutJackson() { 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 Dave Syer
* @author Phillip Webb * @author Phillip Webb
*/ */
@SuppressWarnings("deprecation")
class HttpMessageConvertersTests { class HttpMessageConvertersTests {
@Test @Test

View File

@ -189,7 +189,7 @@ public class RestTemplateBuilder {
* @return a new builder instance * @return a new builder instance
* @see #additionalMessageConverters(HttpMessageConverter...) * @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"); Assert.notNull(messageConverters, "'messageConverters' must not be null");
return new RestTemplateBuilder(this.requestFactorySettings, this.detectRequestFactory, this.rootUri, return new RestTemplateBuilder(this.requestFactorySettings, this.detectRequestFactory, this.rootUri,
copiedSetOf(messageConverters), this.interceptors, this.requestFactoryBuilder, this.uriTemplateHandler, copiedSetOf(messageConverters), this.interceptors, this.requestFactoryBuilder, this.uriTemplateHandler,
@ -726,8 +726,10 @@ public class RestTemplateBuilder {
return copiedSetOf(Arrays.asList(items)); return copiedSetOf(Arrays.asList(items));
} }
private <T> Set<T> copiedSetOf(Collection<? extends T> collection) { private <T> Set<T> copiedSetOf(Iterable<? extends T> collection) {
return Collections.unmodifiableSet(new LinkedHashSet<>(collection)); LinkedHashSet<T> set = new LinkedHashSet<>();
collection.forEach(set::add);
return Collections.unmodifiableSet(set);
} }
private static <T> List<T> copiedListOf(T[] items) { 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.Arrays;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable; import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -31,33 +29,26 @@ import org.springframework.web.client.RestClient;
* {@link RestClientCustomizer} to apply {@link HttpMessageConverter * {@link RestClientCustomizer} to apply {@link HttpMessageConverter
* HttpMessageConverters}. * HttpMessageConverters}.
* *
* @author Phillip Webb * @author Brian Clozel
* @since 4.0.0 * @since 4.0.0
*/ */
public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer { public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer {
private final @Nullable Iterable<? extends HttpMessageConverter<?>> messageConverters; private final List<ClientHttpMessageConvertersCustomizer> customizers;
public HttpMessageConvertersRestClientCustomizer(HttpMessageConverter<?>... messageConverters) { public HttpMessageConvertersRestClientCustomizer(ClientHttpMessageConvertersCustomizer... customizers) {
Assert.notNull(messageConverters, "'messageConverters' must not be null"); this(Arrays.asList(customizers));
this.messageConverters = Arrays.asList(messageConverters);
} }
HttpMessageConvertersRestClientCustomizer(@Nullable HttpMessageConverters messageConverters) { public HttpMessageConvertersRestClientCustomizer(List<ClientHttpMessageConvertersCustomizer> customizers) {
this.messageConverters = messageConverters; Assert.notNull(customizers, "customizers must not be null");
this.customizers = customizers;
} }
@SuppressWarnings("removal")
@Override @Override
public void customize(RestClient.Builder restClientBuilder) { public void customize(RestClient.Builder restClientBuilder) {
restClientBuilder.messageConverters(this::configureMessageConverters); restClientBuilder.configureMessageConverters(
} (builder) -> this.customizers.forEach((customizer) -> customizer.customize(builder)));
private void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
if (this.messageConverters != null) {
messageConverters.clear();
this.messageConverters.forEach(messageConverters::add);
}
} }
} }

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.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; 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.restclient.RestClientCustomizer;
import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean; 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.context.annotation.Scope;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.web.client.ApiVersionFormatter; import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
@ -102,11 +103,11 @@ public final class RestClientAutoConfiguration {
static class HttpMessageConvertersConfiguration { static class HttpMessageConvertersConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnBean(ClientHttpMessageConvertersCustomizer.class)
@Order(Ordered.LOWEST_PRECEDENCE) @Order(Ordered.LOWEST_PRECEDENCE)
HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer( HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer(
ObjectProvider<HttpMessageConverters> messageConverters) { ObjectProvider<ClientHttpMessageConvertersCustomizer> customizerProvider) {
return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique()); 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.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; 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.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateCustomizer; import org.springframework.boot.restclient.RestTemplateCustomizer;
import org.springframework.boot.restclient.RestTemplateRequestCustomizer; import org.springframework.boot.restclient.RestTemplateRequestCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
/** /**
@ -51,13 +52,13 @@ public final class RestTemplateAutoConfiguration {
RestTemplateBuilderConfigurer restTemplateBuilderConfigurer( RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder, ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings, ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
ObjectProvider<HttpMessageConverters> messageConverters, ObjectProvider<ClientHttpMessageConvertersCustomizer> convertersCustomizers,
ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers, ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers,
ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) { ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) {
RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer(); RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer();
configurer.setRequestFactoryBuilder(clientHttpRequestFactoryBuilder.getIfAvailable()); configurer.setRequestFactoryBuilder(clientHttpRequestFactoryBuilder.getIfAvailable());
configurer.setRequestFactorySettings(clientHttpRequestFactorySettings.getIfAvailable()); configurer.setRequestFactorySettings(clientHttpRequestFactorySettings.getIfAvailable());
configurer.setHttpMessageConverters(messageConverters.getIfUnique()); configurer.setHttpMessageConvertersCustomizers(convertersCustomizers.orderedStream().toList());
configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().toList()); configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().toList());
configurer.setRestTemplateRequestCustomizers(restTemplateRequestCustomizers.orderedStream().toList()); configurer.setRestTemplateRequestCustomizers(restTemplateRequestCustomizers.orderedStream().toList());
return configurer; 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.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; 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.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateCustomizer; import org.springframework.boot.restclient.RestTemplateCustomizer;
import org.springframework.boot.restclient.RestTemplateRequestCustomizer; import org.springframework.boot.restclient.RestTemplateRequestCustomizer;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
@ -46,7 +48,7 @@ public final class RestTemplateBuilderConfigurer {
private @Nullable ClientHttpRequestFactorySettings requestFactorySettings; private @Nullable ClientHttpRequestFactorySettings requestFactorySettings;
private @Nullable HttpMessageConverters httpMessageConverters; private @Nullable List<ClientHttpMessageConvertersCustomizer> httpMessageConvertersCustomizers;
private @Nullable List<RestTemplateCustomizer> restTemplateCustomizers; private @Nullable List<RestTemplateCustomizer> restTemplateCustomizers;
@ -60,8 +62,9 @@ public final class RestTemplateBuilderConfigurer {
this.requestFactorySettings = requestFactorySettings; this.requestFactorySettings = requestFactorySettings;
} }
void setHttpMessageConverters(@Nullable HttpMessageConverters httpMessageConverters) { void setHttpMessageConvertersCustomizers(
this.httpMessageConverters = httpMessageConverters; @Nullable List<ClientHttpMessageConvertersCustomizer> httpMessageConvertersCustomizers) {
this.httpMessageConvertersCustomizers = httpMessageConvertersCustomizers;
} }
void setRestTemplateCustomizers(@Nullable List<RestTemplateCustomizer> restTemplateCustomizers) { void setRestTemplateCustomizers(@Nullable List<RestTemplateCustomizer> restTemplateCustomizers) {
@ -86,8 +89,10 @@ public final class RestTemplateBuilderConfigurer {
if (this.requestFactorySettings != null) { if (this.requestFactorySettings != null) {
builder = builder.requestFactorySettings(this.requestFactorySettings); builder = builder.requestFactorySettings(this.requestFactorySettings);
} }
if (this.httpMessageConverters != null) { if (this.httpMessageConvertersCustomizers != null) {
builder = builder.messageConverters(this.httpMessageConverters.getConverters()); 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.restTemplateCustomizers, RestTemplateBuilder::customizers);
builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers);

View File

@ -16,62 +16,35 @@
package org.springframework.boot.restclient.autoconfigure; package org.springframework.boot.restclient.autoconfigure;
import java.util.ArrayList; import org.assertj.core.api.InstanceOfAssertFactories;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test; 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.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat; 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; import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link HttpMessageConvertersRestClientCustomizer} * Tests for {@link HttpMessageConvertersRestClientCustomizer}
* *
* @author Phillip Webb * @author Brian Clozel
*/ */
class HttpMessageConvertersRestClientCustomizerTests { 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 @Test
void customizeConfiguresMessageConverters() { void customizeConfiguresMessageConverters() {
HttpMessageConverter<?> c0 = mock(); HttpMessageConverter<?> c0 = mock();
HttpMessageConverter<?> c1 = mock(); HttpMessageConverter<?> c1 = mock();
HttpMessageConverter<?> c2 = mock(); ClientHttpMessageConvertersCustomizer customizer = (clientBuilder) -> clientBuilder.customMessageConverter(c0)
assertThat(apply(new HttpMessageConvertersRestClientCustomizer(c1, c2), c0)).containsExactly(c1, c2); .customMessageConverter(c1);
}
@SuppressWarnings({ "unchecked", "removal" }) RestClient.Builder builder = RestClient.builder();
private List<HttpMessageConverter<?>> apply(HttpMessageConvertersRestClientCustomizer customizer, new HttpMessageConvertersRestClientCustomizer(customizer).customize(builder);
HttpMessageConverter<?>... converters) { assertThat(builder.build()).extracting("messageConverters")
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(Arrays.asList(converters)); .asInstanceOf(InstanceOfAssertFactories.LIST)
RestClient.Builder restClientBuilder = mock(); .containsSubsequence(c0, c1);
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;
} }
} }

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.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; 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.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.boot.ssl.SslBundle; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.ApiVersionFormatter; import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.client.ApiVersionInserter;
@ -71,7 +70,6 @@ class RestClientAutoConfigurationTests {
@Test @Test
void shouldSupplyBeans() { void shouldSupplyBeans() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class); assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
assertThat(context).hasSingleBean(RestClient.Builder.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 @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void restClientWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() { void restClientWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
@ -257,15 +240,13 @@ class RestClientAutoConfigurationTests {
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class) assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class)
.hasSingleBean(ClientHttpRequestFactorySettings.class) .hasSingleBean(ClientHttpRequestFactorySettings.class)
.hasSingleBean(ClientHttpRequestFactoryBuilder.class) .hasSingleBean(ClientHttpRequestFactoryBuilder.class);
.hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class); RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class);
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactoryBuilder", assertThat(configurer).hasFieldOrPropertyWithValue("requestFactoryBuilder",
context.getBean(ClientHttpRequestFactoryBuilder.class)); context.getBean(ClientHttpRequestFactoryBuilder.class));
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactorySettings", assertThat(configurer).hasFieldOrPropertyWithValue("requestFactorySettings",
context.getBean(ClientHttpRequestFactorySettings.class)); context.getBean(ClientHttpRequestFactorySettings.class));
assertThat(configurer).hasFieldOrPropertyWithValue("customizers", List.of(customizer1, customizer2, assertThat(configurer).hasFieldOrPropertyWithValue("customizers", List.of(customizer1, customizer2));
context.getBean(HttpMessageConvertersRestClientCustomizer.class)));
}); });
} }
@ -284,7 +265,6 @@ class RestClientAutoConfigurationTests {
void whenServletWebApplicationRestClientIsConfigured() { void whenServletWebApplicationRestClientIsConfigured() {
new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class)) new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class))
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class); assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
assertThat(context).hasSingleBean(RestClient.Builder.class); assertThat(context).hasSingleBean(RestClient.Builder.class);
}); });
@ -297,7 +277,6 @@ class RestClientAutoConfigurationTests {
.withConfiguration( .withConfiguration(
AutoConfigurations.of(RestClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class)) AutoConfigurations.of(RestClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class))
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class); assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
assertThat(context).hasSingleBean(RestClient.Builder.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; package org.springframework.boot.restclient.autoconfigure;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; 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.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateCustomizer; 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.context.annotation.Configuration;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; 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.MockClientHttpRequest;
import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@ -84,19 +81,6 @@ class RestTemplateAutoConfigurationTests {
.isTrue()); .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 @Test
void restTemplateWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() { void restTemplateWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
this.contextRunner.withUserConfiguration(RestTemplateConfig.class).run((context) -> { 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; package org.springframework.boot.webmvc.test.autoconfigure;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.Bean;
import org.springframework.context.annotation.Configuration; 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.MockMvc;
import org.springframework.test.web.servlet.assertj.MockMvcTester; import org.springframework.test.web.servlet.assertj.MockMvcTester;
@ -36,11 +40,14 @@ class MockMvcTesterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
MockMvcTester mockMvcTester(MockMvc mockMvc, ObjectProvider<HttpMessageConverters> httpMessageConverters) { MockMvcTester mockMvcTester(MockMvc mockMvc,
ObjectProvider<ServerHttpMessageConvertersCustomizer> customizersProvider) {
MockMvcTester mockMvcTester = MockMvcTester.create(mockMvc); MockMvcTester mockMvcTester = MockMvcTester.create(mockMvc);
HttpMessageConverters converters = httpMessageConverters.getIfAvailable(); List<ServerHttpMessageConvertersCustomizer> customizers = customizersProvider.orderedStream().toList();
if (converters != null) { if (!customizers.isEmpty()) {
mockMvcTester = mockMvcTester.withHttpMessageConverters(converters); ServerBuilder serverBuilder = HttpMessageConverters.forServer();
customizersProvider.orderedStream().forEach((customizer) -> customizer.customize(serverBuilder));
mockMvcTester = mockMvcTester.withHttpMessageConverters(serverBuilder.build());
} }
return mockMvcTester; 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.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.ApplicationConversionService; 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.OrderedFormContentFilter;
import org.springframework.boot.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.servlet.filter.OrderedRequestContextFilter; 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.format.support.FormattingConversionService;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.MediaType; 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.lang.Contract;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -200,12 +200,12 @@ public final class WebMvcAutoConfiguration {
private final ListableBeanFactory beanFactory; private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
private final ObjectProvider<DispatcherServletPath> dispatcherServletPath; private final ObjectProvider<DispatcherServletPath> dispatcherServletPath;
private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations; private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations;
private final ObjectProvider<ServerHttpMessageConvertersCustomizer> httpMessageConvertersCustomizerProvider;
private final @Nullable ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; private final @Nullable ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private @Nullable ServletContext servletContext; private @Nullable ServletContext servletContext;
@ -217,7 +217,8 @@ public final class WebMvcAutoConfiguration {
private final ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler; private final ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler;
WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ListableBeanFactory beanFactory,
ObjectProvider<ServerHttpMessageConvertersCustomizer> httpMessageConvertersCustomizerProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations,
@ -227,7 +228,7 @@ public final class WebMvcAutoConfiguration {
this.resourceProperties = webProperties.getResources(); this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties; this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory; this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider; this.httpMessageConvertersCustomizerProvider = httpMessageConvertersCustomizerProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath; this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations; this.servletRegistrations = servletRegistrations;
@ -242,10 +243,8 @@ public final class WebMvcAutoConfiguration {
} }
@Override @Override
@SuppressWarnings("removal") public void configureMessageConverters(ServerBuilder builder) {
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { this.httpMessageConvertersCustomizerProvider.forEach((customizer) -> customizer.customize(builder));
this.messageConvertersProvider
.ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
} }
@Override @Override

View File

@ -58,7 +58,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role; import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.MediaType; 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.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
@ -174,13 +174,12 @@ public class WebMvcEndpointManagementContextConfiguration {
} }
@Override @Override
@SuppressWarnings("removal") public void configureMessageConverters(ServerBuilder builder) {
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { builder.configureMessageConverters((converter) -> {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter) { if (converter instanceof JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter) {
configure(jacksonJsonHttpMessageConverter); configure(jacksonJsonHttpMessageConverter);
} }
} });
} }
private void configure(JacksonJsonHttpMessageConverter converter) { 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.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; 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.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.servlet.filter.OrderedFormContentFilter; import org.springframework.boot.servlet.filter.OrderedFormContentFilter;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
@ -185,8 +184,7 @@ class WebMvcAutoConfigurationTests {
void handlerAdaptersCreated() { void handlerAdaptersCreated() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).getBeans(HandlerAdapter.class).hasSize(4); assertThat(context).getBeans(HandlerAdapter.class).hasSize(4);
assertThat(context.getBean(RequestMappingHandlerAdapter.class).getMessageConverters()).isNotEmpty() assertThat(context.getBean(RequestMappingHandlerAdapter.class).getMessageConverters()).isNotEmpty();
.isEqualTo(context.getBean(HttpMessageConverters.class).getConverters());
}); });
} }