From 1631ae23f5541753c9e83b529b356fc833535013 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 9 Sep 2020 11:47:13 +0200 Subject: [PATCH] Allow RestTemplateBuilder to be further customized Closes gh-23389 --- .../client/RestTemplateAutoConfiguration.java | 33 ++++---- .../client/RestTemplateBuilderConfigurer.java | 78 +++++++++++++++++++ .../RestTemplateAutoConfigurationTests.java | 59 +++++++++++--- .../docs/asciidoc/spring-boot-features.adoc | 11 ++- ...stTemplateBuilderCustomizationExample.java | 43 ++++++++++ 5 files changed, 192 insertions(+), 32 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateBuilderCustomizationExample.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java index 23b128790c..970f600c27 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java @@ -16,9 +16,6 @@ package org.springframework.boot.autoconfigure.web.client; -import java.util.Collection; -import java.util.List; -import java.util.function.BiFunction; import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; @@ -57,26 +54,24 @@ public class RestTemplateAutoConfiguration { @Bean @Lazy @ConditionalOnMissingBean - public RestTemplateBuilder restTemplateBuilder(ObjectProvider messageConverters, + public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer( + ObjectProvider messageConverters, ObjectProvider restTemplateCustomizers, ObjectProvider> restTemplateRequestCustomizers) { - RestTemplateBuilder builder = new RestTemplateBuilder(); - HttpMessageConverters converters = messageConverters.getIfUnique(); - if (converters != null) { - builder = builder.messageConverters(converters.getConverters()); - } - builder = addCustomizers(builder, restTemplateCustomizers, RestTemplateBuilder::customizers); - builder = addCustomizers(builder, restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); - return builder; + RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer(); + configurer.setHttpMessageConverters(messageConverters.getIfUnique()); + configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().collect(Collectors.toList())); + configurer.setRestTemplateRequestCustomizers( + restTemplateRequestCustomizers.orderedStream().collect(Collectors.toList())); + return configurer; } - private RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, ObjectProvider objectProvider, - BiFunction, RestTemplateBuilder> method) { - List customizers = objectProvider.orderedStream().collect(Collectors.toList()); - if (!customizers.isEmpty()) { - return method.apply(builder, customizers); - } - return builder; + @Bean + @Lazy + @ConditionalOnMissingBean + public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) { + RestTemplateBuilder builder = new RestTemplateBuilder(); + return restTemplateBuilderConfigurer.configure(builder); } static class NotReactiveWebApplicationCondition extends NoneNestedConditions { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java new file mode 100644 index 0000000000..55e3351b85 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2020 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.Collection; +import java.util.List; +import java.util.function.BiFunction; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.client.RestTemplateRequestCustomizer; +import org.springframework.util.ObjectUtils; + +/** + * Configure {@link RestTemplateBuilder} with sensible defaults. + * + * @author Stephane Nicoll + * @since 2.4.0 + */ +public final class RestTemplateBuilderConfigurer { + + private HttpMessageConverters httpMessageConverters; + + private List restTemplateCustomizers; + + private List> restTemplateRequestCustomizers; + + void setHttpMessageConverters(HttpMessageConverters httpMessageConverters) { + this.httpMessageConverters = httpMessageConverters; + } + + void setRestTemplateCustomizers(List restTemplateCustomizers) { + this.restTemplateCustomizers = restTemplateCustomizers; + } + + void setRestTemplateRequestCustomizers(List> restTemplateRequestCustomizers) { + this.restTemplateRequestCustomizers = restTemplateRequestCustomizers; + } + + /** + * Configure the specified {@link RestTemplateBuilder}. The builder can be further + * tuned and default settings can be overridden. + * @param builder the {@link RestTemplateBuilder} instance to configure + * @return the configured builder + */ + public RestTemplateBuilder configure(RestTemplateBuilder builder) { + if (this.httpMessageConverters != null) { + builder = builder.messageConverters(this.httpMessageConverters.getConverters()); + } + builder = addCustomizers(builder, this.restTemplateCustomizers, RestTemplateBuilder::customizers); + builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); + return builder; + } + + private RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, List customizers, + BiFunction, RestTemplateBuilder> method) { + if (!ObjectUtils.isEmpty(customizers)) { + return method.apply(builder, customizers); + } + return builder; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java index aa18715787..ed5767c4e5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java @@ -46,6 +46,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link RestTemplateAutoConfiguration} @@ -58,6 +59,12 @@ class RestTemplateAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class)); + @Test + void restTemplateBuilderConfigurerShouldBeLazilyDefined() { + this.contextRunner.run((context) -> assertThat( + context.getBeanFactory().getBeanDefinition("restTemplateBuilderConfigurer").isLazyInit()).isTrue()); + } + @Test void restTemplateBuilderShouldBeLazilyDefined() { this.contextRunner.run( @@ -101,24 +108,40 @@ class RestTemplateAutoConfigurationTests { }); } + @Test + void restTemplateShouldApplyCustomizer() { + this.contextRunner.withUserConfiguration(RestTemplateConfig.class, RestTemplateCustomizerConfig.class) + .run((context) -> { + assertThat(context).hasSingleBean(RestTemplate.class); + RestTemplate restTemplate = context.getBean(RestTemplate.class); + RestTemplateCustomizer customizer = context.getBean(RestTemplateCustomizer.class); + verify(customizer).customize(restTemplate); + }); + } + @Test void restTemplateWhenHasCustomBuilderShouldUseCustomBuilder() { - this.contextRunner.withUserConfiguration(RestTemplateConfig.class, CustomRestTemplateBuilderConfig.class) + this.contextRunner.withUserConfiguration(RestTemplateConfig.class, CustomRestTemplateBuilderConfig.class, + RestTemplateCustomizerConfig.class).run((context) -> { + assertThat(context).hasSingleBean(RestTemplate.class); + RestTemplate restTemplate = context.getBean(RestTemplate.class); + assertThat(restTemplate.getMessageConverters()).hasSize(1); + assertThat(restTemplate.getMessageConverters().get(0)) + .isInstanceOf(CustomHttpMessageConverter.class); + verifyNoInteractions(context.getBean(RestTemplateCustomizer.class)); + }); + } + + @Test + void restTemplateWhenHasCustomBuilderCouldReuseBuilderConfigurer() { + this.contextRunner.withUserConfiguration(RestTemplateConfig.class, + CustomRestTemplateBuilderWithConfigurerConfig.class, RestTemplateCustomizerConfig.class) .run((context) -> { assertThat(context).hasSingleBean(RestTemplate.class); RestTemplate restTemplate = context.getBean(RestTemplate.class); assertThat(restTemplate.getMessageConverters()).hasSize(1); assertThat(restTemplate.getMessageConverters().get(0)) .isInstanceOf(CustomHttpMessageConverter.class); - }); - } - - @Test - void restTemplateShouldApplyCustomizer() { - this.contextRunner.withUserConfiguration(RestTemplateConfig.class, RestTemplateCustomizerConfig.class) - .run((context) -> { - assertThat(context).hasSingleBean(RestTemplate.class); - RestTemplate restTemplate = context.getBean(RestTemplate.class); RestTemplateCustomizer customizer = context.getBean(RestTemplateCustomizer.class); verify(customizer).customize(restTemplate); }); @@ -147,14 +170,16 @@ class RestTemplateAutoConfigurationTests { @Test void whenServletWebApplicationRestTemplateBuilderIsConfigured() { new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(RestTemplateBuilder.class)); + .run((context) -> assertThat(context).hasSingleBean(RestTemplateBuilder.class) + .hasSingleBean(RestTemplateBuilderConfigurer.class)); } @Test void whenReactiveWebApplicationRestTemplateBuilderIsNotConfigured() { new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class)) - .run((context) -> assertThat(context).doesNotHaveBean(RestTemplateBuilder.class)); + .run((context) -> assertThat(context).doesNotHaveBean(RestTemplateBuilder.class) + .doesNotHaveBean(RestTemplateBuilderConfigurer.class)); } @Configuration(proxyBeanMethods = false) @@ -208,6 +233,16 @@ class RestTemplateAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomRestTemplateBuilderWithConfigurerConfig { + + @Bean + RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()).messageConverters(new CustomHttpMessageConverter()); + } + + } + @Configuration(proxyBeanMethods = false) static class RestTemplateCustomizerConfig { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 5fe73f9cc8..3c87912404 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -5975,7 +5975,16 @@ The following example shows a customizer that configures the use of a proxy for include::{code-examples}/web/client/RestTemplateProxyCustomizationExample.java[tag=customizer] ---- -Finally, the most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean. +Finally, you can also create your own `RestTemplateBuilder` bean. +To prevent switching off the auto-configuration of a `RestTemplateBuilder` and prevent any `RestTemplateCustomizer` beans from being used, make sure to configure your custom instance with a `RestTemplateBuilderConfigurer`. +The following example exposes a `RestTemplateBuilder` with what Spring Boot would auto-configure, except that custom connect and read timeouts are also specified: + +[source,java,indent=0] +---- +include::{code-examples}/web/client/RestTemplateBuilderCustomizationExample.java[tag=customizer] +---- + +The most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean without using a configurer. Doing so switches off the auto-configuration of a `RestTemplateBuilder` and prevents any `RestTemplateCustomizer` beans from being used. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateBuilderCustomizationExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateBuilderCustomizationExample.java new file mode 100644 index 0000000000..d67e4328a6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateBuilderCustomizationExample.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 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.docs.web.client; + +import java.time.Duration; + +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Example configuration for using a {@link RestTemplateBuilderConfigurer} to configure a + * custom {@link RestTemplateBuilder}. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +public class RestTemplateBuilderCustomizationExample { + + // tag::customizer[] + @Bean + public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()).setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(2)); + } + // end::customizer[] + +}