diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java index 34e7590f1796..c6159edfc8e9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java @@ -22,21 +22,28 @@ import java.io.IOException; /** - * The execution context of asynchronous client http request. + * Represents the context of a client-side HTTP request execution. + * + *

Used to invoke the next interceptor in the interceptor chain, or - if the + * calling interceptor is last - execute the request itself. * * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 * @see AsyncClientHttpRequestInterceptor */ public interface AsyncClientHttpRequestExecution { /** - * Resumes the request execution by invoking next interceptor in the chain or executing the - * request to the remote service. + * Resume the request execution by invoking next interceptor in the chain + * or executing the request to the remote service. * * @param request the http request, containing the http method and headers * @param body the body of the request * @return the future * @throws IOException in case of I/O errors */ - ListenableFuture executeAsync(HttpRequest request, byte[] body) throws IOException; -} + ListenableFuture executeAsync(HttpRequest request, byte[] body) + throws IOException; + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java index 121f42b844b8..133ba6f4110e 100644 --- a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java @@ -23,23 +23,52 @@ import java.io.IOException; /** - * The asynchronous HTTP request interceptor. + * Intercepts client-side HTTP requests. Implementations of this interface can be + * {@linkplain org.springframework.web.client.AsyncRestTemplate#setInterceptors(java.util.List) + * registered} with the {@link org.springframework.web.client.AsyncRestTemplate + * AsyncRestTemplate} as to modify the outgoing {@link HttpRequest} and/or + * register to modify the incoming {@link ClientHttpResponse} with help of a + * {@link org.springframework.util.concurrent.ListenableFutureAdapter + * ListenableFutureAdapter}. + * + *

The main entry point for interceptors is {@link #intercept}. * * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 * @see org.springframework.web.client.AsyncRestTemplate * @see InterceptingAsyncHttpAccessor */ public interface AsyncClientHttpRequestInterceptor { /** - * Intercepts the outgoing client HTTP request. + * Intercept the given request, and return a response future. The given + * {@link AsyncClientHttpRequestExecution} allows the interceptor to pass on + * the request to the next entity in the chain. + * + *

An implementation might follow this pattern: + *

    + *
  1. Examine the {@linkplain HttpRequest request} and body
  2. + *
  3. Optionally {@linkplain org.springframework.http.client.support.HttpRequestWrapper + * wrap} the request to filter HTTP attributes.
  4. + *
  5. Optionally modify the body of the request.
  6. + *
  7. One of the following: + * + *
  8. Optionally adapt the response to filter HTTP attributes with the help of + * {@link org.springframework.util.concurrent.ListenableFutureAdapter + * ListenableFutureAdapter}.
  9. + *
* - * @param request the request - * @param body the request's body - * @param execution the request execution context - * @return the future + * @param request the request, containing method, URI, and headers + * @param body the body of the request + * @param execution the request execution + * @return the response future * @throws IOException in case of I/O errors */ - ListenableFuture interceptRequest( - HttpRequest request, byte[] body, AsyncClientHttpRequestExecution execution) throws IOException; + ListenableFuture intercept(HttpRequest request, byte[] body, + AsyncClientHttpRequestExecution execution) throws IOException; + } diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java index 6721bf0b72f8..0b13181a295a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java @@ -16,24 +16,23 @@ package org.springframework.http.client; +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; +import java.util.List; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.util.StreamUtils; import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureAdapter; - -import java.io.IOException; -import java.net.URI; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutionException; /** - * A {@link AsyncClientHttpRequest} wrapper that enriches it proceeds the actual request execution with calling - * the registered interceptors. + * An {@link AsyncClientHttpRequest} wrapper that enriches it proceeds the actual + * request execution with calling the registered interceptors. * * @author Jakub Narloch + * @author Rossen Stoyanchev * @see InterceptingAsyncClientHttpRequestFactory */ class InterceptingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { @@ -46,6 +45,7 @@ class InterceptingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHtt private HttpMethod httpMethod; + /** * Creates new instance of {@link InterceptingAsyncClientHttpRequest}. * @@ -55,8 +55,7 @@ class InterceptingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHtt * @param httpMethod the HTTP method */ public InterceptingAsyncClientHttpRequest(AsyncClientHttpRequestFactory requestFactory, - List interceptors, URI uri, - HttpMethod httpMethod) { + List interceptors, URI uri, HttpMethod httpMethod) { this.requestFactory = requestFactory; this.interceptors = interceptors; @@ -64,8 +63,11 @@ public InterceptingAsyncClientHttpRequest(AsyncClientHttpRequestFactory requestF this.httpMethod = httpMethod; } + @Override - protected ListenableFuture executeInternal(HttpHeaders headers, byte[] body) throws IOException { + protected ListenableFuture executeInternal(HttpHeaders headers, byte[] body) + throws IOException { + return new AsyncRequestExecution().executeAsync(this, body); } @@ -79,37 +81,37 @@ public URI getURI() { return uri; } + private class AsyncRequestExecution implements AsyncClientHttpRequestExecution { - private Iterator nextInterceptor = interceptors.iterator(); + private Iterator iterator; + + public AsyncRequestExecution() { + this.iterator = interceptors.iterator(); + } @Override - public ListenableFuture executeAsync(HttpRequest request, byte[] body) throws IOException { - if (nextInterceptor.hasNext()) { - AsyncClientHttpRequestInterceptor interceptor = nextInterceptor.next(); - ListenableFuture future = interceptor.interceptRequest(request, body, this); - return new IdentityListenableFutureAdapter(future); + public ListenableFuture executeAsync(HttpRequest request, byte[] body) + throws IOException { + + if (this.iterator.hasNext()) { + AsyncClientHttpRequestInterceptor interceptor = this.iterator.next(); + return interceptor.intercept(request, body, this); } else { - AsyncClientHttpRequest req = requestFactory.createAsyncRequest(uri, httpMethod); - req.getHeaders().putAll(getHeaders()); + URI theUri = request.getURI(); + HttpMethod theMethod = request.getMethod(); + HttpHeaders theHeaders = request.getHeaders(); + + AsyncClientHttpRequest delegate = requestFactory.createAsyncRequest(theUri, theMethod); + delegate.getHeaders().putAll(theHeaders); if (body.length > 0) { - StreamUtils.copy(body, req.getBody()); + StreamUtils.copy(body, delegate.getBody()); } - return req.executeAsync(); + + return delegate.executeAsync(); } } } - private static class IdentityListenableFutureAdapter extends ListenableFutureAdapter { - - protected IdentityListenableFutureAdapter(ListenableFuture adaptee) { - super(adaptee); - } - - @Override - protected T adapt(T adapteeResult) throws ExecutionException { - return adapteeResult; - } - } } diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java index cbf5923d1631..36ee48d78f1e 100644 --- a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java @@ -16,17 +16,18 @@ package org.springframework.http.client; -import org.springframework.http.HttpMethod; - -import java.io.IOException; import java.net.URI; import java.util.Collections; import java.util.List; +import org.springframework.http.HttpMethod; + /** - * The intercepting request factory. + * Wrapper for a {@link AsyncClientHttpRequestFactory} that has support for + * {@link AsyncClientHttpRequestInterceptor}s. * * @author Jakub Narloch + * @since 4.3 * @see InterceptingAsyncClientHttpRequest */ public class InterceptingAsyncClientHttpRequestFactory implements AsyncClientHttpRequestFactory { @@ -35,22 +36,25 @@ public class InterceptingAsyncClientHttpRequestFactory implements AsyncClientHtt private List interceptors; + /** - * Creates new instance of {@link InterceptingAsyncClientHttpRequestFactory} with delegated request factory and - * list of interceptors. + * Create new instance of {@link InterceptingAsyncClientHttpRequestFactory} + * with delegated request factory and list of interceptors. * - * @param delegate the delegated request factory - * @param interceptors the list of interceptors. + * @param delegate the request factory to delegate to + * @param interceptors the list of interceptors to use */ - public InterceptingAsyncClientHttpRequestFactory(AsyncClientHttpRequestFactory delegate, List interceptors) { + public InterceptingAsyncClientHttpRequestFactory(AsyncClientHttpRequestFactory delegate, + List interceptors) { this.delegate = delegate; - this.interceptors = interceptors != null ? interceptors : Collections.emptyList(); + this.interceptors = (interceptors != null ? interceptors : + Collections.emptyList()); } @Override - public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException { - - return new InterceptingAsyncClientHttpRequest(delegate, interceptors, uri, httpMethod); + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod method) { + return new InterceptingAsyncClientHttpRequest(this.delegate, this.interceptors, uri, method); } + } diff --git a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java index de71b321fe58..9474447d6e5e 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java @@ -19,30 +19,27 @@ import org.springframework.http.client.AsyncClientHttpRequestFactory; import org.springframework.http.client.AsyncClientHttpRequestInterceptor; import org.springframework.http.client.InterceptingAsyncClientHttpRequestFactory; +import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; /** - * The HTTP accessor that extends the base {@link AsyncHttpAccessor} with request intercepting functionality. + * The HTTP accessor that extends the base {@link AsyncHttpAccessor} with request + * intercepting functionality. * * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 */ public abstract class InterceptingAsyncHttpAccessor extends AsyncHttpAccessor { - private List interceptors = new ArrayList(); + private List interceptors = + new ArrayList(); - /** - * Retrieves the list of interceptors. - * - * @return the list of interceptors - */ - public List getInterceptors() { - return interceptors; - } /** - * Sets the list of interceptors. + * Sets the request interceptors that this accessor should use. * * @param interceptors the list of interceptors */ @@ -50,12 +47,22 @@ public void setInterceptors(List interceptors this.interceptors = interceptors; } + /** + * Return the request interceptor that this accessor uses. + */ + public List getInterceptors() { + return this.interceptors; + } + @Override public AsyncClientHttpRequestFactory getAsyncRequestFactory() { - AsyncClientHttpRequestFactory asyncRequestFactory = super.getAsyncRequestFactory(); - if(interceptors.isEmpty()) { - return asyncRequestFactory; + AsyncClientHttpRequestFactory delegate = super.getAsyncRequestFactory(); + if (!CollectionUtils.isEmpty(getInterceptors())) { + return new InterceptingAsyncClientHttpRequestFactory(delegate, getInterceptors()); + } + else { + return delegate; } - return new InterceptingAsyncClientHttpRequestFactory(asyncRequestFactory, getInterceptors()); } + } diff --git a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java index e534a60ce72d..14c288d76fa7 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java @@ -18,9 +18,10 @@ import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -43,12 +44,20 @@ import org.springframework.http.client.AsyncClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory; +import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureCallback; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author Arjen Poutsma @@ -610,8 +619,8 @@ public void multipart() throws Exception { @Test public void getAndInterceptResponse() throws Exception { RequestInterceptor interceptor = new RequestInterceptor(); - template.setInterceptors(Arrays.asList(interceptor)); - ListenableFuture> future = template.getForEntity(baseUrl + "/get", String.class); + template.setInterceptors(Collections.singletonList(interceptor)); + ListenableFuture> future = template.getForEntity("/get", String.class); ResponseEntity response = future.get(); assertNotNull(interceptor.response); @@ -623,28 +632,44 @@ public void getAndInterceptResponse() throws Exception { @Test public void getAndInterceptError() throws Exception { RequestInterceptor interceptor = new RequestInterceptor(); - template.setInterceptors(Arrays.asList(interceptor)); - ListenableFuture> future = template.getForEntity(baseUrl + "/status/notfound", String.class); + template.setInterceptors(Collections.singletonList(interceptor)); + ListenableFuture> future = template.getForEntity("/status/notfound", String.class); try { future.get(); fail("No exception thrown"); } catch (ExecutionException ex) { + // expected } assertNotNull(interceptor.response); assertEquals(HttpStatus.NOT_FOUND, interceptor.response.getStatusCode()); assertNull(interceptor.exception); } - public static class RequestInterceptor implements AsyncClientHttpRequestInterceptor { - private ClientHttpResponse response; + private static class RequestInterceptor implements AsyncClientHttpRequestInterceptor { - private Throwable exception; + private volatile ClientHttpResponse response; + + private volatile Throwable exception; @Override - public ListenableFuture interceptRequest(HttpRequest request, byte[] body, - AsyncClientHttpRequestExecution execution) throws IOException { + public ListenableFuture intercept(HttpRequest request, byte[] body, + AsyncClientHttpRequestExecution execution) throws IOException { + + request = new HttpRequestWrapper(request) { + + @Override + public URI getURI() { + try { + return new URI(baseUrl + super.getURI().toString()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + }; + ListenableFuture future = execution.executeAsync(request, body); future.addCallback(resp -> response = resp, ex -> exception = ex); return future;