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 a41463ad70..bd8536c3f0 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,7 +16,9 @@ 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; @@ -32,6 +34,7 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConf import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.NotReactiveWebApplicationCondition; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.client.RestTemplateRequestCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -53,15 +56,23 @@ public class RestTemplateAutoConfiguration { @Bean @ConditionalOnMissingBean public RestTemplateBuilder restTemplateBuilder(ObjectProvider messageConverters, - ObjectProvider restTemplateCustomizers) { + ObjectProvider restTemplateCustomizers, + ObjectProvider> restTemplateRequestCustomizers) { RestTemplateBuilder builder = new RestTemplateBuilder(); HttpMessageConverters converters = messageConverters.getIfUnique(); if (converters != null) { builder = builder.messageConverters(converters.getConverters()); } - List customizers = restTemplateCustomizers.orderedStream().collect(Collectors.toList()); + builder = addCustomizers(builder, restTemplateCustomizers, RestTemplateBuilder::customizers); + builder = addCustomizers(builder, restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); + return builder; + } + + private RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, ObjectProvider objectProvider, + BiFunction, RestTemplateBuilder> method) { + List customizers = objectProvider.orderedStream().collect(Collectors.toList()); if (!customizers.isEmpty()) { - builder = builder.customizers(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 7fc93af5c1..2d6420b356 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 @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.web.client; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; @@ -28,14 +29,21 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.client.RestTemplateRequestCustomizer; 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.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; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -109,6 +117,20 @@ class RestTemplateAutoConfigurationTests { }); } + @Test + void restTemplateShouldApplyRequestCustomizer() { + this.contextRunner.withUserConfiguration(RestTemplateRequestCustomizerConfig.class).run((context) -> { + RestTemplateBuilder builder = context.getBean(RestTemplateBuilder.class); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + MockClientHttpRequest request = new MockClientHttpRequest(); + request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK)); + given(requestFactory.createRequest(any(), any())).willReturn(request); + RestTemplate restTemplate = builder.requestFactory(() -> requestFactory).build(); + restTemplate.getForEntity("http://localhost:8080/test", String.class); + assertThat(request.getHeaders()).containsEntry("spring", Collections.singletonList("boot")); + }); + } + @Test void builderShouldBeFreshForEachUse() { this.contextRunner.withUserConfiguration(DirtyRestTemplateConfig.class) @@ -189,6 +211,16 @@ class RestTemplateAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class RestTemplateRequestCustomizerConfig { + + @Bean + public RestTemplateRequestCustomizer restTemplateRequestCustomizer() { + return (request) -> request.getHeaders().add("spring", "boot"); + } + + } + static class CustomHttpMessageConverter extends StringHttpMessageConverter { } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java index c22a89eded..f7dcc228fb 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java @@ -31,6 +31,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate.HttpClientOptio import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; @@ -43,6 +44,7 @@ import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.Base64Utils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.web.client.ResponseErrorHandler; @@ -97,7 +99,8 @@ class TestRestTemplateTests { RestTemplateBuilder builder = new RestTemplateBuilder().requestFactory(() -> customFactory); TestRestTemplate testRestTemplate = new TestRestTemplate(builder).withBasicAuth("test", "test"); RestTemplate restTemplate = testRestTemplate.getRestTemplate(); - assertThat(restTemplate.getRequestFactory().getClass().getName()).contains("BasicAuth"); + assertThat(restTemplate.getRequestFactory().getClass().getName()) + .contains("RestTemplateBuilderClientHttpRequestFactoryWrapper"); Object requestFactory = ReflectionTestUtils.getField(restTemplate.getRequestFactory(), "requestFactory"); assertThat(requestFactory).isEqualTo(customFactory).hasSameClassAs(customFactory); } @@ -125,10 +128,9 @@ class TestRestTemplateTests { } @Test - void authenticated() { - RestTemplate restTemplate = new TestRestTemplate("user", "password").getRestTemplate(); - ClientHttpRequestFactory factory = restTemplate.getRequestFactory(); - assertThat(factory.getClass().getName()).contains("BasicAuthentication"); + void authenticated() throws Exception { + TestRestTemplate restTemplate = new TestRestTemplate("user", "password"); + assertBasicAuthorizationCredentials(restTemplate, "user", "password"); } @Test @@ -201,11 +203,12 @@ class TestRestTemplateTests { } @Test - void withBasicAuthAddsBasicAuthClientFactoryWhenNotAlreadyPresent() { + void withBasicAuthAddsBasicAuthClientFactoryWhenNotAlreadyPresent() throws Exception { TestRestTemplate original = new TestRestTemplate(); TestRestTemplate basicAuth = original.withBasicAuth("user", "password"); assertThat(getConverterClasses(original)).containsExactlyElementsOf(getConverterClasses(basicAuth)); - assertThat(basicAuth.getRestTemplate().getRequestFactory().getClass().getName()).contains("BasicAuth"); + assertThat(basicAuth.getRestTemplate().getRequestFactory().getClass().getName()) + .contains("RestTemplateBuilderClientHttpRequestFactoryWrapper"); assertThat(ReflectionTestUtils.getField(basicAuth.getRestTemplate().getRequestFactory(), "requestFactory")) .isInstanceOf(CustomHttpComponentsClientHttpRequestFactory.class); assertThat(basicAuth.getRestTemplate().getInterceptors()).isEmpty(); @@ -213,11 +216,12 @@ class TestRestTemplateTests { } @Test - void withBasicAuthReplacesBasicAuthClientFactoryWhenAlreadyPresent() { + void withBasicAuthReplacesBasicAuthClientFactoryWhenAlreadyPresent() throws Exception { TestRestTemplate original = new TestRestTemplate("foo", "bar").withBasicAuth("replace", "replace"); TestRestTemplate basicAuth = original.withBasicAuth("user", "password"); assertThat(getConverterClasses(basicAuth)).containsExactlyElementsOf(getConverterClasses(original)); - assertThat(basicAuth.getRestTemplate().getRequestFactory().getClass().getName()).contains("BasicAuth"); + assertThat(basicAuth.getRestTemplate().getRequestFactory().getClass().getName()) + .contains("RestTemplateBuilderClientHttpRequestFactoryWrapper"); assertThat(ReflectionTestUtils.getField(basicAuth.getRestTemplate().getRequestFactory(), "requestFactory")) .isInstanceOf(CustomHttpComponentsClientHttpRequestFactory.class); assertThat(basicAuth.getRestTemplate().getInterceptors()).isEmpty(); @@ -342,11 +346,12 @@ class TestRestTemplateTests { } private void assertBasicAuthorizationCredentials(TestRestTemplate testRestTemplate, String username, - String password) { + String password) throws Exception { ClientHttpRequestFactory requestFactory = testRestTemplate.getRestTemplate().getRequestFactory(); - Object authentication = ReflectionTestUtils.getField(requestFactory, "authentication"); - assertThat(authentication).hasFieldOrPropertyWithValue("username", username); - assertThat(authentication).hasFieldOrPropertyWithValue("password", password); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.POST); + assertThat(request.getHeaders()).containsKeys(HttpHeaders.AUTHORIZATION); + assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly( + "Basic " + Base64Utils.encodeToString(String.format("%s:%s", username, password).getBytes())); } @@ -356,16 +361,4 @@ class TestRestTemplateTests { } - static class TestClientHttpRequestFactory implements ClientHttpRequestFactory { - - TestClientHttpRequestFactory(String value) { - } - - @Override - public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { - return null; - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java index 059499ccef..2039822288 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java @@ -19,15 +19,13 @@ package org.springframework.boot.web.client; import java.nio.charset.Charset; import org.springframework.http.HttpHeaders; -import org.springframework.http.client.ClientHttpRequest; import org.springframework.util.Assert; /** - * Basic authentication properties which are used by - * {@link BasicAuthenticationClientHttpRequestFactory}. + * Basic authentication details to be applied to {@link HttpHeaders}. * * @author Dmytro Nosan - * @see BasicAuthenticationClientHttpRequestFactory + * @author Ilya Lukyanovich */ class BasicAuthentication { @@ -45,8 +43,7 @@ class BasicAuthentication { this.charset = charset; } - void applyTo(ClientHttpRequest request) { - HttpHeaders headers = request.getHeaders(); + public void applyTo(HttpHeaders headers) { if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) { headers.setBasicAuth(this.username, this.password, this.charset); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthenticationClientHttpRequestFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthenticationClientHttpRequestFactory.java deleted file mode 100644 index 8e58a9ad08..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthenticationClientHttpRequestFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 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.web.client; - -import java.io.IOException; -import java.net.URI; - -import org.springframework.http.HttpMethod; -import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.util.Assert; - -/** - * {@link ClientHttpRequestFactory} to apply a given HTTP Basic Authentication - * username/password pair, unless a custom Authorization header has been set before. - * - * @author Dmytro Nosan - */ -class BasicAuthenticationClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper { - - private final BasicAuthentication authentication; - - BasicAuthenticationClientHttpRequestFactory(BasicAuthentication authentication, - ClientHttpRequestFactory clientHttpRequestFactory) { - super(clientHttpRequestFactory); - Assert.notNull(authentication, "Authentication must not be null"); - this.authentication = authentication; - } - - @Override - protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) - throws IOException { - ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); - this.authentication.applyTo(request); - return request; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java index 75bca79ff9..9de267f268 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java @@ -25,12 +25,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; +import reactor.netty.http.client.HttpClientRequest; + import org.springframework.beans.BeanUtils; import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; import org.springframework.http.client.ClientHttpRequest; @@ -64,17 +68,22 @@ import org.springframework.web.util.UriTemplateHandler; * @author Brian Clozel * @author Dmytro Nosan * @author Kevin Strijbos + * @author Ilya Lukyanovich * @since 1.4.0 */ public class RestTemplateBuilder { + private final RequestFactoryCustomizer requestFactoryCustomizer; + private final boolean detectRequestFactory; private final String rootUri; private final Set> messageConverters; - private final Supplier requestFactorySupplier; + private final Set interceptors; + + private final Supplier requestFactory; private final UriTemplateHandler uriTemplateHandler; @@ -82,11 +91,11 @@ public class RestTemplateBuilder { private final BasicAuthentication basicAuthentication; - private final Set restTemplateCustomizers; + private final Map defaultHeaders; - private final RequestFactoryCustomizer requestFactoryCustomizer; + private final Set customizers; - private final Set interceptors; + private final Set> requestCustomizers; /** * Create a new {@link RestTemplateBuilder} instance. @@ -95,33 +104,38 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder(RestTemplateCustomizer... customizers) { Assert.notNull(customizers, "Customizers must not be null"); + this.requestFactoryCustomizer = new RequestFactoryCustomizer(); this.detectRequestFactory = true; this.rootUri = null; this.messageConverters = null; - this.requestFactorySupplier = null; + this.interceptors = Collections.emptySet(); + this.requestFactory = null; this.uriTemplateHandler = null; this.errorHandler = null; this.basicAuthentication = null; - this.restTemplateCustomizers = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(customizers))); - this.requestFactoryCustomizer = new RequestFactoryCustomizer(); - this.interceptors = Collections.emptySet(); + this.defaultHeaders = Collections.emptyMap(); + this.customizers = setOf(customizers); + this.requestCustomizers = Collections.emptySet(); } - private RestTemplateBuilder(boolean detectRequestFactory, String rootUri, - Set> messageConverters, Supplier requestFactorySupplier, + private RestTemplateBuilder(RequestFactoryCustomizer requestFactoryCustomizer, boolean detectRequestFactory, + String rootUri, Set> messageConverters, + Set interceptors, Supplier requestFactorySupplier, UriTemplateHandler uriTemplateHandler, ResponseErrorHandler errorHandler, - BasicAuthentication basicAuthentication, Set restTemplateCustomizers, - RequestFactoryCustomizer requestFactoryCustomizer, Set interceptors) { + BasicAuthentication basicAuthentication, Map defaultHeaders, + Set customizers, Set> requestCustomizers) { + this.requestFactoryCustomizer = requestFactoryCustomizer; this.detectRequestFactory = detectRequestFactory; this.rootUri = rootUri; this.messageConverters = messageConverters; - this.requestFactorySupplier = requestFactorySupplier; + this.interceptors = interceptors; + this.requestFactory = requestFactorySupplier; this.uriTemplateHandler = uriTemplateHandler; this.errorHandler = errorHandler; this.basicAuthentication = basicAuthentication; - this.restTemplateCustomizers = restTemplateCustomizers; - this.requestFactoryCustomizer = requestFactoryCustomizer; - this.interceptors = interceptors; + this.defaultHeaders = defaultHeaders; + this.customizers = customizers; + this.requestCustomizers = requestCustomizers; } /** @@ -132,9 +146,10 @@ public class RestTemplateBuilder { * @return a new builder instance */ public RestTemplateBuilder detectRequestFactory(boolean detectRequestFactory) { - return new RestTemplateBuilder(detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -144,9 +159,10 @@ public class RestTemplateBuilder { * @return a new builder instance */ public RestTemplateBuilder rootUri(String rootUri) { - return new RestTemplateBuilder(this.detectRequestFactory, rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -174,10 +190,10 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder messageConverters(Collection> messageConverters) { Assert.notNull(messageConverters, "MessageConverters must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, - Collections.unmodifiableSet(new LinkedHashSet>(messageConverters)), - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + setOf(messageConverters), this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -204,10 +220,10 @@ public class RestTemplateBuilder { public RestTemplateBuilder additionalMessageConverters( Collection> messageConverters) { Assert.notNull(messageConverters, "MessageConverters must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, - append(this.messageConverters, messageConverters), this.requestFactorySupplier, this.uriTemplateHandler, - this.errorHandler, this.basicAuthentication, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + append(this.messageConverters, messageConverters), this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -218,10 +234,10 @@ public class RestTemplateBuilder { * @see #messageConverters(HttpMessageConverter...) */ public RestTemplateBuilder defaultMessageConverters() { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, - Collections.unmodifiableSet(new LinkedHashSet<>(new RestTemplate().getMessageConverters())), - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + setOf(new RestTemplate().getMessageConverters()), this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -249,10 +265,10 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder interceptors(Collection interceptors) { Assert.notNull(interceptors, "interceptors must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, - Collections.unmodifiableSet(new LinkedHashSet<>(interceptors))); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, setOf(interceptors), this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -278,9 +294,10 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder additionalInterceptors(Collection interceptors) { Assert.notNull(interceptors, "interceptors must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, append(this.interceptors, interceptors)); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, append(this.interceptors, interceptors), this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -308,15 +325,15 @@ public class RestTemplateBuilder { /** * Set the {@code Supplier} of {@link ClientHttpRequestFactory} that should be called * each time we {@link #build()} a new {@link RestTemplate} instance. - * @param requestFactorySupplier the supplier for the request factory + * @param requestFactory the supplier for the request factory * @return a new builder instance * @since 2.0.0 */ - public RestTemplateBuilder requestFactory(Supplier requestFactorySupplier) { - Assert.notNull(requestFactorySupplier, "RequestFactory Supplier must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + public RestTemplateBuilder requestFactory(Supplier requestFactory) { + Assert.notNull(requestFactory, "RequestFactory Supplier must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, requestFactory, this.uriTemplateHandler, this.errorHandler, + this.basicAuthentication, this.defaultHeaders, this.customizers, this.requestCustomizers); } /** @@ -327,9 +344,9 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder uriTemplateHandler(UriTemplateHandler uriTemplateHandler) { Assert.notNull(uriTemplateHandler, "UriTemplateHandler must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, uriTemplateHandler, this.errorHandler, + this.basicAuthentication, this.defaultHeaders, this.customizers, this.requestCustomizers); } /** @@ -340,9 +357,9 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder errorHandler(ResponseErrorHandler errorHandler) { Assert.notNull(errorHandler, "ErrorHandler must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, errorHandler, + this.basicAuthentication, this.defaultHeaders, this.customizers, this.requestCustomizers); } /** @@ -366,72 +383,27 @@ public class RestTemplateBuilder { * @param charset the charset to use * @return a new builder instance * @since 2.2.0 - * @see #basicAuthentication(String, String) */ public RestTemplateBuilder basicAuthentication(String username, String password, Charset charset) { - BasicAuthentication basicAuthentication = new BasicAuthentication(username, password, charset); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer, this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, new BasicAuthentication(username, password, charset), this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** - * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be - * applied to the {@link RestTemplate}. Customizers are applied in the order that they - * were added after builder configuration has been applied. Setting this value will - * replace any previously configured customizers. - * @param restTemplateCustomizers the customizers to set + * Add a default header that will be set if not already present on the outgoing + * {@link HttpClientRequest}. + * @param name the name of the header + * @param value the header value * @return a new builder instance - * @see #additionalCustomizers(RestTemplateCustomizer...) + * @since 2.2.0 */ - public RestTemplateBuilder customizers(RestTemplateCustomizer... restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return customizers(Arrays.asList(restTemplateCustomizers)); - } - - /** - * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be - * applied to the {@link RestTemplate}. Customizers are applied in the order that they - * were added after builder configuration has been applied. Setting this value will - * replace any previously configured customizers. - * @param restTemplateCustomizers the customizers to set - * @return a new builder instance - * @see #additionalCustomizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder customizers(Collection restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - Collections.unmodifiableSet(new LinkedHashSet(restTemplateCustomizers)), - this.requestFactoryCustomizer, this.interceptors); - } - - /** - * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied - * to the {@link RestTemplate}. Customizers are applied in the order that they were - * added after builder configuration has been applied. - * @param restTemplateCustomizers the customizers to add - * @return a new builder instance - * @see #customizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder additionalCustomizers(RestTemplateCustomizer... restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return additionalCustomizers(Arrays.asList(restTemplateCustomizers)); - } - - /** - * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied - * to the {@link RestTemplate}. Customizers are applied in the order that they were - * added after builder configuration has been applied. - * @param customizers the customizers to add - * @return a new builder instance - * @see #customizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder additionalCustomizers(Collection customizers) { - Assert.notNull(customizers, "RestTemplateCustomizers must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - append(this.restTemplateCustomizers, customizers), this.requestFactoryCustomizer, this.interceptors); + public RestTemplateBuilder defaultHeader(String name, String value) { + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, append(this.defaultHeaders, name, value), this.customizers, + this.requestCustomizers); } /** @@ -441,10 +413,10 @@ public class RestTemplateBuilder { * @since 2.1.0 */ public RestTemplateBuilder setConnectTimeout(Duration connectTimeout) { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer.connectTimeout(connectTimeout), - this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer.connectTimeout(connectTimeout), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -454,10 +426,10 @@ public class RestTemplateBuilder { * @since 2.1.0 */ public RestTemplateBuilder setReadTimeout(Duration readTimeout) { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer.readTimeout(readTimeout), - this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer.readTimeout(readTimeout), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -470,10 +442,136 @@ public class RestTemplateBuilder { * @see HttpComponentsClientHttpRequestFactory#setBufferRequestBody(boolean) */ public RestTemplateBuilder setBufferRequestBody(boolean bufferRequestBody) { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, - this.restTemplateCustomizers, this.requestFactoryCustomizer.bufferRequestBody(bufferRequestBody), - this.interceptors); + return new RestTemplateBuilder(this.requestFactoryCustomizer.bufferRequestBody(bufferRequestBody), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); + } + + /** + * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be + * applied to the {@link RestTemplate}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder customizers(RestTemplateCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be + * applied to the {@link RestTemplate}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder customizers(Collection customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, setOf(customizers), + this.requestCustomizers); + } + + /** + * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied + * to the {@link RestTemplate}. Customizers are applied in the order that they were + * added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder additionalCustomizers(RestTemplateCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied + * to the {@link RestTemplate}. Customizers are applied in the order that they were + * added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder additionalCustomizers(Collection customizers) { + Assert.notNull(customizers, "RestTemplateCustomizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, append(this.customizers, customizers), + this.requestCustomizers); + } + + /** + * Set the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. Setting this value will replace any previously + * configured request customizers. + * @param requestCustomizers the request customizers to set + * @return a new builder instance + * @since 2.2.0 + * @see #additionalRequestCustomizers(RestTemplateRequestCustomizer...) + */ + public RestTemplateBuilder requestCustomizers(RestTemplateRequestCustomizer... requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return requestCustomizers(Arrays.asList(requestCustomizers)); + } + + /** + * Set the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. Setting this value will replace any previously + * configured request customizers. + * @param requestCustomizers the request customizers to set + * @return a new builder instance + * @since 2.2.0 + * @see #additionalRequestCustomizers(RestTemplateRequestCustomizer...) + */ + public RestTemplateBuilder requestCustomizers( + Collection> requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + setOf(requestCustomizers)); + } + + /** + * Add the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. + * @param requestCustomizers the request customizers to add + * @return a new builder instance + * @since 2.2.0 + * @see #requestCustomizers(RestTemplateRequestCustomizer...) + */ + public RestTemplateBuilder additionalRequestCustomizers(RestTemplateRequestCustomizer... requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return additionalRequestCustomizers(Arrays.asList(requestCustomizers)); + } + + /** + * Add the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. + * @param requestCustomizers the request customizers to add + * @return a new builder instance + * @since 2.2.0 + * @see #requestCustomizers(Collection) + */ + public RestTemplateBuilder additionalRequestCustomizers( + Collection> requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + append(this.requestCustomizers, requestCustomizers)); } /** @@ -512,9 +610,7 @@ public class RestTemplateBuilder { if (requestFactory != null) { restTemplate.setRequestFactory(requestFactory); } - if (this.basicAuthentication != null) { - configureBasicAuthentication(restTemplate); - } + addClientHttpRequestFactoryWrapper(restTemplate); if (!CollectionUtils.isEmpty(this.messageConverters)) { restTemplate.setMessageConverters(new ArrayList<>(this.messageConverters)); } @@ -528,8 +624,8 @@ public class RestTemplateBuilder { RootUriTemplateHandler.addTo(restTemplate, this.rootUri); } restTemplate.getInterceptors().addAll(this.interceptors); - if (!CollectionUtils.isEmpty(this.restTemplateCustomizers)) { - for (RestTemplateCustomizer customizer : this.restTemplateCustomizers) { + if (!CollectionUtils.isEmpty(this.customizers)) { + for (RestTemplateCustomizer customizer : this.customizers) { customizer.customize(restTemplate); } } @@ -544,8 +640,8 @@ public class RestTemplateBuilder { */ public ClientHttpRequestFactory buildRequestFactory() { ClientHttpRequestFactory requestFactory = null; - if (this.requestFactorySupplier != null) { - requestFactory = this.requestFactorySupplier.get(); + if (this.requestFactory != null) { + requestFactory = this.requestFactory.get(); } else if (this.detectRequestFactory) { requestFactory = new ClientHttpRequestFactorySupplier().get(); @@ -558,7 +654,10 @@ public class RestTemplateBuilder { return requestFactory; } - private void configureBasicAuthentication(RestTemplate restTemplate) { + private void addClientHttpRequestFactoryWrapper(RestTemplate restTemplate) { + if (this.basicAuthentication == null && this.defaultHeaders.isEmpty() && this.requestCustomizers.isEmpty()) { + return; + } List interceptors = null; if (!restTemplate.getInterceptors().isEmpty()) { // Stash and clear the interceptors so we can access the real factory @@ -566,20 +665,41 @@ public class RestTemplateBuilder { restTemplate.getInterceptors().clear(); } ClientHttpRequestFactory requestFactory = restTemplate.getRequestFactory(); - restTemplate.setRequestFactory( - new BasicAuthenticationClientHttpRequestFactory(this.basicAuthentication, requestFactory)); + ClientHttpRequestFactory wrapper = new RestTemplateBuilderClientHttpRequestFactoryWrapper(requestFactory, + this.basicAuthentication, this.defaultHeaders, this.requestCustomizers); + restTemplate.setRequestFactory(wrapper); // Restore the original interceptors if (interceptors != null) { restTemplate.getInterceptors().addAll(interceptors); } } - private Set append(Set set, Collection additions) { - Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); - result.addAll(additions); + @SuppressWarnings("unchecked") + private Set setOf(T... items) { + return setOf(Arrays.asList(items)); + } + + private Set setOf(Collection collection) { + return Collections.unmodifiableSet(new LinkedHashSet<>(collection)); + } + + private static Set append(Collection collection, Collection additions) { + Set result = new LinkedHashSet<>((collection != null) ? collection : Collections.emptySet()); + if (additions != null) { + result.addAll(additions); + } return Collections.unmodifiableSet(result); } + private static Map append(Map map, K key, V value) { + Map result = new LinkedHashMap<>((map != null) ? map : Collections.emptyMap()); + result.put(key, value); + return Collections.unmodifiableMap(result); + } + + /** + * Internal customizer used to apply {@link ClientHttpRequestFactory} settings. + */ private static class RequestFactoryCustomizer implements Consumer { private final Duration connectTimeout; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java new file mode 100644 index 0000000000..bc3019ad65 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2019 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.web.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.util.LambdaSafe; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; + +/** + * {@link ClientHttpRequestFactory} to apply customizations from the + * {@link RestTemplateBuilder}. + * + * @author Dmytro Nosan + * @author Ilya Lukyanovich + */ +class RestTemplateBuilderClientHttpRequestFactoryWrapper extends AbstractClientHttpRequestFactoryWrapper { + + private final BasicAuthentication basicAuthentication; + + private final Map defaultHeaders; + + private final Set> requestCustomizers; + + RestTemplateBuilderClientHttpRequestFactoryWrapper(ClientHttpRequestFactory requestFactory, + BasicAuthentication basicAuthentication, Map defaultHeaders, + Set> requestCustomizers) { + super(requestFactory); + this.basicAuthentication = basicAuthentication; + this.defaultHeaders = defaultHeaders; + this.requestCustomizers = requestCustomizers; + } + + @Override + @SuppressWarnings("unchecked") + protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) + throws IOException { + ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); + HttpHeaders headers = request.getHeaders(); + if (this.basicAuthentication != null) { + this.basicAuthentication.applyTo(headers); + } + this.defaultHeaders.forEach(headers::addIfAbsent); + LambdaSafe.callbacks(RestTemplateRequestCustomizer.class, this.requestCustomizers, request) + .invoke((customizer) -> customizer.customize(request)); + return request; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateRequestCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateRequestCustomizer.java new file mode 100644 index 0000000000..2d7e0210d7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateRequestCustomizer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2018 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.web.client; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.web.client.RestTemplate; + +/** + * Callback interface that can be used to customize the {@link ClientHttpRequest} sent + * from a {@link RestTemplate}. + * + * @param the {@link ClientHttpRequest} type + * @author Ilya Lukyanovich + * @author Phillip Webb + * @since 2.2.0 + * @see RestTemplateBuilder + */ +@FunctionalInterface +public interface RestTemplateRequestCustomizer { + + /** + * Customize the specified {@link ClientHttpRequest}. + * @param request the request to customize + */ + void customize(T request); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/BasicAuthenticationClientHttpRequestFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/BasicAuthenticationClientHttpRequestFactoryTests.java deleted file mode 100644 index cf8951052e..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/BasicAuthenticationClientHttpRequestFactoryTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-2019 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.web.client; - -import java.io.IOException; -import java.net.URI; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link BasicAuthenticationClientHttpRequestFactory}. - * - * @author Dmytro Nosan - */ -class BasicAuthenticationClientHttpRequestFactoryTests { - - private final HttpHeaders headers = new HttpHeaders(); - - private final BasicAuthentication authentication = new BasicAuthentication("spring", "boot", null); - - private ClientHttpRequestFactory requestFactory; - - @BeforeEach - public void setUp() throws IOException { - ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); - ClientHttpRequest request = mock(ClientHttpRequest.class); - given(requestFactory.createRequest(any(), any())).willReturn(request); - given(request.getHeaders()).willReturn(this.headers); - this.requestFactory = new BasicAuthenticationClientHttpRequestFactory(this.authentication, requestFactory); - } - - @Test - void shouldAddAuthorizationHeader() throws IOException { - ClientHttpRequest request = createRequest(); - assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); - } - - @Test - void shouldNotAddAuthorizationHeaderAuthorizationAlreadySet() throws IOException { - this.headers.setBasicAuth("boot", "spring"); - ClientHttpRequest request = createRequest(); - assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).doesNotContain("Basic c3ByaW5nOmJvb3Q="); - - } - - private ClientHttpRequest createRequest() throws IOException { - return this.requestFactory.createRequest(URI.create("https://localhost:8080"), HttpMethod.POST); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java new file mode 100644 index 0000000000..f136547137 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2019 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.web.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestTemplateBuilderClientHttpRequestFactoryWrapper}. + * + * @author Dmytro Nosan + * @author Ilya Lukyanovich + * @author Phillip Webb + */ +public class RestTemplateBuilderClientHttpRequestFactoryWrapperTests { + + private ClientHttpRequestFactory requestFactory; + + private final HttpHeaders headers = new HttpHeaders(); + + @BeforeEach + public void setUp() throws IOException { + this.requestFactory = mock(ClientHttpRequestFactory.class); + ClientHttpRequest request = mock(ClientHttpRequest.class); + given(this.requestFactory.createRequest(any(), any())).willReturn(request); + given(request.getHeaders()).willReturn(this.headers); + } + + @Test + void createRequestWhenHasBasicAuthAndNoAuthHeaderAddsHeader() throws IOException { + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, + new BasicAuthentication("spring", "boot", null), Collections.emptyMap(), Collections.emptySet()); + ClientHttpRequest request = createRequest(); + assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); + } + + @Test + void createRequestWhenHasBasicAuthAndExistingAuthHeaderDoesNotAddHeader() throws IOException { + this.headers.setBasicAuth("boot", "spring"); + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, + new BasicAuthentication("spring", "boot", null), Collections.emptyMap(), Collections.emptySet()); + ClientHttpRequest request = createRequest(); + assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).doesNotContain("Basic c3ByaW5nOmJvb3Q="); + } + + @Test + void createRequestWhenHasDefaultHeadersAddsMissing() throws IOException { + this.headers.add("one", "existing"); + Map defaultHeaders = new LinkedHashMap<>(); + defaultHeaders.put("one", "1"); + defaultHeaders.put("two", "2"); + defaultHeaders.put("three", "3"); + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, null, + defaultHeaders, Collections.emptySet()); + ClientHttpRequest request = createRequest(); + assertThat(request.getHeaders().get("one")).containsExactly("existing"); + assertThat(request.getHeaders().get("two")).containsExactly("2"); + assertThat(request.getHeaders().get("three")).containsExactly("3"); + } + + @Test + @SuppressWarnings("unchecked") + void createRequestWhenHasRequestCustomizersAppliesThemInOrder() throws IOException { + Set> customizers = new LinkedHashSet<>(); + customizers.add(mock(RestTemplateRequestCustomizer.class)); + customizers.add(mock(RestTemplateRequestCustomizer.class)); + customizers.add(mock(RestTemplateRequestCustomizer.class)); + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, null, + Collections.emptyMap(), customizers); + ClientHttpRequest request = createRequest(); + InOrder inOrder = inOrder(customizers.toArray()); + for (RestTemplateRequestCustomizer customizer : customizers) { + inOrder.verify((RestTemplateRequestCustomizer) customizer).customize(request); + } + } + + private ClientHttpRequest createRequest() throws IOException { + return this.requestFactory.createRequest(URI.create("https://localhost:8080"), HttpMethod.POST); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java index c8158b233d..818526780e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.web.client; +import java.io.IOException; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collections; @@ -29,7 +31,10 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; @@ -48,6 +53,7 @@ import org.springframework.web.util.UriTemplateHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -65,6 +71,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat * @author Andy Wilkinson * @author Dmytro Nosan * @author Kevin Strijbos + * @author Ilya Lukyanovich */ class RestTemplateBuilderTests { @@ -298,25 +305,53 @@ class RestTemplateBuilderTests { } @Test - void basicAuthenticationShouldApply() { + void basicAuthenticationShouldApply() throws Exception { RestTemplate template = this.builder.basicAuthentication("spring", "boot", StandardCharsets.UTF_8).build(); ClientHttpRequestFactory requestFactory = template.getRequestFactory(); - Object authentication = ReflectionTestUtils.getField(requestFactory, "authentication"); - assertThat(authentication).extracting("username", "password", "charset").containsExactly("spring", "boot", - StandardCharsets.UTF_8); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.POST); + assertThat(request.getHeaders()).containsOnlyKeys(HttpHeaders.AUTHORIZATION); + assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); + } + + @Test + void defaultHeaderAddsHeader() throws IOException { + RestTemplate template = this.builder.defaultHeader("spring", "boot").build(); + ClientHttpRequestFactory requestFactory = template.getRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.GET); + assertThat(request.getHeaders()).contains(entry("spring", Collections.singletonList("boot"))); + } + + @Test + void requestCustomizersAddsCustomizers() throws IOException { + RestTemplate template = this.builder + .requestCustomizers((request) -> request.getHeaders().add("spring", "framework")).build(); + ClientHttpRequestFactory requestFactory = template.getRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.GET); + assertThat(request.getHeaders()).contains(entry("spring", Collections.singletonList("framework"))); + } + + @Test + void additionalRequestCustomizersAddsCustomizers() throws IOException { + RestTemplate template = this.builder + .requestCustomizers((request) -> request.getHeaders().add("spring", "framework")) + .additionalRequestCustomizers((request) -> request.getHeaders().add("for", "java")).build(); + ClientHttpRequestFactory requestFactory = template.getRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.GET); + assertThat(request.getHeaders()).contains(entry("spring", Collections.singletonList("framework"))) + .contains(entry("for", Collections.singletonList("java"))); } @Test void customizersWhenCustomizersAreNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> this.builder.customizers((RestTemplateCustomizer[]) null)) - .withMessageContaining("RestTemplateCustomizers must not be null"); + .withMessageContaining("Customizers must not be null"); } @Test void customizersCollectionWhenCustomizersAreNullShouldThrowException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.builder.customizers((Set) null)) - .withMessageContaining("RestTemplateCustomizers must not be null"); + .withMessageContaining("Customizers must not be null"); } @Test @@ -348,7 +383,7 @@ class RestTemplateBuilderTests { void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.builder.additionalCustomizers((RestTemplateCustomizer[]) null)) - .withMessageContaining("RestTemplateCustomizers must not be null"); + .withMessageContaining("Customizers must not be null"); } @Test @@ -383,7 +418,8 @@ class RestTemplateBuilderTests { assertThat(actualRequestFactory).isInstanceOf(InterceptingClientHttpRequestFactory.class); ClientHttpRequestFactory authRequestFactory = (ClientHttpRequestFactory) ReflectionTestUtils .getField(actualRequestFactory, "requestFactory"); - assertThat(authRequestFactory).isInstanceOf(BasicAuthenticationClientHttpRequestFactory.class); + assertThat(authRequestFactory) + .isInstanceOf(RestTemplateBuilderClientHttpRequestFactoryWrapper.class); assertThat(authRequestFactory).hasFieldOrPropertyWithValue("requestFactory", requestFactory); }).build(); }