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
This commit is contained in:
Arjen Poutsma 2017-06-19 11:40:10 +02:00
parent 52148a10b7
commit 4a0597d612
9 changed files with 325 additions and 81 deletions

View File

@ -48,13 +48,14 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientResponse; 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.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilder;
import static java.nio.charset.StandardCharsets.*; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.test.util.AssertionErrors.*; import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.web.reactive.function.BodyExtractors.*; 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}. * Default implementation of {@link WebTestClient}.
@ -80,12 +81,6 @@ class DefaultWebTestClient implements WebTestClient {
this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5)); 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() { private Duration getTimeout() {
return this.timeout; return this.timeout;
@ -134,12 +129,6 @@ class DefaultWebTestClient implements WebTestClient {
} }
@Override
public WebTestClient filter(ExchangeFilterFunction filter) {
return new DefaultWebTestClient(this, filter);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> { private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> {
@ -193,8 +182,8 @@ class DefaultWebTestClient implements WebTestClient {
} }
@Override @Override
public RequestBodySpec headers(HttpHeaders headers) { public RequestBodySpec headers(Consumer<HttpHeaders> headersConsumer) {
this.bodySpec.headers(headers); this.bodySpec.headers(headersConsumer);
return this; return this;
} }
@ -229,8 +218,9 @@ class DefaultWebTestClient implements WebTestClient {
} }
@Override @Override
public RequestBodySpec cookies(MultiValueMap<String, String> cookies) { public RequestBodySpec cookies(
this.bodySpec.cookies(cookies); Consumer<MultiValueMap<String, String>> cookiesConsumer) {
this.bodySpec.cookies(cookiesConsumer);
return this; return this;
} }

View File

@ -17,10 +17,15 @@
package org.springframework.test.web.reactive.server; package org.springframework.test.web.reactive.server;
import java.time.Duration; 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.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.server.reactive.HttpHandler; 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.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilderFactory; import org.springframework.web.util.UriBuilderFactory;
@ -71,12 +76,37 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
return this; return this;
} }
@Override
public WebTestClient.Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
this.webClientBuilder.defaultHeaders(headersConsumer);
return this;
}
@Override @Override
public WebTestClient.Builder defaultCookie(String cookieName, String... cookieValues) { public WebTestClient.Builder defaultCookie(String cookieName, String... cookieValues) {
this.webClientBuilder.defaultCookie(cookieName, cookieValues); this.webClientBuilder.defaultCookie(cookieName, cookieValues);
return this; return this;
} }
@Override
public WebTestClient.Builder defaultCookies(
Consumer<MultiValueMap<String, String>> 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<List<ExchangeFilterFunction>> filtersConsumer) {
this.webClientBuilder.filters(filtersConsumer);
return this;
}
@Override @Override
public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) { public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
this.webClientBuilder.exchangeStrategies(strategies); this.webClientBuilder.exchangeStrategies(strategies);

View File

@ -43,7 +43,6 @@ import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction; 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.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.HandlerStrategies;
@ -128,15 +127,6 @@ public interface WebTestClient {
UriSpec<RequestHeadersSpec<?>> options(); UriSpec<RequestHeadersSpec<?>> 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 // Static, factory methods
/** /**
@ -321,6 +311,17 @@ public interface WebTestClient {
*/ */
Builder defaultHeader(String headerName, String... headerValues); 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<HttpHeaders> headersConsumer);
/** /**
* Add the given header to all requests that haven't added it. * Add the given header to all requests that haven't added it.
* @param cookieName the cookie name * @param cookieName the cookie name
@ -328,6 +329,32 @@ public interface WebTestClient {
*/ */
Builder defaultCookie(String cookieName, String... cookieValues); 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<MultiValueMap<String, String>> 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<List<ExchangeFilterFunction>> filtersConsumer);
/** /**
* Configure the {@link ExchangeStrategies} to use. * Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used. * <p>By default {@link ExchangeStrategies#withDefaults()} is used.
@ -417,12 +444,15 @@ public interface WebTestClient {
S cookie(String name, String value); S cookie(String name, String value);
/** /**
* Copy the given cookies into the entity's cookies map. * 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
* @param cookies the existing cookies to copy from * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
* @return the same instance * {@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<String, String> cookies); S cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/** /**
* Set the value of the {@code If-Modified-Since} header. * Set the value of the {@code If-Modified-Since} header.
@ -449,11 +479,15 @@ public interface WebTestClient {
S header(String headerName, String... headerValues); S header(String headerName, String... headerValues);
/** /**
* Copy the given headers into the entity's headers map. * Manipulate the request's headers with the given consumer. The
* @param headers the existing headers to copy from * headers provided to the consumer are "live", so that the consumer can be used to
* @return the same instance * {@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<HttpHeaders> headersConsumer);
/** /**
* Perform the exchange without a request body. * Perform the exchange without a request body.

View File

@ -59,8 +59,14 @@ public class ExchangeMutatorWebFilterTests {
@Test @Test
public void perRequestMutators() throws Exception { public void perRequestMutators() throws Exception {
this.webTestClient
this.webTestClient = WebTestClient.bindToController(new TestController())
.webFilter(this.exchangeMutator)
.configureClient()
.filter(this.exchangeMutator.perClient(userIdentity("Giovanni"))) .filter(this.exchangeMutator.perClient(userIdentity("Giovanni")))
.build();
this.webTestClient
.get().uri("/userIdentity") .get().uri("/userIdentity")
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()

View File

@ -24,6 +24,7 @@ import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
@ -66,16 +67,18 @@ class DefaultWebClient implements WebClient {
private final MultiValueMap<String, String> defaultCookies; private final MultiValueMap<String, String> defaultCookies;
private final DefaultWebClientBuilder builder;
DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable UriBuilderFactory factory, DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable UriBuilderFactory factory,
@Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap<String, String> defaultCookies) { @Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap<String, String> defaultCookies,
DefaultWebClientBuilder builder) {
this.exchangeFunction = exchangeFunction; this.exchangeFunction = exchangeFunction;
this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory()); this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory());
this.defaultHeaders = (defaultHeaders != null ? this.defaultHeaders = defaultHeaders;
HttpHeaders.readOnlyHttpHeaders(defaultHeaders) : null); this.defaultCookies = defaultCookies;
this.defaultCookies = (defaultCookies != null ? this.builder = builder;
CollectionUtils.unmodifiableMultiValueMap(defaultCookies) : null);
} }
@ -125,13 +128,10 @@ class DefaultWebClient implements WebClient {
} }
@Override @Override
public WebClient filter(ExchangeFilterFunction filterFunction) { public Builder mutate() {
ExchangeFunction filteredExchangeFunction = this.exchangeFunction.filter(filterFunction); return this.builder;
return new DefaultWebClient(filteredExchangeFunction,
this.uriBuilderFactory, this.defaultHeaders, this.defaultCookies);
} }
private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> { private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> {
private final HttpMethod httpMethod; private final HttpMethod httpMethod;
@ -204,8 +204,9 @@ class DefaultWebClient implements WebClient {
} }
@Override @Override
public DefaultRequestBodySpec headers(HttpHeaders headers) { public DefaultRequestBodySpec headers(Consumer<HttpHeaders> headersConsumer) {
getHeaders().putAll(headers); Assert.notNull(headersConsumer, "'headersConsumer' must not be null");
headersConsumer.accept(this.headers);
return this; return this;
} }
@ -240,8 +241,10 @@ class DefaultWebClient implements WebClient {
} }
@Override @Override
public DefaultRequestBodySpec cookies(MultiValueMap<String, String> cookies) { public DefaultRequestBodySpec cookies(
getCookies().putAll(cookies); Consumer<MultiValueMap<String, String>> cookiesConsumer) {
Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null");
cookiesConsumer.accept(this.cookies);
return this; return this;
} }

View File

@ -16,13 +16,20 @@
package org.springframework.web.reactive.function.client; package org.springframework.web.reactive.function.client;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.DefaultUriBuilderFactory;
@ -46,12 +53,38 @@ class DefaultWebClientBuilder implements WebClient.Builder {
private MultiValueMap<String, String> defaultCookies; private MultiValueMap<String, String> defaultCookies;
private List<ExchangeFilterFunction> filters;
private ClientHttpConnector connector; private ClientHttpConnector connector;
private ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults(); private ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults();
private ExchangeFunction exchangeFunction; 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 @Override
public WebClient.Builder baseUrl(String baseUrl) { public WebClient.Builder baseUrl(String baseUrl) {
@ -73,22 +106,47 @@ class DefaultWebClientBuilder implements WebClient.Builder {
@Override @Override
public WebClient.Builder defaultHeader(String headerName, String... headerValues) { public WebClient.Builder defaultHeader(String headerName, String... headerValues) {
if (this.defaultHeaders == null) { initHeaders();
this.defaultHeaders = new HttpHeaders();
}
for (String headerValue : headerValues) { for (String headerValue : headerValues) {
this.defaultHeaders.add(headerName, headerValue); this.defaultHeaders.add(headerName, headerValue);
} }
return this; return this;
} }
@Override
public WebClient.Builder defaultHeaders(Consumer<HttpHeaders> 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 @Override
public WebClient.Builder defaultCookie(String cookieName, String... cookieValues) { public WebClient.Builder defaultCookie(String cookieName, String... cookieValues) {
initCookies();
this.defaultCookies.addAll(cookieName, Arrays.asList(cookieValues));
return this;
}
@Override
public WebClient.Builder defaultCookies(
Consumer<MultiValueMap<String, String>> cookiesConsumer) {
Assert.notNull(cookiesConsumer, "'cookiesConsumer' must not be null");
initCookies();
cookiesConsumer.accept(this.defaultCookies);
return this;
}
private void initCookies() {
if (this.defaultCookies == null) { if (this.defaultCookies == null) {
this.defaultCookies = new LinkedMultiValueMap<>(4); this.defaultCookies = new LinkedMultiValueMap<>(4);
} }
this.defaultCookies.addAll(cookieName, Arrays.asList(cookieValues));
return this;
} }
@Override @Override
@ -97,9 +155,31 @@ class DefaultWebClientBuilder implements WebClient.Builder {
return this; 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<List<ExchangeFilterFunction>> 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 @Override
public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
Assert.notNull(strategies, "ExchangeStrategies is required."); Assert.notNull(strategies, "'strategies' must not be null");
this.exchangeStrategies = strategies; this.exchangeStrategies = strategies;
return this; return this;
} }
@ -112,8 +192,37 @@ class DefaultWebClientBuilder implements WebClient.Builder {
@Override @Override
public WebClient build() { public WebClient build() {
return new DefaultWebClient(initExchangeFunction(), initUriBuilderFactory(), ExchangeFunction exchange = initExchangeFunction();
this.defaultHeaders, this.defaultCookies); 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 <K, V> MultiValueMap<K, V> unmodifiableCopy(@Nullable MultiValueMap<K, V> original) {
if (original != null) {
return CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(original));
}
else {
return null;
}
}
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
return Collections.unmodifiableList(new ArrayList<>(list));
} }
private UriBuilderFactory initUriBuilderFactory() { private UriBuilderFactory initUriBuilderFactory() {

View File

@ -21,6 +21,7 @@ import java.nio.charset.Charset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
@ -108,12 +109,9 @@ public interface WebClient {
/** /**
* Filter the client with the given {@code ExchangeFilterFunction}. * Return a builder to mutate properties of this web client.
* @param filterFunction the filter to apply to this client
* @return the filtered client
* @see ExchangeFilterFunction#apply(ExchangeFunction)
*/ */
WebClient filter(ExchangeFilterFunction filterFunction); Builder mutate();
// Static, factory methods // Static, factory methods
@ -217,12 +215,23 @@ public interface WebClient {
Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); 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 headerName the header name
* @param headerValues the header values * @param headerValues the header values
*/ */
Builder defaultHeader(String headerName, String... headerValues); 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<HttpHeaders> headersConsumer);
/** /**
* Add the given header to all requests that haven't added it. * Add the given header to all requests that haven't added it.
* @param cookieName the cookie name * @param cookieName the cookie name
@ -230,6 +239,17 @@ public interface WebClient {
*/ */
Builder defaultCookie(String cookieName, String... cookieValues); 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<MultiValueMap<String, String>> cookiesConsumer);
/** /**
* Configure the {@link ClientHttpConnector} to use. * Configure the {@link ClientHttpConnector} to use.
* <p>By default an instance of * <p>By default an instance of
@ -243,6 +263,21 @@ public interface WebClient {
*/ */
Builder clientConnector(ClientHttpConnector connector); 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<List<ExchangeFilterFunction>> filtersConsumer);
/** /**
* Configure the {@link ExchangeStrategies} to use. * Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used. * <p>By default {@link ExchangeStrategies#withDefaults()} is used.
@ -334,11 +369,15 @@ public interface WebClient {
S cookie(String name, String value); S cookie(String name, String value);
/** /**
* Copy the given cookies into the entity's cookies map. * Manipulate the request's cookies with the given consumer. The
* @param cookies the existing cookies to copy from * 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 * @return this builder
*/ */
S cookies(MultiValueMap<String, String> cookies); S cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/** /**
* Set the value of the {@code If-Modified-Since} header. * Set the value of the {@code If-Modified-Since} header.
@ -365,11 +404,15 @@ public interface WebClient {
S header(String headerName, String... headerValues); S header(String headerName, String... headerValues);
/** /**
* Copy the given headers into the entity's headers map. * Manipulate the request's headers with the given consumer. The
* @param headers the existing headers to copy from * 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 * @return this builder
*/ */
S headers(HttpHeaders headers); S headers(Consumer<HttpHeaders> headersConsumer);
/** /**
* Exchange the request for a {@code ClientResponse} with full access * Exchange the request for a {@code ClientResponse} with full access

View File

@ -124,6 +124,28 @@ public class DefaultWebClientTests {
client.post().uri("http://example.com").syncBody(mono); 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() { private WebClient.Builder builder() {
return WebClient.builder().baseUrl("/base").exchangeFunction(this.exchangeFunction); return WebClient.builder().baseUrl("/base").exchangeFunction(this.exchangeFunction);

View File

@ -52,11 +52,12 @@ public class WebClientIntegrationTests {
private WebClient webClient; private WebClient webClient;
private String baseUrl;
@Before @Before
public void setup() { public void setup() {
this.server = new MockWebServer(); this.server = new MockWebServer();
String baseUrl = this.server.url("/").toString(); baseUrl = this.server.url("/").toString();
this.webClient = WebClient.create(baseUrl); this.webClient = WebClient.create(baseUrl);
} }
@ -394,13 +395,16 @@ public class WebClientIntegrationTests {
@Test @Test
public void filter() throws Exception { 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( WebClient filteredClient = this.webClient.mutate()
(request, next) -> { .filter((request, next) -> {
ClientRequest filteredRequest = ClientRequest.from(request).header("foo", "bar").build(); ClientRequest filteredRequest =
ClientRequest.from(request).header("foo", "bar").build();
return next.exchange(filteredRequest); return next.exchange(filteredRequest);
}); })
.build();
Mono<String> result = filteredClient.get() Mono<String> result = filteredClient.get()
.uri("/greeting?name=Spring") .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 // header not present
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!"));
@ -462,6 +468,7 @@ public class WebClientIntegrationTests {
Assert.assertEquals(2, server.getRequestCount()); Assert.assertEquals(2, server.getRequestCount());
} }
@SuppressWarnings("serial") @SuppressWarnings("serial")
private static class MyException extends RuntimeException { private static class MyException extends RuntimeException {