From 5e01c66552247cb30512f2f8569ac36f97808819 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 5 Jul 2023 14:11:28 +0100 Subject: [PATCH] Add RestClient HttpMessageConverters support Update `RestClientAutoConfiguration` to apply `HttpMessageConverters` configuration. See gh-36213 --- ...MessageConvertersRestClientCustomizer.java | 60 +++++++++++++++ .../client/RestClientAutoConfiguration.java | 11 +++ ...geConvertersRestClientCustomizerTests.java | 77 +++++++++++++++++++ .../RestClientAutoConfigurationTests.java | 64 +++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java new file mode 100644 index 0000000000..c5372d5d7f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.autoconfigure.web.client; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClient; + +/** + * {@link RestClientCustomizer} to apply {@link HttpMessageConverter + * HttpMessageConverters}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer { + + private final Iterable> messageConverters; + + public HttpMessageConvertersRestClientCustomizer(HttpMessageConverter... messageConverters) { + Assert.notNull(messageConverters, "MessageConverters must not be null"); + this.messageConverters = Arrays.asList(messageConverters); + } + + HttpMessageConvertersRestClientCustomizer(HttpMessageConverters messageConverters) { + this.messageConverters = messageConverters; + } + + @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); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java index e62dc869f4..b33fc882f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; @@ -28,6 +29,8 @@ import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Scope; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.client.RestClient; /** @@ -45,6 +48,14 @@ import org.springframework.web.client.RestClient; @Conditional(NotReactiveWebApplicationCondition.class) public class RestClientAutoConfiguration { + @Bean + @ConditionalOnMissingBean + @Order(Ordered.LOWEST_PRECEDENCE) + public HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer( + ObjectProvider messageConverters) { + return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique()); + } + @Bean @Scope("prototype") @ConditionalOnMissingBean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java new file mode 100644 index 0000000000..f4138baa18 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.autoconfigure.web.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +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 + */ +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); + } + + @SuppressWarnings("unchecked") + 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; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java index 2a256bb3fb..368c29d88d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java @@ -16,14 +16,21 @@ package org.springframework.boot.autoconfigure.web.client; +import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -77,6 +84,49 @@ 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() { + this.contextRunner.withUserConfiguration(RestClientConfig.class).run((context) -> { + RestClient restClient = context.getBean(RestClient.class); + RestClient defaultRestClient = RestClient.builder().build(); + List> actualConverters = (List>) ReflectionTestUtils + .getField(restClient, "messageConverters"); + List> expectedConverters = (List>) ReflectionTestUtils + .getField(defaultRestClient, "messageConverters"); + assertThat(actualConverters).hasSameSizeAs(expectedConverters); + }); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void restClientWhenHasCustomMessageConvertersShouldHaveMessageConverters() { + this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .withUserConfiguration(CustomHttpMessageConverter.class, RestClientConfig.class) + .run((context) -> { + RestClient restClient = context.getBean(RestClient.class); + List> actualConverters = (List>) ReflectionTestUtils + .getField(restClient, "messageConverters"); + assertThat(actualConverters).extracting(HttpMessageConverter::getClass) + .contains((Class) CustomHttpMessageConverter.class); + }); + } + @Configuration(proxyBeanMethods = false) static class CodecConfiguration { @@ -111,4 +161,18 @@ class RestClientAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class RestClientConfig { + + @Bean + RestClient restClient(RestClient.Builder restClientBuilder) { + return restClientBuilder.build(); + } + + } + + static class CustomHttpMessageConverter extends StringHttpMessageConverter { + + } + }