diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java new file mode 100644 index 00000000000..bc2ea689f81 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperations.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2017 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.reactive.function.client; + +import java.net.URI; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.util.function.Function; + +import org.jetbrains.annotations.NotNull; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; + + +/** + * Default implementation of {@link WebClientOperations}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +class DefaultWebClientOperations implements WebClientOperations { + + private final WebClient webClient; + + private final UriBuilderFactory uriBuilderFactory; + + + DefaultWebClientOperations(WebClient webClient, UriBuilderFactory factory) { + this.webClient = webClient; + this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory()); + } + + + private WebClient getWebClient() { + return this.webClient; + } + + private UriBuilderFactory getUriBuilderFactory() { + return this.uriBuilderFactory; + } + + + @Override + public UriSpec get() { + return method(HttpMethod.GET); + } + + @Override + public UriSpec head() { + return method(HttpMethod.HEAD); + } + + @Override + public UriSpec post() { + return method(HttpMethod.POST); + } + + @Override + public UriSpec put() { + return method(HttpMethod.PUT); + } + + @Override + public UriSpec patch() { + return method(HttpMethod.PATCH); + } + + @Override + public UriSpec delete() { + return method(HttpMethod.DELETE); + } + + @Override + public UriSpec options() { + return method(HttpMethod.OPTIONS); + } + + @NotNull + private UriSpec method(HttpMethod httpMethod) { + return new DefaultUriSpec(httpMethod); + } + + + @Override + public WebClientOperations filter(ExchangeFilterFunction filterFunction) { + WebClient filteredWebClient = this.webClient.filter(filterFunction); + return new DefaultWebClientOperations(filteredWebClient, this.uriBuilderFactory); + } + + + private class DefaultUriSpec implements UriSpec { + + private final HttpMethod httpMethod; + + + DefaultUriSpec(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + @Override + public HeaderSpec uri(URI uri) { + return new DefaultHeaderSpec(ClientRequest.method(this.httpMethod, uri)); + } + + @Override + public HeaderSpec uri(String uriTemplate, Object... uriVariables) { + return uri(getUriBuilderFactory().expand(uriTemplate)); + } + + @Override + public HeaderSpec uri(Function uriFunction) { + return uri(uriFunction.apply(getUriBuilderFactory())); + } + } + + private class DefaultHeaderSpec implements HeaderSpec { + + private ClientRequest.BodyBuilder requestBuilder; + + + DefaultHeaderSpec(ClientRequest.BodyBuilder requestBuilder) { + this.requestBuilder = requestBuilder; + } + + + @Override + public DefaultHeaderSpec header(String headerName, String... headerValues) { + this.requestBuilder.header(headerName, headerValues); + return this; + } + + @Override + public DefaultHeaderSpec headers(HttpHeaders headers) { + this.requestBuilder.headers(headers); + return this; + } + + @Override + public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { + this.requestBuilder.accept(acceptableMediaTypes); + return this; + } + + @Override + public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { + this.requestBuilder.acceptCharset(acceptableCharsets); + return this; + } + + @Override + public DefaultHeaderSpec contentType(MediaType contentType) { + this.requestBuilder.contentType(contentType); + return this; + } + + @Override + public DefaultHeaderSpec contentLength(long contentLength) { + this.requestBuilder.contentLength(contentLength); + return this; + } + + @Override + public DefaultHeaderSpec cookie(String name, String value) { + this.requestBuilder.cookie(name, value); + return this; + } + + @Override + public DefaultHeaderSpec cookies(MultiValueMap cookies) { + this.requestBuilder.cookies(cookies); + return this; + } + + @Override + public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { + this.requestBuilder.ifModifiedSince(ifModifiedSince); + return this; + } + + @Override + public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { + this.requestBuilder.ifNoneMatch(ifNoneMatches); + return this; + } + + @Override + public Mono exchange() { + ClientRequest request = this.requestBuilder.build(); + return getWebClient().exchange(request); + } + + @Override + public Mono exchange(BodyInserter inserter) { + ClientRequest request = this.requestBuilder.body(inserter); + return getWebClient().exchange(request); + } + + @Override + public > Mono exchange(S publisher, Class elementClass) { + ClientRequest request = this.requestBuilder.body(publisher, elementClass); + return getWebClient().exchange(request); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java new file mode 100644 index 00000000000..f8c7749837d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientOperationsBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2017 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.reactive.function.client; + +import org.springframework.util.Assert; +import org.springframework.web.util.UriBuilderFactory; + +/** + * Default implementation of {@link WebClientOperations.Builder}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +class DefaultWebClientOperationsBuilder implements WebClientOperations.Builder { + + private final WebClient webClient; + + private UriBuilderFactory uriBuilderFactory; + + + public DefaultWebClientOperationsBuilder(WebClient webClient) { + Assert.notNull(webClient, "WebClient is required"); + this.webClient = webClient; + } + + + @Override + public WebClientOperations.Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory) { + this.uriBuilderFactory = uriBuilderFactory; + return this; + } + + @Override + public WebClientOperations build() { + return new DefaultWebClientOperations(this.webClient, this.uriBuilderFactory); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java new file mode 100644 index 00000000000..7200f8fb8c3 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/client/WebClientOperations.java @@ -0,0 +1,295 @@ +/* + * Copyright 2002-2017 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.reactive.function.client; + +import java.net.URI; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.util.UriBuilderFactory; + +/** + * The main class for performing requests through a WebClient. + * + *
+ *
+ * // Create WebClient (application-wide)
+ *
+ * ClientHttpConnector connector = new ReactorClientHttpConnector();
+ * WebClient webClient = WebClient.create(connector);
+ *
+ * // Create WebClientOperations (per base URI)
+ *
+ * String baseUri = "http://abc.com";
+ * UriBuilderFactory factory = new DefaultUriBuilderFactory(baseUri);
+ * WebClientOperations operations = WebClientOperations.create(webClient, factory);
+ *
+ * // Perform requests...
+ *
+ * Mono result = operations.get()
+ *     .uri("/foo")
+ *     .exchange()
+ *     .then(response -> response.bodyToMono(String.class));
+ * 
+ * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public interface WebClientOperations { + + /** + * Prepare an HTTP GET request. + * @return a spec for specifying the target URL + */ + UriSpec get(); + + /** + * Prepare an HTTP HEAD request. + * @return a spec for specifying the target URL + */ + UriSpec head(); + + /** + * Prepare an HTTP POST request. + * @return a spec for specifying the target URL + */ + UriSpec post(); + + /** + * Prepare an HTTP PUT request. + * @return a spec for specifying the target URL + */ + UriSpec put(); + + /** + * Prepare an HTTP PATCH request. + * @return a spec for specifying the target URL + */ + UriSpec patch(); + + /** + * Prepare an HTTP DELETE request. + * @return a spec for specifying the target URL + */ + UriSpec delete(); + + /** + * Prepare an HTTP OPTIONS request. + * @return a spec for specifying the target URL + */ + UriSpec options(); + + + /** + * Filter the client with the given {@code ExchangeFilterFunction}. + * @param filterFunction the filter to apply to this client + * @return the filtered client + * @see ExchangeFilterFunction#apply(ExchangeFunction) + */ + WebClientOperations filter(ExchangeFilterFunction filterFunction); + + + // Static, factory methods + + /** + * Create {@link WebClientOperations} that wraps the given {@link WebClient}. + * @param webClient the underlying client to use + */ + static WebClientOperations create(WebClient webClient) { + return builder(webClient).build(); + } + + /** + * Create {@link WebClientOperations} with a builder for additional + * configuration options. + * @param webClient the underlying client to use + */ + static WebClientOperations.Builder builder(WebClient webClient) { + return new DefaultWebClientOperationsBuilder(webClient); + } + + + /** + * A mutable builder for a {@link WebClientOperations}. + */ + interface Builder { + + /** + * Configure a {@code UriBuilderFactory} for use with this client for + * example to define a common "base" URI. + * @param uriBuilderFactory the URI builder factory + */ + Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); + + /** + * Builder the {@link WebClient} instance. + */ + WebClientOperations build(); + + } + + + /** + * Contract for specifying the URI for a request. + */ + interface UriSpec { + + /** + * Specify the URI using an absolute, fully constructed {@link URI}. + */ + HeaderSpec uri(URI uri); + + /** + * Specify the URI for the request using a URI template and URI variables. + * If a {@link UriBuilderFactory} was configured for the client (e.g. + * with a base URI) it will be used to expand the URI template. + * @see Builder#uriBuilderFactory(UriBuilderFactory) + */ + HeaderSpec uri(String uri, Object... uriVariables); + + /** + * Build the URI for the request using the {@link UriBuilderFactory} + * configured for this client. + * @see Builder#uriBuilderFactory(UriBuilderFactory) + */ + HeaderSpec uri(Function uriFunction); + + } + + /** + * Contract for specifying request headers leading up to the exchange. + */ + interface HeaderSpec { + + /** + * Set the list of acceptable {@linkplain MediaType media types}, as + * specified by the {@code Accept} header. + * @param acceptableMediaTypes the acceptable media types + * @return this builder + */ + HeaderSpec accept(MediaType... acceptableMediaTypes); + + /** + * Set the list of acceptable {@linkplain Charset charsets}, as specified + * by the {@code Accept-Charset} header. + * @param acceptableCharsets the acceptable charsets + * @return this builder + */ + HeaderSpec acceptCharset(Charset... acceptableCharsets); + + /** + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + * @param contentLength the content length + * @return this builder + * @see HttpHeaders#setContentLength(long) + */ + HeaderSpec contentLength(long contentLength); + + /** + * Set the {@linkplain MediaType media type} of the body, as specified + * by the {@code Content-Type} header. + * @param contentType the content type + * @return this builder + * @see HttpHeaders#setContentType(MediaType) + */ + HeaderSpec contentType(MediaType contentType); + + /** + * Add a cookie with the given name and value. + * @param name the cookie name + * @param value the cookie value + * @return this builder + */ + HeaderSpec cookie(String name, String value); + + /** + * Copy the given cookies into the entity's cookies map. + * + * @param cookies the existing cookies to copy from + * @return this builder + */ + HeaderSpec cookies(MultiValueMap cookies); + + /** + * Set the value of the {@code If-Modified-Since} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + * @param ifModifiedSince the new value of the header + * @return this builder + */ + HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); + + /** + * Set the values of the {@code If-None-Match} header. + * @param ifNoneMatches the new value of the header + * @return this builder + */ + HeaderSpec ifNoneMatch(String... ifNoneMatches); + + /** + * Add the given, single header value under the given name. + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + */ + HeaderSpec header(String headerName, String... headerValues); + + /** + * Copy the given headers into the entity's headers map. + * @param headers the existing headers to copy from + * @return this builder + */ + HeaderSpec headers(HttpHeaders headers); + + /** + * Perform the request without a request body. + * @return a {@code Mono} with the response + */ + Mono exchange(); + + /** + * Set the body of the request to the given {@code BodyInserter} and + * perform the request. + * @param inserter the {@code BodyInserter} that writes to the request + * @param the type contained in the body + * @return a {@code Mono} with the response + */ + Mono exchange(BodyInserter inserter); + + /** + * Set the body of the request to the given {@code Publisher} and + * perform the request. + * @param publisher the {@code Publisher} to write to the request + * @param elementClass the class of elements contained in the publisher + * @param the type of the elements contained in the publisher + * @param the type of the {@code Publisher} + * @return a {@code Mono} with the response + */ + > Mono exchange(S publisher, Class elementClass); + + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 8cfc60dab99..9d8a2c0b924 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -38,37 +38,53 @@ import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.Pojo; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriBuilderFactory; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.springframework.web.reactive.function.BodyExtractors.toFlux; import static org.springframework.web.reactive.function.BodyExtractors.toMono; +import static org.springframework.web.reactive.function.BodyInserters.fromObject; /** - * {@link WebClient} integration tests with the {@code Flux} and {@code Mono} API. + * Integration tests using a {@link WebClient} through {@link WebClientOperations}. * * @author Brian Clozel + * @author Rossen Stoyanchev */ public class WebClientIntegrationTests { private MockWebServer server; - private WebClient webClient; + private WebClientOperations operations; + @Before public void setup() { this.server = new MockWebServer(); - this.webClient = WebClient.create(new ReactorClientHttpConnector()); + + WebClient webClient = WebClient.create(new ReactorClientHttpConnector()); + UriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(this.server.url("/").toString()); + + this.operations = WebClientOperations.builder(webClient) + .uriBuilderFactory(uriBuilderFactory) + .build(); + } + + @After + public void tearDown() throws Exception { + this.server.shutdown(); } + @Test public void headers() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); - Mono result = this.webClient - .exchange(request) + Mono result = this.operations.get() + .uri("/greeting?name=Spring") + .exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result) @@ -88,16 +104,13 @@ public class WebClientIntegrationTests { @Test public void plainText() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setBody("Hello Spring!")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/greeting?name=Spring") .header("X-Test-Header", "testvalue") - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(String.class))); + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("Hello Spring!") @@ -113,18 +126,15 @@ public class WebClientIntegrationTests { @Test public void jsonString() throws Exception { - HttpUrl baseUrl = server.url("/json"); String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") .setBody(content)); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/json") .accept(MediaType.APPLICATION_JSON) - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(String.class))); + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext(content) @@ -139,17 +149,14 @@ public class WebClientIntegrationTests { @Test public void jsonPojoMono() throws Exception { - HttpUrl baseUrl = server.url("/pojo"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") .setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/pojo") .accept(MediaType.APPLICATION_JSON) - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(Pojo.class))); + .exchange() + .then(response -> response.bodyToMono(Pojo.class)); StepVerifier.create(result) .consumeNextWith(p -> assertEquals("barbar", p.getBar())) @@ -172,9 +179,11 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON) .build(); - Flux result = this.webClient - .exchange(request) - .flatMap(response -> response.body(toFlux(Pojo.class))); + Flux result = this.operations.get() + .uri("/pojos") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .flatMap(response -> response.bodyToFlux(Pojo.class)); StepVerifier.create(result) .consumeNextWith(p -> assertThat(p.getBar(), Matchers.is("bar1"))) @@ -195,15 +204,12 @@ public class WebClientIntegrationTests { .setHeader("Content-Type", "application/json") .setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); - Pojo spring = new Pojo("foofoo", "barbar"); - ClientRequest request = ClientRequest.POST(baseUrl.toString()) + Mono result = this.operations.post() + .uri("/pojo/capitalize") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromObject(spring)); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(BodyExtractors.toMono(Pojo.class))); + .exchange(fromObject(new Pojo("foofoo", "barbar"))) + .then(response -> response.bodyToMono(Pojo.class)); StepVerifier.create(result) .consumeNextWith(p -> assertEquals("BARBAR", p.getBar())) @@ -221,17 +227,14 @@ public class WebClientIntegrationTests { @Test public void cookies() throws Exception { - HttpUrl baseUrl = server.url("/test"); this.server.enqueue(new MockResponse() .setHeader("Content-Type", "text/plain").setBody("test")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()) + Mono result = this.operations.get() + .uri("/test") .cookie("testkey", "testvalue") - .build(); - - Mono result = this.webClient - .exchange(request) - .then(response -> response.body(toMono(String.class))); + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("test") @@ -246,19 +249,13 @@ public class WebClientIntegrationTests { @Test public void notFound() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setResponseCode(404) .setHeader("Content-Type", "text/plain").setBody("Not Found")); - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); - - Mono result = this.webClient - .exchange(request); + Mono result = this.operations.get().uri("/greeting?name=Spring").exchange(); StepVerifier.create(result) - .consumeNextWith(response -> { - assertEquals(HttpStatus.NOT_FOUND, response.statusCode()); - }) + .consumeNextWith(response -> assertEquals(HttpStatus.NOT_FOUND, response.statusCode())) .expectComplete() .verify(Duration.ofSeconds(3)); @@ -270,21 +267,18 @@ public class WebClientIntegrationTests { @Test public void buildFilter() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - ExchangeFilterFunction filter = (request, next) -> { - ClientRequest filteredRequest = ClientRequest.from(request) - .header("foo", "bar").build(); - return next.exchange(filteredRequest); - }; - WebClient filteredClient = WebClient.builder(new ReactorClientHttpConnector()) - .filter(filter).build(); - - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); + WebClientOperations filteredClient = this.operations.filter( + (request, next) -> { + ClientRequest filteredRequest = ClientRequest.from(request).header("foo", "bar").build(); + return next.exchange(filteredRequest); + }); - Mono result = filteredClient.exchange(request) - .then(response -> response.body(toMono(String.class))); + Mono result = filteredClient.get() + .uri("/greeting?name=Spring") + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("Hello Spring!") @@ -299,21 +293,18 @@ public class WebClientIntegrationTests { @Test public void filter() throws Exception { - HttpUrl baseUrl = server.url("/greeting?name=Spring"); this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - ExchangeFilterFunction filter = (request, next) -> { - ClientRequest filteredRequest = ClientRequest.from(request) - .header("foo", "bar").build(); - return next.exchange(filteredRequest); - }; - WebClient client = WebClient.create(new ReactorClientHttpConnector()); - WebClient filteredClient = client.filter(filter); + WebClientOperations filteredClient = this.operations.filter( + (request, next) -> { + ClientRequest filteredRequest = ClientRequest.from(request).header("foo", "bar").build(); + return next.exchange(filteredRequest); + }); - ClientRequest request = ClientRequest.GET(baseUrl.toString()).build(); - - Mono result = filteredClient.exchange(request) - .then(response -> response.body(toMono(String.class))); + Mono result = filteredClient.get() + .uri("/greeting?name=Spring") + .exchange() + .then(response -> response.bodyToMono(String.class)); StepVerifier.create(result) .expectNext("Hello Spring!") @@ -326,8 +317,4 @@ public class WebClientIntegrationTests { } - @After - public void tearDown() throws Exception { - this.server.shutdown(); - } } \ No newline at end of file