From 5d785390eb4fae08922d2d98f61a31e617cd0115 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 9 Sep 2019 11:37:55 -0700 Subject: [PATCH] Add `ClientHttpRequestInitializer` support Add `ClientHttpRequestInitializer` interface that can be used with any `HttpAccessor` to initialize each `ClientHttpRequest` before it's used. This provides a useful alternative to `ClientHttpRequestInterceptor` when users need to configure things like `HttpHeaders`. Closes gh-22002 --- .../client/ClientHttpRequestInitializer.java | 45 +++++++++++++++++++ .../http/client/support/HttpAccessor.java | 36 ++++++++++++++- .../web/client/RestTemplateTests.java | 27 +++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInitializer.java diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInitializer.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInitializer.java new file mode 100644 index 00000000000..d2d06e4981e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInitializer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-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.http.client; + +import org.springframework.http.client.support.HttpAccessor; + +/** + * Callback interface for initializing a {@link ClientHttpRequest} prior to it + * being used. + * + *

Typically used with {@link HttpAccessor} and subclasses such as + * {@link org.springframework.web.client.RestTemplate RestTemplate} to apply + * consistent settings or headers to each request. + * + *

Unlike {@link ClientHttpRequestInterceptor}, this interface can apply + * customizations without needing to read the entire request body into memory. + * + * @author Phillip Webb + * @since 5.2 + * @see HttpAccessor#getClientHttpRequestInitializers() + */ +@FunctionalInterface +public interface ClientHttpRequestInitializer { + + /** + * Initialize the given client HTTP request. + * @param request the request to configure + */ + void initialize(ClientHttpRequest request); + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java index 895fde9ef05..bd1fec8235b 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -18,13 +18,17 @@ package org.springframework.http.client.support; import java.io.IOException; import java.net.URI; +import java.util.ArrayList; +import java.util.List; import org.apache.commons.logging.Log; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpLogging; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestInitializer; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.util.Assert; @@ -49,6 +53,8 @@ public abstract class HttpAccessor { private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + private List clientHttpRequestInitializers = new ArrayList<>(); + /** * Set the request factory that this accessor uses for obtaining client request handles. @@ -74,6 +80,28 @@ public abstract class HttpAccessor { } + /** + * Set the request initializers the this accessor should use. + *

The initializers will get sorted according to their order + * before the {@link ClientHttpRequest} is initialized. + */ + public void setClientHttpRequestInitializers( + List clientHttpRequestInitializers) { + if (this.clientHttpRequestInitializers != clientHttpRequestInitializers) { + this.clientHttpRequestInitializers.clear(); + this.clientHttpRequestInitializers.addAll(clientHttpRequestInitializers); + AnnotationAwareOrderComparator.sort(this.clientHttpRequestInitializers); + } + } + + /** + * Return the request initializers that this accessor uses. + *

The returned {@link List} is active and may get appended to. + */ + public List getClientHttpRequestInitializers() { + return this.clientHttpRequestInitializers; + } + /** * Create a new {@link ClientHttpRequest} via this template's {@link ClientHttpRequestFactory}. * @param url the URL to connect to @@ -85,10 +113,16 @@ public abstract class HttpAccessor { */ protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { ClientHttpRequest request = getRequestFactory().createRequest(url, method); + initialize(request); if (logger.isDebugEnabled()) { logger.debug("HTTP " + method.name() + " " + url); } return request; } + private void initialize(ClientHttpRequest request) { + this.clientHttpRequestInitializers.forEach( + initializer -> initializer.initialize(request)); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 7648eab8b1a..fbe20803a06 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -40,6 +40,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestInitializer; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.converter.GenericHttpMessageConverter; @@ -679,6 +680,32 @@ public class RestTemplateTests { verify(response).close(); } + @Test + public void clientHttpRequestInitializerAndRequestInterceptorAreBothApplied() throws Exception { + ClientHttpRequestInitializer initializer = request -> + request.getHeaders().add("MyHeader", "MyInitializerValue"); + ClientHttpRequestInterceptor interceptor = (request, body, execution) -> { + request.getHeaders().add("MyHeader", "MyInterceptorValue"); + return execution.execute(request, body); + }; + template.setClientHttpRequestInitializers(Collections.singletonList(initializer)); + template.setInterceptors(Collections.singletonList(interceptor)); + + MediaType contentType = MediaType.TEXT_PLAIN; + given(converter.canWrite(String.class, contentType)).willReturn(true); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(POST, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + + HttpHeaders entityHeaders = new HttpHeaders(); + entityHeaders.setContentType(contentType); + HttpEntity entity = new HttpEntity<>("Hello World", entityHeaders); + template.exchange("https://example.com", POST, entity, Void.class); + assertThat(requestHeaders.get("MyHeader")).contains("MyInterceptorValue", "MyInitializerValue"); + + verify(response).close(); + } + private void mockSentRequest(HttpMethod method, String uri) throws Exception { mockSentRequest(method, uri, new HttpHeaders()); }