Browse Source
This commit adds a new chain-based, interception contract to be used with `WebClient`. This is the HTTP client equivalent of the `WebFilter` contract already implemented in web reactive server. A `ClientHttpRequestInterceptor` implementation can transform the outgoing HTTP request (method, URI or headers) before delegating it to the next interceptor in the chain, or bypass the request processing altogether and return a (cached) HTTP response. Issue: SPR-14502pull/1178/head
4 changed files with 350 additions and 6 deletions
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright 2002-2016 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 |
||||
* |
||||
* http://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.web.client.reactive; |
||||
|
||||
import java.net.URI; |
||||
import java.util.function.Consumer; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.HttpMessage; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.client.reactive.ClientHttpResponse; |
||||
|
||||
/** |
||||
* Delegate to the next {@link ClientHttpRequestInterceptor} in the chain. |
||||
* |
||||
* @author Brian Clozel |
||||
* @since 5.0 |
||||
*/ |
||||
public interface ClientHttpRequestInterceptionChain { |
||||
|
||||
/** |
||||
* Delegate to the next {@link ClientHttpRequestInterceptor} in the chain. |
||||
* |
||||
* @param method the HTTP request method |
||||
* @param uri the HTTP request URI |
||||
* @param requestCallback a function that can customize the request |
||||
* by changing the HTTP request headers with {@code HttpMessage.getHeaders()}. |
||||
* @return a publisher of the resulting {@link ClientHttpResponse} |
||||
*/ |
||||
Mono<ClientHttpResponse> intercept(HttpMethod method, URI uri, Consumer<? super HttpMessage> requestCallback); |
||||
|
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
/* |
||||
* Copyright 2002-2016 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 |
||||
* |
||||
* http://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.web.client.reactive; |
||||
|
||||
import java.net.URI; |
||||
import java.util.List; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.client.reactive.ClientHttpResponse; |
||||
|
||||
/** |
||||
* Contract for chain-based, interception processing of client http requests |
||||
* that may be used to implement cross-cutting requirements such |
||||
* as security, timeouts, caching, and others. |
||||
* |
||||
* <p>Implementations of this interface can be |
||||
* {@link WebClient#setInterceptors(List) registered} with the {@link WebClient}. |
||||
* |
||||
* @author Brian Clozel |
||||
* @see org.springframework.web.client.reactive.WebClient |
||||
* @since 5.0 |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface ClientHttpRequestInterceptor { |
||||
|
||||
/** |
||||
* Intercept the client HTTP request |
||||
* |
||||
* <p>The provided {@link ClientHttpRequestInterceptionChain} |
||||
* instance allows the interceptor to delegate the request |
||||
* to the next interceptor in the chain. |
||||
* |
||||
* <p>An implementation might follow this pattern: |
||||
* <ol> |
||||
* <li>Examine the {@link HttpMethod method} and {@link URI uri}</li> |
||||
* <li>Optionally change those when delegating to the next interceptor |
||||
* with the {@code ClientHttpRequestInterceptionChain}.</li> |
||||
* <li>Optionally transform the HTTP message given as an |
||||
* argument of the request callback in |
||||
* {@code chain.intercept(method, uri, requestCallback)}.</li> |
||||
* <li>Optionally transform the response before returning it.</li> |
||||
* </ol> |
||||
* |
||||
* @param method the HTTP request method |
||||
* @param uri the HTTP request URI |
||||
* @param chain the request interception chain |
||||
* @return a publisher of the {@link ClientHttpResponse} |
||||
*/ |
||||
Mono<ClientHttpResponse> intercept(HttpMethod method, URI uri, ClientHttpRequestInterceptionChain chain); |
||||
} |
||||
@ -0,0 +1,165 @@
@@ -0,0 +1,165 @@
|
||||
/* |
||||
* Copyright 2002-2016 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 |
||||
* |
||||
* http://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.web.client.reactive; |
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat; |
||||
import static org.hamcrest.Matchers.*; |
||||
import static org.junit.Assert.*; |
||||
import static org.springframework.web.client.reactive.ClientWebRequestBuilders.*; |
||||
import static org.springframework.web.client.reactive.ResponseExtractors.*; |
||||
|
||||
import java.net.URI; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.function.Function; |
||||
|
||||
import org.junit.Assert; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.client.reactive.ClientHttpConnector; |
||||
import org.springframework.http.client.reactive.ClientHttpRequest; |
||||
import org.springframework.http.client.reactive.ClientHttpResponse; |
||||
import org.springframework.tests.TestSubscriber; |
||||
import org.springframework.web.client.reactive.test.MockClientHttpRequest; |
||||
import org.springframework.web.client.reactive.test.MockClientHttpResponse; |
||||
|
||||
/** |
||||
* @author Brian Clozel |
||||
*/ |
||||
public class ClientHttpRequestInterceptorTests { |
||||
|
||||
private MockClientHttpRequest mockRequest; |
||||
|
||||
private MockClientHttpResponse mockResponse; |
||||
|
||||
private MockClientHttpConnector mockClientHttpConnector; |
||||
|
||||
private WebClient webClient; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
this.mockClientHttpConnector = new MockClientHttpConnector(); |
||||
this.webClient = new WebClient(this.mockClientHttpConnector); |
||||
this.mockResponse = new MockClientHttpResponse(); |
||||
this.mockResponse.setStatus(HttpStatus.OK); |
||||
this.mockResponse.getHeaders().setContentType(MediaType.TEXT_PLAIN); |
||||
this.mockResponse.setBody("Spring Framework"); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldExecuteInterceptors() throws Exception { |
||||
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(); |
||||
interceptors.add(new NoOpInterceptor()); |
||||
interceptors.add(new NoOpInterceptor()); |
||||
interceptors.add(new NoOpInterceptor()); |
||||
this.webClient.setInterceptors(interceptors); |
||||
|
||||
Mono<String> result = this.webClient.perform(get("http://example.org/resource")) |
||||
.extract(body(String.class)); |
||||
|
||||
TestSubscriber.subscribe(result) |
||||
.assertNoError() |
||||
.assertValues("Spring Framework") |
||||
.assertComplete(); |
||||
interceptors.stream().forEach(interceptor -> { |
||||
Assert.assertTrue(((NoOpInterceptor) interceptor).invoked); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldChangeRequest() throws Exception { |
||||
ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() { |
||||
@Override |
||||
public Mono<ClientHttpResponse> intercept(HttpMethod method, URI uri, |
||||
ClientHttpRequestInterceptionChain interception) { |
||||
|
||||
return interception.intercept(HttpMethod.POST, URI.create("http://example.org/other"), |
||||
(request) -> { |
||||
request.getHeaders().set("X-Custom", "Spring Framework"); |
||||
}); |
||||
} |
||||
}; |
||||
this.webClient.setInterceptors(Collections.singletonList(interceptor)); |
||||
|
||||
Mono<String> result = this.webClient.perform(get("http://example.org/resource")) |
||||
.extract(body(String.class)); |
||||
|
||||
TestSubscriber.subscribe(result) |
||||
.assertNoError() |
||||
.assertValues("Spring Framework") |
||||
.assertComplete(); |
||||
|
||||
assertThat(this.mockRequest.getMethod(), is(HttpMethod.POST)); |
||||
assertThat(this.mockRequest.getURI().toString(), is("http://example.org/other")); |
||||
assertThat(this.mockRequest.getHeaders().getFirst("X-Custom"), is("Spring Framework")); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldShortCircuitConnector() throws Exception { |
||||
|
||||
MockClientHttpResponse otherResponse = new MockClientHttpResponse(); |
||||
otherResponse.setStatus(HttpStatus.OK); |
||||
otherResponse.setBody("Other content"); |
||||
|
||||
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(); |
||||
interceptors.add((method, uri, interception) -> Mono.just(otherResponse)); |
||||
interceptors.add(new NoOpInterceptor()); |
||||
this.webClient.setInterceptors(interceptors); |
||||
|
||||
Mono<String> result = this.webClient.perform(get("http://example.org/resource")) |
||||
.extract(body(String.class)); |
||||
|
||||
TestSubscriber.subscribe(result) |
||||
.assertNoError() |
||||
.assertValues("Other content") |
||||
.assertComplete(); |
||||
|
||||
assertFalse(((NoOpInterceptor) interceptors.get(1)).invoked); |
||||
} |
||||
|
||||
private class MockClientHttpConnector implements ClientHttpConnector { |
||||
|
||||
@Override |
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri, |
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) { |
||||
|
||||
mockRequest = new MockClientHttpRequest(method, uri); |
||||
return requestCallback.apply(mockRequest).then(Mono.just(mockResponse)); |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class NoOpInterceptor implements ClientHttpRequestInterceptor { |
||||
|
||||
public boolean invoked = false; |
||||
|
||||
@Override |
||||
public Mono<ClientHttpResponse> intercept(HttpMethod method, URI uri, |
||||
ClientHttpRequestInterceptionChain interception) { |
||||
|
||||
this.invoked = true; |
||||
return interception.intercept(method, uri, (request) -> { }); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue