diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index 88f72ef40f6..4a5d1020e52 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -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]] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java index 531f3313a2a..9815b38707d 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java @@ -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()); } } diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.kt index c89e441fb7f..65f8a851979 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.kt @@ -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()) + } } } diff --git a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java index 43fb107e08a..be3bc347045 100644 --- a/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java +++ b/module/spring-boot-graphql/src/main/java/org/springframework/boot/graphql/autoconfigure/servlet/GraphQlWebMvcAutoConfiguration.java @@ -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> converters) { return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters), properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive()); } - private HttpMessageConverter getJsonConverter(HttpMessageConverters converters) { - return converters.getConverters() - .stream() + private HttpMessageConverter getJsonConverter(ObjectProvider> converters) { + return converters.orderedStream() .filter(this::canReadJsonMap) .findFirst() .map(this::asObjectHttpMessageConverter) diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/ClientHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/ClientHttpMessageConvertersCustomizer.java new file mode 100644 index 00000000000..b46b121cbb3 --- /dev/null +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/ClientHttpMessageConvertersCustomizer.java @@ -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); + +} diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java new file mode 100644 index 00000000000..cc81d8fd201 --- /dev/null +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultClientHttpMessageConvertersCustomizer.java @@ -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> converters; + + DefaultClientHttpMessageConvertersCustomizer(@Nullable HttpMessageConverters legacyConverters, + Collection> 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); + } + }); + } + } + +} diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java new file mode 100644 index 00000000000..102ec3434e7 --- /dev/null +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/DefaultServerHttpMessageConvertersCustomizer.java @@ -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> converters; + + DefaultServerHttpMessageConvertersCustomizer(@Nullable HttpMessageConverters legacyConverters, + Collection> 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); + } + }); + } + } + +} diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java index 81150e3fa43..503e7c1515e 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConverters.java @@ -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> { private static final List> NON_REPLACING_CONVERTERS; diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java index 2b519977bec..324320cd9a7 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java @@ -30,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> converters) { - return new HttpMessageConverters(converters.orderedStream().toList()); + @Order(Ordered.LOWEST_PRECEDENCE) + @SuppressWarnings("deprecation") + ClientHttpMessageConvertersCustomizer clientConvertersCustomizer( + ObjectProvider legacyConverters, + ObjectProvider> converters) { + return new DefaultClientHttpMessageConvertersCustomizer(legacyConverters.getIfAvailable(), + converters.orderedStream().toList()); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + @SuppressWarnings("deprecation") + ServerHttpMessageConvertersCustomizer serverConvertersCustomizer( + ObjectProvider legacyConverters, + ObjectProvider> converters) { + return new DefaultServerHttpMessageConvertersCustomizer(legacyConverters.getIfAvailable(), + converters.orderedStream().toList()); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/ServerHttpMessageConvertersCustomizer.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/ServerHttpMessageConvertersCustomizer.java new file mode 100644 index 00000000000..1bce72b52e5 --- /dev/null +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/ServerHttpMessageConvertersCustomizer.java @@ -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); + +} diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java index 3e434379d27..2635e75a730 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java @@ -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> 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>> types) { List> 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) diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java index 2c4a8ffdb4d..10a19a37775 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationWithoutJacksonTests.java @@ -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)); } } diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java index 820115b316d..7f7c36853b6 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersTests.java @@ -44,6 +44,7 @@ import static org.mockito.Mockito.mock; * @author Dave Syer * @author Phillip Webb */ +@SuppressWarnings("deprecation") class HttpMessageConvertersTests { @Test diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java index 7199d1058b3..043f557211e 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RestTemplateBuilder.java @@ -189,7 +189,7 @@ public class RestTemplateBuilder { * @return a new builder instance * @see #additionalMessageConverters(HttpMessageConverter...) */ - public RestTemplateBuilder messageConverters(Collection> messageConverters) { + public RestTemplateBuilder messageConverters(Iterable> 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 Set copiedSetOf(Collection collection) { - return Collections.unmodifiableSet(new LinkedHashSet<>(collection)); + private Set copiedSetOf(Iterable collection) { + LinkedHashSet set = new LinkedHashSet<>(); + collection.forEach(set::add); + return Collections.unmodifiableSet(set); } private static List copiedListOf(T[] items) { diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizer.java index ab09cb9f3b6..5a8ffd9df24 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizer.java @@ -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> messageConverters; + private final List 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 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> messageConverters) { - if (this.messageConverters != null) { - messageConverters.clear(); - this.messageConverters.forEach(messageConverters::add); - } + restClientBuilder.configureMessageConverters( + (builder) -> this.customizers.forEach((customizer) -> customizer.customize(builder))); } } diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfiguration.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfiguration.java index 75db3dcea28..24e1917659c 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfiguration.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfiguration.java @@ -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 messageConverters) { - return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique()); + ObjectProvider customizerProvider) { + return new HttpMessageConvertersRestClientCustomizer(customizerProvider.orderedStream().toList()); } } diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java index 82e468aad45..6dd9df347fc 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfiguration.java @@ -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, ObjectProvider clientHttpRequestFactorySettings, - ObjectProvider messageConverters, + ObjectProvider convertersCustomizers, ObjectProvider restTemplateCustomizers, ObjectProvider> 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; diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java index fd77def8cdf..62eeab17687 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestTemplateBuilderConfigurer.java @@ -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 httpMessageConvertersCustomizers; private @Nullable List restTemplateCustomizers; @@ -60,8 +62,9 @@ public final class RestTemplateBuilderConfigurer { this.requestFactorySettings = requestFactorySettings; } - void setHttpMessageConverters(@Nullable HttpMessageConverters httpMessageConverters) { - this.httpMessageConverters = httpMessageConverters; + void setHttpMessageConvertersCustomizers( + @Nullable List httpMessageConvertersCustomizers) { + this.httpMessageConvertersCustomizers = httpMessageConvertersCustomizers; } void setRestTemplateCustomizers(@Nullable List 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); diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizerTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizerTests.java index aeeafb58610..3d419a45769 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizerTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/HttpMessageConvertersRestClientCustomizerTests.java @@ -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> apply(HttpMessageConvertersRestClientCustomizer customizer, - HttpMessageConverter... converters) { - List> messageConverters = new ArrayList<>(Arrays.asList(converters)); - RestClient.Builder restClientBuilder = mock(); - ArgumentCaptor>>> 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); } } diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java index 9181dd53de9..b8ce53289bb 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfigurationTests.java @@ -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> expectedConverters = context.getBean(HttpMessageConverters.class) - .getConverters(); - List> actualConverters = (List>) 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 { } diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java index 78b229d9a8e..7ab62ce36b8 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/RestTemplateAutoConfigurationTests.java @@ -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> 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 { } diff --git a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcTesterConfiguration.java b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcTesterConfiguration.java index 64ee2a9cf4b..a369f00cd1e 100644 --- a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcTesterConfiguration.java +++ b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcTesterConfiguration.java @@ -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) { + MockMvcTester mockMvcTester(MockMvc mockMvc, + ObjectProvider customizersProvider) { MockMvcTester mockMvcTester = MockMvcTester.create(mockMvc); - HttpMessageConverters converters = httpMessageConverters.getIfAvailable(); - if (converters != null) { - mockMvcTester = mockMvcTester.withHttpMessageConverters(converters); + List 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; } diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index ebb87984322..53ad6523acb 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -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 messageConvertersProvider; - private final ObjectProvider dispatcherServletPath; private final ObjectProvider> servletRegistrations; + private final ObjectProvider httpMessageConvertersCustomizerProvider; + private final @Nullable ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; private @Nullable ServletContext servletContext; @@ -217,7 +217,8 @@ public final class WebMvcAutoConfiguration { private final ObjectProvider apiVersionDeprecationHandler; WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, - ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider, + ListableBeanFactory beanFactory, + ObjectProvider httpMessageConvertersCustomizerProvider, ObjectProvider resourceHandlerRegistrationCustomizerProvider, ObjectProvider dispatcherServletPath, ObjectProvider> 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> converters) { - this.messageConvertersProvider - .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters())); + public void configureMessageConverters(ServerBuilder builder) { + this.httpMessageConvertersCustomizerProvider.forEach((customizer) -> customizer.customize(builder)); } @Override diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java index a5493ef6320..83dc093df2a 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java @@ -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> 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) { diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index a19c8b154a0..63eaebfcedb 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -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(); }); }