From 4a0597d612d68d6e948c748da8d36621639d1b7f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 19 Jun 2017 11:40:10 +0200 Subject: [PATCH] Replace WebClient.filter with Builder.filter This commit replaces the WebClient.filter method with WebClient.Builder.filter. The reason for this change is that filters added via WebClient.filter would be applied in the opposite order of their declaration, due to the compositional nature of the method, combined with the immutable nature of the WebClient. WebClient.Builder.filter does keep the order of the filters, as registered. Furthermore, this commit introduces a WebClient.mutate() method, returning a WebClient.Builder. This method allow to add/remove filters and other defaults from a given WebClient. Issue: SPR-15657 Add WebClient.Builder.addFilter Add Consumer-based headers and cookies methods to builders. Add WebClient.mutate --- .../reactive/server/DefaultWebTestClient.java | 30 ++--- .../server/DefaultWebTestClientBuilder.java | 30 +++++ .../web/reactive/server/WebTestClient.java | 72 +++++++--- .../ExchangeMutatorWebFilterTests.java | 8 +- .../function/client/DefaultWebClient.java | 31 +++-- .../client/DefaultWebClientBuilder.java | 125 ++++++++++++++++-- .../reactive/function/client/WebClient.java | 67 ++++++++-- .../client/DefaultWebClientTests.java | 22 +++ .../client/WebClientIntegrationTests.java | 21 ++- 9 files changed, 325 insertions(+), 81 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 61b5beff4f..34f0909721 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -48,13 +48,14 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriBuilder; -import static java.nio.charset.StandardCharsets.*; -import static org.springframework.test.util.AssertionErrors.*; -import static org.springframework.web.reactive.function.BodyExtractors.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.springframework.web.reactive.function.BodyExtractors.toFlux; +import static org.springframework.web.reactive.function.BodyExtractors.toMono; /** * Default implementation of {@link WebTestClient}. @@ -80,12 +81,6 @@ class DefaultWebTestClient implements WebTestClient { this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5)); } - private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) { - this.webClient = webTestClient.webClient.filter(filter); - this.wiretapConnector = webTestClient.wiretapConnector; - this.timeout = webTestClient.timeout; - } - private Duration getTimeout() { return this.timeout; @@ -134,12 +129,6 @@ class DefaultWebTestClient implements WebTestClient { } - @Override - public WebTestClient filter(ExchangeFilterFunction filter) { - return new DefaultWebTestClient(this, filter); - } - - @SuppressWarnings("unchecked") private class DefaultUriSpec> implements UriSpec { @@ -193,8 +182,8 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public RequestBodySpec headers(HttpHeaders headers) { - this.bodySpec.headers(headers); + public RequestBodySpec headers(Consumer headersConsumer) { + this.bodySpec.headers(headersConsumer); return this; } @@ -229,8 +218,9 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public RequestBodySpec cookies(MultiValueMap cookies) { - this.bodySpec.cookies(cookies); + public RequestBodySpec cookies( + Consumer> cookiesConsumer) { + this.bodySpec.cookies(cookiesConsumer); return this; } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 7950ab7f13..edddbe8ba1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -17,10 +17,15 @@ package org.springframework.test.web.reactive.server; import java.time.Duration; +import java.util.List; +import java.util.function.Consumer; +import org.springframework.http.HttpHeaders; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriBuilderFactory; @@ -71,12 +76,37 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { return this; } + @Override + public WebTestClient.Builder defaultHeaders(Consumer headersConsumer) { + this.webClientBuilder.defaultHeaders(headersConsumer); + return this; + } + @Override public WebTestClient.Builder defaultCookie(String cookieName, String... cookieValues) { this.webClientBuilder.defaultCookie(cookieName, cookieValues); return this; } + @Override + public WebTestClient.Builder defaultCookies( + Consumer> cookiesConsumer) { + this.webClientBuilder.defaultCookies(cookiesConsumer); + return this; + } + + @Override + public WebTestClient.Builder filter(ExchangeFilterFunction filter) { + this.webClientBuilder.filter(filter); + return this; + } + + @Override + public WebTestClient.Builder filters(Consumer> filtersConsumer) { + this.webClientBuilder.filters(filtersConsumer); + return this; + } + @Override public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) { this.webClientBuilder.exchangeStrategies(strategies); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 7d02579f8a..043c8eae32 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -43,7 +43,6 @@ import org.springframework.web.reactive.config.ViewResolverRegistry; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.server.HandlerStrategies; @@ -128,15 +127,6 @@ public interface WebTestClient { 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) - */ - WebTestClient filter(ExchangeFilterFunction filterFunction); - - // Static, factory methods /** @@ -321,6 +311,17 @@ public interface WebTestClient { */ Builder defaultHeader(String headerName, String... headerValues); + /** + * Manipulate the default headers with the given consumer. The + * headers provided to the consumer are "live", so that the consumer can be used to + * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, + * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@link HttpHeaders} methods. + * @param headersConsumer a function that consumes the {@code HttpHeaders} + * @return this builder + */ + Builder defaultHeaders(Consumer headersConsumer); + /** * Add the given header to all requests that haven't added it. * @param cookieName the cookie name @@ -328,6 +329,32 @@ public interface WebTestClient { */ Builder defaultCookie(String cookieName, String... cookieValues); + /** + * Manipulate the default cookies with the given consumer. The + * map provided to the consumer is "live", so that the consumer can be used to + * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, + * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other + * {@link MultiValueMap} methods. + * @param cookiesConsumer a function that consumes the cookies map + * @return this builder + */ + Builder defaultCookies(Consumer> cookiesConsumer); + + /** + * Add the given filter to the filter chain. + * @param filter the filter to be added to the chain + */ + Builder filter(ExchangeFilterFunction filter); + + /** + * Manipulate the filters with the given consumer. The + * list provided to the consumer is "live", so that the consumer can be used to remove + * filters, change ordering, etc. + * @param filtersConsumer a function that consumes the filter list + * @return this builder + */ + Builder filters(Consumer> filtersConsumer); + /** * Configure the {@link ExchangeStrategies} to use. *

By default {@link ExchangeStrategies#withDefaults()} is used. @@ -417,12 +444,15 @@ public interface WebTestClient { S cookie(String name, String value); /** - * Copy the given cookies into the entity's cookies map. - * - * @param cookies the existing cookies to copy from - * @return the same instance + * Manipulate this request's cookies with the given consumer. The + * map provided to the consumer is "live", so that the consumer can be used to + * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, + * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other + * {@link MultiValueMap} methods. + * @param cookiesConsumer a function that consumes the cookies map + * @return this builder */ - S cookies(MultiValueMap cookies); + S cookies(Consumer> cookiesConsumer); /** * Set the value of the {@code If-Modified-Since} header. @@ -449,11 +479,15 @@ public interface WebTestClient { S header(String headerName, String... headerValues); /** - * Copy the given headers into the entity's headers map. - * @param headers the existing headers to copy from - * @return the same instance + * Manipulate the request's headers with the given consumer. The + * headers provided to the consumer are "live", so that the consumer can be used to + * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, + * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@link HttpHeaders} methods. + * @param headersConsumer a function that consumes the {@code HttpHeaders} + * @return this builder */ - S headers(HttpHeaders headers); + S headers(Consumer headersConsumer); /** * Perform the exchange without a request body. diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java index f2de0a72de..1aeb38cbf9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java @@ -59,8 +59,14 @@ public class ExchangeMutatorWebFilterTests { @Test public void perRequestMutators() throws Exception { - this.webTestClient + + this.webTestClient = WebTestClient.bindToController(new TestController()) + .webFilter(this.exchangeMutator) + .configureClient() .filter(this.exchangeMutator.perClient(userIdentity("Giovanni"))) + .build(); + + this.webTestClient .get().uri("/userIdentity") .exchange() .expectStatus().isOk() diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index d577c930f2..2b44659191 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -24,6 +24,7 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import org.reactivestreams.Publisher; @@ -66,16 +67,18 @@ class DefaultWebClient implements WebClient { private final MultiValueMap defaultCookies; + private final DefaultWebClientBuilder builder; + DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable UriBuilderFactory factory, - @Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap defaultCookies) { + @Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap defaultCookies, + DefaultWebClientBuilder builder) { this.exchangeFunction = exchangeFunction; this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory()); - this.defaultHeaders = (defaultHeaders != null ? - HttpHeaders.readOnlyHttpHeaders(defaultHeaders) : null); - this.defaultCookies = (defaultCookies != null ? - CollectionUtils.unmodifiableMultiValueMap(defaultCookies) : null); + this.defaultHeaders = defaultHeaders; + this.defaultCookies = defaultCookies; + this.builder = builder; } @@ -125,13 +128,10 @@ class DefaultWebClient implements WebClient { } @Override - public WebClient filter(ExchangeFilterFunction filterFunction) { - ExchangeFunction filteredExchangeFunction = this.exchangeFunction.filter(filterFunction); - return new DefaultWebClient(filteredExchangeFunction, - this.uriBuilderFactory, this.defaultHeaders, this.defaultCookies); + public Builder mutate() { + return this.builder; } - private class DefaultUriSpec> implements UriSpec { private final HttpMethod httpMethod; @@ -204,8 +204,9 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultRequestBodySpec headers(HttpHeaders headers) { - getHeaders().putAll(headers); + public DefaultRequestBodySpec headers(Consumer headersConsumer) { + Assert.notNull(headersConsumer, "'headersConsumer' must not be null"); + headersConsumer.accept(this.headers); return this; } @@ -240,8 +241,10 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultRequestBodySpec cookies(MultiValueMap cookies) { - getCookies().putAll(cookies); + public DefaultRequestBodySpec cookies( + Consumer> cookiesConsumer) { + Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null"); + cookiesConsumer.accept(this.cookies); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index 24e295aa84..5bef1ae576 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -16,13 +16,20 @@ package org.springframework.web.reactive.function.client; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.springframework.http.HttpHeaders; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.DefaultUriBuilderFactory; @@ -46,12 +53,38 @@ class DefaultWebClientBuilder implements WebClient.Builder { private MultiValueMap defaultCookies; + private List filters; + private ClientHttpConnector connector; private ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults(); private ExchangeFunction exchangeFunction; + public DefaultWebClientBuilder() { + } + + public DefaultWebClientBuilder(DefaultWebClientBuilder other) { + Assert.notNull(other, "'other' must not be null"); + + this.baseUrl = other.baseUrl; + this.defaultUriVariables = (other.defaultUriVariables != null ? + new LinkedHashMap<>(other.defaultUriVariables) : null); + this.uriBuilderFactory = other.uriBuilderFactory; + if (other.defaultHeaders != null) { + this.defaultHeaders = new HttpHeaders(); + this.defaultHeaders.putAll(other.defaultHeaders); + } + else { + this.defaultHeaders = null; + } + this.defaultCookies = (other.defaultCookies != null ? + new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); + this.connector = other.connector; + this.exchangeStrategies = other.exchangeStrategies; + this.exchangeFunction = other.exchangeFunction; + } @Override public WebClient.Builder baseUrl(String baseUrl) { @@ -73,22 +106,47 @@ class DefaultWebClientBuilder implements WebClient.Builder { @Override public WebClient.Builder defaultHeader(String headerName, String... headerValues) { - if (this.defaultHeaders == null) { - this.defaultHeaders = new HttpHeaders(); - } + initHeaders(); for (String headerValue : headerValues) { this.defaultHeaders.add(headerName, headerValue); } return this; } + @Override + public WebClient.Builder defaultHeaders(Consumer headersConsumer) { + Assert.notNull(headersConsumer, "'headersConsumer' must not be null"); + initHeaders(); + headersConsumer.accept(this.defaultHeaders); + return this; + } + + private void initHeaders() { + if (this.defaultHeaders == null) { + this.defaultHeaders = new HttpHeaders(); + } + } + @Override public WebClient.Builder defaultCookie(String cookieName, String... cookieValues) { + initCookies(); + this.defaultCookies.addAll(cookieName, Arrays.asList(cookieValues)); + return this; + } + + @Override + public WebClient.Builder defaultCookies( + Consumer> cookiesConsumer) { + Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null"); + initCookies(); + cookiesConsumer.accept(this.defaultCookies); + return this; + } + + private void initCookies() { if (this.defaultCookies == null) { this.defaultCookies = new LinkedMultiValueMap<>(4); } - this.defaultCookies.addAll(cookieName, Arrays.asList(cookieValues)); - return this; } @Override @@ -97,9 +155,31 @@ class DefaultWebClientBuilder implements WebClient.Builder { return this; } + @Override + public WebClient.Builder filter(ExchangeFilterFunction filter) { + Assert.notNull(filter, "'filter' must not be null"); + initFilters(); + this.filters.add(filter); + return this; + } + + @Override + public WebClient.Builder filters(Consumer> filtersConsumer) { + Assert.notNull(filtersConsumer, "'filtersConsumer' must not be null"); + initFilters(); + filtersConsumer.accept(this.filters); + return this; + } + + private void initFilters() { + if (this.filters == null) { + this.filters = new ArrayList<>(); + } + } + @Override public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { - Assert.notNull(strategies, "ExchangeStrategies is required."); + Assert.notNull(strategies, "'strategies' must not be null"); this.exchangeStrategies = strategies; return this; } @@ -112,8 +192,37 @@ class DefaultWebClientBuilder implements WebClient.Builder { @Override public WebClient build() { - return new DefaultWebClient(initExchangeFunction(), initUriBuilderFactory(), - this.defaultHeaders, this.defaultCookies); + ExchangeFunction exchange = initExchangeFunction(); + ExchangeFunction filteredExchange = (this.filters != null ? this.filters.stream() + .reduce(ExchangeFilterFunction::andThen) + .map(filter -> filter.apply(exchange)) + .orElse(exchange) : exchange); + return new DefaultWebClient(filteredExchange, initUriBuilderFactory(), + unmodifiableCopy(this.defaultHeaders), unmodifiableCopy(this.defaultCookies), + new DefaultWebClientBuilder(this)); + } + + private static @Nullable HttpHeaders unmodifiableCopy(@Nullable HttpHeaders original) { + if (original != null) { + HttpHeaders copy = new HttpHeaders(); + copy.putAll(original); + return HttpHeaders.readOnlyHttpHeaders(copy); + } else { + return null; + } + } + + private static @Nullable MultiValueMap unmodifiableCopy(@Nullable MultiValueMap original) { + if (original != null) { + return CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(original)); + } + else { + return null; + } + } + + private static List unmodifiableCopy(List list) { + return Collections.unmodifiableList(new ArrayList<>(list)); } private UriBuilderFactory initUriBuilderFactory() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index b8e1aae32f..cc05d5ca28 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -21,6 +21,7 @@ import java.nio.charset.Charset; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import org.reactivestreams.Publisher; @@ -108,12 +109,9 @@ public interface WebClient { /** - * 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) + * Return a builder to mutate properties of this web client. */ - WebClient filter(ExchangeFilterFunction filterFunction); + Builder mutate(); // Static, factory methods @@ -217,12 +215,23 @@ public interface WebClient { Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); /** - * Add the given header to all requests that haven't added it. + * Add the given header to all requests that have not added it. * @param headerName the header name * @param headerValues the header values */ Builder defaultHeader(String headerName, String... headerValues); + /** + * Manipulate the default headers with the given consumer. The + * headers provided to the consumer are "live", so that the consumer can be used to + * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, + * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@link HttpHeaders} methods. + * @param headersConsumer a function that consumes the {@code HttpHeaders} + * @return this builder + */ + Builder defaultHeaders(Consumer headersConsumer); + /** * Add the given header to all requests that haven't added it. * @param cookieName the cookie name @@ -230,6 +239,17 @@ public interface WebClient { */ Builder defaultCookie(String cookieName, String... cookieValues); + /** + * Manipulate the default cookies with the given consumer. The + * map provided to the consumer is "live", so that the consumer can be used to + * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, + * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other + * {@link MultiValueMap} methods. + * @param cookiesConsumer a function that consumes the cookies map + * @return this builder + */ + Builder defaultCookies(Consumer> cookiesConsumer); + /** * Configure the {@link ClientHttpConnector} to use. *

By default an instance of @@ -243,6 +263,21 @@ public interface WebClient { */ Builder clientConnector(ClientHttpConnector connector); + /** + * Add the given filter to the filter chain. + * @param filter the filter to be added to the chain + */ + Builder filter(ExchangeFilterFunction filter); + + /** + * Manipulate the filters with the given consumer. The + * list provided to the consumer is "live", so that the consumer can be used to remove + * filters, change ordering, etc. + * @param filtersConsumer a function that consumes the filter list + * @return this builder + */ + Builder filters(Consumer> filtersConsumer); + /** * Configure the {@link ExchangeStrategies} to use. *

By default {@link ExchangeStrategies#withDefaults()} is used. @@ -334,11 +369,15 @@ public interface WebClient { S cookie(String name, String value); /** - * Copy the given cookies into the entity's cookies map. - * @param cookies the existing cookies to copy from + * Manipulate the request's cookies with the given consumer. The + * map provided to the consumer is "live", so that the consumer can be used to + * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, + * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other + * {@link MultiValueMap} methods. + * @param cookiesConsumer a function that consumes the cookies map * @return this builder */ - S cookies(MultiValueMap cookies); + S cookies(Consumer> cookiesConsumer); /** * Set the value of the {@code If-Modified-Since} header. @@ -365,11 +404,15 @@ public interface WebClient { S header(String headerName, String... headerValues); /** - * Copy the given headers into the entity's headers map. - * @param headers the existing headers to copy from + * Manipulate the request's headers with the given consumer. The + * headers provided to the consumer are "live", so that the consumer can be used to + * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, + * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@link HttpHeaders} methods. + * @param headersConsumer a function that consumes the {@code HttpHeaders} * @return this builder */ - S headers(HttpHeaders headers); + S headers(Consumer headersConsumer); /** * Exchange the request for a {@code ClientResponse} with full access diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 4fdaabe30b..d9b9138ee1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -124,6 +124,28 @@ public class DefaultWebClientTests { client.post().uri("http://example.com").syncBody(mono); } + @Test + public void mutateDoesCopy() throws Exception { + + WebClient.Builder builder = WebClient.builder(); + builder.filter((request, next) -> next.exchange(request)); + builder.defaultHeader("foo", "bar"); + builder.defaultCookie("foo", "bar"); + WebClient client1 = builder.build(); + builder.filter((request, next) -> next.exchange(request)); + builder.defaultHeader("baz", "qux"); + builder.defaultCookie("baz", "qux"); + WebClient client2 = builder.build(); + + client1.mutate().filters(filters -> assertEquals(1, filters.size())); + client1.mutate().defaultHeaders(headers -> assertEquals(1, headers.size())); + client1.mutate().defaultCookies(cookies -> assertEquals(1, cookies.size())); + client2.mutate().filters(filters -> assertEquals(2, filters.size())); + client2.mutate().defaultHeaders(headers -> assertEquals(2, headers.size())); + client2.mutate().defaultCookies(cookies -> assertEquals(2, cookies.size())); + } + + private WebClient.Builder builder() { return WebClient.builder().baseUrl("/base").exchangeFunction(this.exchangeFunction); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 7a133ce59d..85346e7255 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -52,11 +52,12 @@ public class WebClientIntegrationTests { private WebClient webClient; + private String baseUrl; @Before public void setup() { this.server = new MockWebServer(); - String baseUrl = this.server.url("/").toString(); + baseUrl = this.server.url("/").toString(); this.webClient = WebClient.create(baseUrl); } @@ -394,13 +395,16 @@ public class WebClientIntegrationTests { @Test public void filter() throws Exception { - this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain") + .setBody("Hello Spring!")); - WebClient filteredClient = this.webClient.filter( - (request, next) -> { - ClientRequest filteredRequest = ClientRequest.from(request).header("foo", "bar").build(); + WebClient filteredClient = this.webClient.mutate() + .filter((request, next) -> { + ClientRequest filteredRequest = + ClientRequest.from(request).header("foo", "bar").build(); return next.exchange(filteredRequest); - }); + }) + .build(); Mono result = filteredClient.get() .uri("/greeting?name=Spring") @@ -429,7 +433,9 @@ public class WebClientIntegrationTests { } ); - WebClient filteredClient = this.webClient.filter(filter); + WebClient filteredClient = this.webClient.mutate() + .filter(filter) + .build(); // header not present this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); @@ -462,6 +468,7 @@ public class WebClientIntegrationTests { Assert.assertEquals(2, server.getRequestCount()); } + @SuppressWarnings("serial") private static class MyException extends RuntimeException {