Allow ExchangeStrategies customizations in WebClient

Prior to this commit, developers could configure their WebClient to use
their custom `ExchangeStrategies`, by providing it in the
`WebClient.Builder` chain.
Once created, an `ExchangeStrategies` instance is not mutable, which
makes it hard for further customizations by other components. In the
case of the reported issue, other components would override the default
configuration for the codecs maxInMemorySize.

This commit makes the `ExchangeStrategies` mutable and uses that fact to
further customize them with a new `WebClient.Builder#exchangeStrategies`
`Consumer` variant. This commit is also deprecating those mutating
variants in favor of a new `WebClient.Builder#exchangeStrategies` that
takes a `ExchangeStrategies#Builder` directly and avoids mutation issues
altogether.

Closes gh-23961
This commit is contained in:
Brian Clozel 2019-11-29 22:26:52 +01:00 committed by Rossen Stoyanchev
parent 17e2a0c7ea
commit d4209392d2
18 changed files with 344 additions and 43 deletions

View File

@ -169,18 +169,19 @@ public interface RSocketRequester {
RSocketRequester.Builder setupMetadata(Object value, @Nullable MimeType mimeType); RSocketRequester.Builder setupMetadata(Object value, @Nullable MimeType mimeType);
/** /**
* Provide {@link RSocketStrategies} to use. * Provide the {@link RSocketStrategies} to use.
* <p>By default this is based on default settings of * <p>This is useful for changing the default settings, yet still allowing
* {@link RSocketStrategies.Builder} but may be further customized via * further customizations via {@link #rsocketStrategies(Consumer)}.
* {@link #rsocketStrategies(Consumer)}. * If not set, defaults are obtained from {@link RSocketStrategies#builder()}.
* @param strategies the strategies to use
*/ */
RSocketRequester.Builder rsocketStrategies(@Nullable RSocketStrategies strategies); RSocketRequester.Builder rsocketStrategies(@Nullable RSocketStrategies strategies);
/** /**
* Customize the {@link RSocketStrategies}. * Customize the {@link RSocketStrategies}.
* <p>By default this starts out as {@link RSocketStrategies#builder()}. * <p>Allows further customization on {@link RSocketStrategies},
* However if strategies were {@link #rsocketStrategies(RSocketStrategies) set} * mutating them if they were {@link #rsocketStrategies(RSocketStrategies) set},
* explicitly, then they are {@link RSocketStrategies#mutate() mutated}. * or starting from {@link RSocketStrategies#builder()} defaults}.
*/ */
RSocketRequester.Builder rsocketStrategies(Consumer<RSocketStrategies.Builder> configurer); RSocketRequester.Builder rsocketStrategies(Consumer<RSocketStrategies.Builder> configurer);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -137,11 +137,24 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
} }
@Override @Override
@Deprecated
public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) { public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
this.webClientBuilder.exchangeStrategies(strategies); this.webClientBuilder.exchangeStrategies(strategies);
return this; return this;
} }
@Override
public WebTestClient.Builder exchangeStrategies(ExchangeStrategies.Builder strategies) {
this.webClientBuilder.exchangeStrategies(strategies);
return this;
}
@Override
public WebTestClient.Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer) {
this.webClientBuilder.exchangeStrategies(configurer);
return this;
}
@Override @Override
public WebTestClient.Builder responseTimeout(Duration timeout) { public WebTestClient.Builder responseTimeout(Duration timeout) {
this.responseTimeout = timeout; this.responseTimeout = timeout;

View File

@ -84,6 +84,7 @@ import org.springframework.web.util.UriBuilderFactory;
* perform integration tests on an embedded WebFlux server. * perform integration tests on an embedded WebFlux server.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0 * @since 5.0
* @see StatusAssertions * @see StatusAssertions
* @see HeaderAssertions * @see HeaderAssertions
@ -443,11 +444,34 @@ public interface WebTestClient {
/** /**
* Configure the {@link ExchangeStrategies} to use. * Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used. * <p>This is useful for changing the default settings, yet still allowing
* further customizations via {@link #exchangeStrategies(Consumer)}.
* By default {@link ExchangeStrategies#withDefaults()} is used.
* @param strategies the strategies to use * @param strategies the strategies to use
* @deprecated as of 5.1 in favor of {@link #exchangeStrategies(ExchangeStrategies.Builder)}
*/ */
@Deprecated
Builder exchangeStrategies(ExchangeStrategies strategies); Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Configure the {@link ExchangeStrategies.Builder} to use.
* <p>This is useful for changing the default settings, yet still allowing
* further customizations via {@link #exchangeStrategies(Consumer)}.
* By default {@link ExchangeStrategies#builder()} is used.
* @param strategies the strategies to use
* @since 5.1.12
*/
Builder exchangeStrategies(ExchangeStrategies.Builder strategies);
/**
* Customize the {@link ExchangeStrategies}.
* <p>Allows further customization on {@link ExchangeStrategies},
* mutating them if they were {@link #exchangeStrategies(ExchangeStrategies) set},
* or starting from {@link ExchangeStrategies#withDefaults() defaults}.
* @since 5.1.12
*/
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
/** /**
* Max amount of time to wait for responses. * Max amount of time to wait for responses.
* <p>By default 5 seconds. * <p>By default 5 seconds.
@ -928,7 +952,7 @@ public interface WebTestClient {
* @since 5.1 * @since 5.1
* @see #xpath(String, Map, Object...) * @see #xpath(String, Map, Object...)
*/ */
default XpathAssertions xpath(String expression, Object... args){ default XpathAssertions xpath(String expression, Object... args) {
return xpath(expression, null, args); return xpath(expression, null, args);
} }
@ -942,7 +966,7 @@ public interface WebTestClient {
* @param args arguments to parameterize the expression * @param args arguments to parameterize the expression
* @since 5.1 * @since 5.1
*/ */
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args); XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
/** /**
* Assert the response body content with the given {@link Consumer}. * Assert the response body content with the given {@link Consumer}.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -63,6 +63,11 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
@Override @Override
ClientDefaultCodecs defaultCodecs(); ClientDefaultCodecs defaultCodecs();
/**
* Clone this {@link ClientCodecConfigurer}.
*/
@Override
ClientCodecConfigurer clone();
/** /**
* Static factory method for a {@code ClientCodecConfigurer}. * Static factory method for a {@code ClientCodecConfigurer}.

View File

@ -87,6 +87,12 @@ public interface CodecConfigurer {
*/ */
List<HttpMessageWriter<?>> getWriters(); List<HttpMessageWriter<?>> getWriters();
/**
* Clone this {@link CodecConfigurer}.
* @since 5.1.12
*/
CodecConfigurer clone();
/** /**
* Customize or replace the HTTP message readers and writers registered by * Customize or replace the HTTP message readers and writers registered by

View File

@ -34,13 +34,14 @@ import org.springframework.util.Assert;
* client and server specific variants. * client and server specific variants.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0 * @since 5.0
*/ */
class BaseCodecConfigurer implements CodecConfigurer { class BaseCodecConfigurer implements CodecConfigurer {
private final BaseDefaultCodecs defaultCodecs; protected final BaseDefaultCodecs defaultCodecs;
private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs(); protected final DefaultCustomCodecs customCodecs;
/** /**
@ -50,6 +51,16 @@ class BaseCodecConfigurer implements CodecConfigurer {
BaseCodecConfigurer(BaseDefaultCodecs defaultCodecs) { BaseCodecConfigurer(BaseDefaultCodecs defaultCodecs) {
Assert.notNull(defaultCodecs, "'defaultCodecs' is required"); Assert.notNull(defaultCodecs, "'defaultCodecs' is required");
this.defaultCodecs = defaultCodecs; this.defaultCodecs = defaultCodecs;
this.customCodecs = new DefaultCustomCodecs();
}
/**
* Constructor with another {@link BaseCodecConfigurer} to copy
* the configuration from.
*/
BaseCodecConfigurer(BaseCodecConfigurer other) {
this.defaultCodecs = other.cloneDefaultCodecs();
this.customCodecs = new DefaultCustomCodecs(other.customCodecs);
} }
@ -87,6 +98,17 @@ class BaseCodecConfigurer implements CodecConfigurer {
return getWritersInternal(false); return getWritersInternal(false);
} }
@Override
public CodecConfigurer clone() {
return new BaseCodecConfigurer(this);
}
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new BaseDefaultCodecs(this.defaultCodecs);
}
/** /**
* Internal method that returns the configured writers. * Internal method that returns the configured writers.
* @param forMultipart whether to returns writers for general use ("false"), * @param forMultipart whether to returns writers for general use ("false"),
@ -110,7 +132,7 @@ class BaseCodecConfigurer implements CodecConfigurer {
/** /**
* Default implementation of {@code CustomCodecs}. * Default implementation of {@code CustomCodecs}.
*/ */
private static final class DefaultCustomCodecs implements CustomCodecs { protected static final class DefaultCustomCodecs implements CustomCodecs {
private final List<HttpMessageReader<?>> typedReaders = new ArrayList<>(); private final List<HttpMessageReader<?>> typedReaders = new ArrayList<>();
@ -121,6 +143,16 @@ class BaseCodecConfigurer implements CodecConfigurer {
private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>(); private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>();
DefaultCustomCodecs() {
}
DefaultCustomCodecs(DefaultCustomCodecs other) {
other.typedReaders.addAll(this.typedReaders);
other.typedWriters.addAll(this.typedWriters);
other.objectReaders.addAll(this.objectReaders);
other.objectWriters.addAll(this.objectWriters);
}
@Override @Override
public void decoder(Decoder<?> decoder) { public void decoder(Decoder<?> decoder) {
reader(new DecoderHttpMessageReader<>(decoder)); reader(new DecoderHttpMessageReader<>(decoder));
@ -143,7 +175,6 @@ class BaseCodecConfigurer implements CodecConfigurer {
(canWriteObject ? this.objectWriters : this.typedWriters).add(writer); (canWriteObject ? this.objectWriters : this.typedWriters).add(writer);
} }
// Package private accessors... // Package private accessors...
List<HttpMessageReader<?>> getTypedReaders() { List<HttpMessageReader<?>> getTypedReaders() {

View File

@ -106,6 +106,21 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
private boolean registerDefaults = true; private boolean registerDefaults = true;
BaseDefaultCodecs() {
}
protected BaseDefaultCodecs(BaseDefaultCodecs other) {
this.jackson2JsonDecoder = other.jackson2JsonDecoder;
this.jackson2JsonEncoder = other.jackson2JsonEncoder;
this.protobufDecoder = other.protobufDecoder;
this.protobufEncoder = other.protobufEncoder;
this.jaxb2Decoder = other.jaxb2Decoder;
this.jaxb2Encoder = other.jaxb2Encoder;
this.maxInMemorySize = other.maxInMemorySize;
this.enableLoggingRequestDetails = other.enableLoggingRequestDetails;
this.registerDefaults = other.registerDefaults;
}
@Override @Override
public void jackson2JsonDecoder(Decoder<?> decoder) { public void jackson2JsonDecoder(Decoder<?> decoder) {
this.jackson2JsonDecoder = decoder; this.jackson2JsonDecoder = decoder;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -49,6 +49,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private Supplier<List<HttpMessageWriter<?>>> partWritersSupplier; private Supplier<List<HttpMessageWriter<?>>> partWritersSupplier;
ClientDefaultCodecsImpl() {
}
ClientDefaultCodecsImpl(ClientDefaultCodecsImpl other) {
super(other);
this.multipartCodecs = new DefaultMultipartCodecs(other.multipartCodecs);
this.sseDecoder = other.sseDecoder;
this.partWritersSupplier = other.partWritersSupplier;
}
/** /**
* Set a supplier for part writers to use when * Set a supplier for part writers to use when
* {@link #multipartCodecs()} are not explicitly configured. * {@link #multipartCodecs()} are not explicitly configured.
@ -73,6 +84,14 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
this.sseDecoder = decoder; this.sseDecoder = decoder;
} }
@Override
public ClientDefaultCodecsImpl clone() {
ClientDefaultCodecsImpl codecs = new ClientDefaultCodecsImpl();
codecs.multipartCodecs = this.multipartCodecs;
codecs.sseDecoder = this.sseDecoder;
codecs.partWritersSupplier = this.partWritersSupplier;
return codecs;
}
@Override @Override
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) { protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
@ -116,6 +135,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private final List<HttpMessageWriter<?>> writers = new ArrayList<>(); private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
DefaultMultipartCodecs() {
}
DefaultMultipartCodecs(@Nullable DefaultMultipartCodecs other) {
if (other != null) {
this.writers.addAll(other.writers);
}
}
@Override @Override
public ClientCodecConfigurer.MultipartCodecs encoder(Encoder<?> encoder) { public ClientCodecConfigurer.MultipartCodecs encoder(Encoder<?> encoder) {
writer(new EncoderHttpMessageWriter<>(encoder)); writer(new EncoderHttpMessageWriter<>(encoder));

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,14 +26,30 @@ import org.springframework.http.codec.ClientCodecConfigurer;
*/ */
public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements ClientCodecConfigurer { public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements ClientCodecConfigurer {
public DefaultClientCodecConfigurer() { public DefaultClientCodecConfigurer() {
super(new ClientDefaultCodecsImpl()); super(new ClientDefaultCodecsImpl());
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true)); ((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true));
} }
private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) {
super(other);
}
@Override @Override
public ClientDefaultCodecs defaultCodecs() { public ClientDefaultCodecs defaultCodecs() {
return (ClientDefaultCodecs) super.defaultCodecs(); return (ClientDefaultCodecs) super.defaultCodecs();
} }
@Override
public DefaultClientCodecConfigurer clone() {
return new DefaultClientCodecConfigurer(this);
}
@Override
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new ClientDefaultCodecsImpl((ClientDefaultCodecsImpl) defaultCodecs());
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,13 +26,28 @@ import org.springframework.http.codec.ServerCodecConfigurer;
*/ */
public class DefaultServerCodecConfigurer extends BaseCodecConfigurer implements ServerCodecConfigurer { public class DefaultServerCodecConfigurer extends BaseCodecConfigurer implements ServerCodecConfigurer {
public DefaultServerCodecConfigurer() { public DefaultServerCodecConfigurer() {
super(new ServerDefaultCodecsImpl()); super(new ServerDefaultCodecsImpl());
} }
private DefaultServerCodecConfigurer(BaseCodecConfigurer other) {
super(other);
}
@Override @Override
public ServerDefaultCodecs defaultCodecs() { public ServerDefaultCodecs defaultCodecs() {
return (ServerDefaultCodecs) super.defaultCodecs(); return (ServerDefaultCodecs) super.defaultCodecs();
} }
@Override
public DefaultServerCodecConfigurer clone() {
return new DefaultServerCodecConfigurer(this);
}
@Override
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new ServerDefaultCodecsImpl((ServerDefaultCodecsImpl) defaultCodecs());
}
} }

View File

@ -46,6 +46,16 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
private Encoder<?> sseEncoder; private Encoder<?> sseEncoder;
ServerDefaultCodecsImpl() {
}
ServerDefaultCodecsImpl(ServerDefaultCodecsImpl other) {
super(other);
this.multipartReader = other.multipartReader;
this.sseEncoder = other.sseEncoder;
}
@Override @Override
public void multipartReader(HttpMessageReader<?> reader) { public void multipartReader(HttpMessageReader<?> reader) {
this.multipartReader = reader; this.multipartReader = reader;

View File

@ -268,6 +268,14 @@ public class CodecConfigurerTests {
assertEncoderInstance(jaxb2Encoder); assertEncoderInstance(jaxb2Encoder);
} }
@Test
public void cloneConfigurer() {
CodecConfigurer clone = this.configurer.clone();
this.configurer.registerDefaults(false);
assertThat(this.configurer.getReaders().size()).isEqualTo(0);
assertThat(clone.getReaders().size()).isEqualTo(11);
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) { private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement()); HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());
assertThat(reader.getClass()).isEqualTo(DecoderHttpMessageReader.class); assertThat(reader.getClass()).isEqualTo(DecoderHttpMessageReader.class);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -43,13 +43,18 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build
} }
private final ClientCodecConfigurer codecConfigurer = ClientCodecConfigurer.create(); private final ClientCodecConfigurer codecConfigurer;
public DefaultExchangeStrategiesBuilder() { public DefaultExchangeStrategiesBuilder() {
this.codecConfigurer = ClientCodecConfigurer.create();
this.codecConfigurer.registerDefaults(false); this.codecConfigurer.registerDefaults(false);
} }
private DefaultExchangeStrategiesBuilder(DefaultExchangeStrategies other) {
this.codecConfigurer = other.codecConfigurer.clone();
}
public void defaultConfiguration() { public void defaultConfiguration() {
this.codecConfigurer.registerDefaults(true); this.codecConfigurer.registerDefaults(true);
@ -69,19 +74,29 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build
private static class DefaultExchangeStrategies implements ExchangeStrategies { private static class DefaultExchangeStrategies implements ExchangeStrategies {
private final ClientCodecConfigurer codecConfigurer;
private final List<HttpMessageReader<?>> readers; private final List<HttpMessageReader<?>> readers;
private final List<HttpMessageWriter<?>> writers; private final List<HttpMessageWriter<?>> writers;
public DefaultExchangeStrategies(ClientCodecConfigurer codecConfigurer) { public DefaultExchangeStrategies(ClientCodecConfigurer codecConfigurer) {
this.readers = unmodifiableCopy(codecConfigurer.getReaders()); this.codecConfigurer = codecConfigurer;
this.writers = unmodifiableCopy(codecConfigurer.getWriters()); this.readers = unmodifiableCopy(this.codecConfigurer.getReaders());
this.writers = unmodifiableCopy(this.codecConfigurer.getWriters());
} }
private static <T> List<T> unmodifiableCopy(List<? extends T> list) { private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
return Collections.unmodifiableList(new ArrayList<>(list)); return Collections.unmodifiableList(new ArrayList<>(list));
} }
@Override
@Deprecated
public Builder mutate() {
return new DefaultExchangeStrategiesBuilder(this);
}
@Override @Override
public List<HttpMessageReader<?>> messageReaders() { public List<HttpMessageReader<?>> messageReaders() {
return this.readers; return this.readers;

View File

@ -40,6 +40,7 @@ import org.springframework.web.util.UriBuilderFactory;
* Default implementation of {@link WebClient.Builder}. * Default implementation of {@link WebClient.Builder}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0 * @since 5.0
*/ */
final class DefaultWebClientBuilder implements WebClient.Builder { final class DefaultWebClientBuilder implements WebClient.Builder {
@ -79,14 +80,16 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
@Nullable @Nullable
private ClientHttpConnector connector; private ClientHttpConnector connector;
private ExchangeStrategies exchangeStrategies; @Nullable
private ExchangeStrategies.Builder strategies;
private List<Consumer<ExchangeStrategies.Builder>> strategiesConfigurers;
@Nullable @Nullable
private ExchangeFunction exchangeFunction; private ExchangeFunction exchangeFunction;
public DefaultWebClientBuilder() { public DefaultWebClientBuilder() {
this.exchangeStrategies = ExchangeStrategies.withDefaults();
} }
public DefaultWebClientBuilder(DefaultWebClientBuilder other) { public DefaultWebClientBuilder(DefaultWebClientBuilder other) {
@ -108,7 +111,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
this.defaultRequest = other.defaultRequest; this.defaultRequest = other.defaultRequest;
this.filters = other.filters != null ? new ArrayList<>(other.filters) : null; this.filters = other.filters != null ? new ArrayList<>(other.filters) : null;
this.connector = other.connector; this.connector = other.connector;
this.exchangeStrategies = other.exchangeStrategies; this.strategies = other.strategies;
this.exchangeFunction = other.exchangeFunction; this.exchangeFunction = other.exchangeFunction;
} }
@ -203,9 +206,23 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
} }
@Override @Override
@Deprecated
public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
Assert.notNull(strategies, "ExchangeStrategies must not be null"); Assert.notNull(strategies, "ExchangeStrategies must not be null");
this.exchangeStrategies = strategies; this.strategies = strategies.mutate();
return this;
}
@Override
public WebClient.Builder exchangeStrategies(ExchangeStrategies.Builder strategies) {
Assert.notNull(strategies, "ExchangeStrategies must not be null");
this.strategies = strategies;
return this;
}
@Override
public WebClient.Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer) {
this.strategiesConfigurers.add(configurer);
return this; return this;
} }
@ -229,7 +246,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
@Override @Override
public WebClient build() { public WebClient build() {
ExchangeFunction exchange = (this.exchangeFunction == null ? ExchangeFunction exchange = (this.exchangeFunction == null ?
ExchangeFunctions.create(getOrInitConnector(), this.exchangeStrategies) : ExchangeFunctions.create(getOrInitConnector(), initExchangeStrategies()) :
this.exchangeFunction); this.exchangeFunction);
ExchangeFunction filteredExchange = (this.filters != null ? this.filters.stream() ExchangeFunction filteredExchange = (this.filters != null ? this.filters.stream()
.reduce(ExchangeFilterFunction::andThen) .reduce(ExchangeFilterFunction::andThen)
@ -254,6 +271,19 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
throw new IllegalStateException("No suitable default ClientHttpConnector found"); throw new IllegalStateException("No suitable default ClientHttpConnector found");
} }
@SuppressWarnings("deprecation")
private ExchangeStrategies initExchangeStrategies() {
if (CollectionUtils.isEmpty(this.strategiesConfigurers)) {
return this.strategies != null ? this.strategies.build() : ExchangeStrategies.withDefaults();
}
ExchangeStrategies.Builder builder =
this.strategies != null ? this.strategies : ExchangeStrategies.builder();
this.strategiesConfigurers.forEach(configurer -> configurer.accept(builder));
return builder.build();
}
private UriBuilderFactory initUriBuilderFactory() { private UriBuilderFactory initUriBuilderFactory() {
if (this.uriBuilderFactory != null) { if (this.uriBuilderFactory != null) {
return this.uriBuilderFactory; return this.uriBuilderFactory;

View File

@ -47,6 +47,18 @@ public interface ExchangeStrategies {
*/ */
List<HttpMessageWriter<?>> messageWriters(); List<HttpMessageWriter<?>> messageWriters();
/**
* Return a builder to create a new {@link ExchangeStrategies} instance
* replicated from the current instance.
* @since 5.1.12
* @deprecated APIs should consume {@link ExchangeStrategies} as final or accept an
* {@link ExchangeStrategies.Builder builder}.
*/
@Deprecated
default Builder mutate() {
throw new UnsupportedOperationException("This ExchangeStrategies implementation does not support mutation.");
}
// Static builder methods // Static builder methods

View File

@ -66,6 +66,7 @@ import org.springframework.web.util.UriBuilderFactory;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Sebastien Deleuze * @author Sebastien Deleuze
* @author Brian Clozel
* @since 5.0 * @since 5.0
*/ */
public interface WebClient { public interface WebClient {
@ -290,12 +291,35 @@ public interface WebClient {
Builder clientConnector(ClientHttpConnector connector); Builder clientConnector(ClientHttpConnector connector);
/** /**
* Configure the {@link ExchangeStrategies} to use. * Provide the {@link ExchangeStrategies} to use.
* <p>By default this is obtained from {@link ExchangeStrategies#withDefaults()}. * <p>This is useful for changing the default settings, yet still allowing
* further customizations via {@link #exchangeStrategies(Consumer)}.
* If not set, defaults are obtained from {@link ExchangeStrategies#withDefaults()}.
* @param strategies the strategies to use * @param strategies the strategies to use
* @deprecated as of 5.1, in favor of {@link #exchangeStrategies(ExchangeStrategies.Builder)}
*/ */
@Deprecated
Builder exchangeStrategies(ExchangeStrategies strategies); Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Provide the {@link ExchangeStrategies.Builder} to use.
* <p>This is useful for changing the default settings, yet still allowing
* further customizations via {@link #exchangeStrategies(Consumer)}.
* If not set, defaults are obtained from {@link ExchangeStrategies#builder()}.
* @param strategies the strategies to use
* @since 5.1.12
*/
Builder exchangeStrategies(ExchangeStrategies.Builder strategies);
/**
* Customize the {@link ExchangeStrategies}.
* <p>Allows further customization on {@link ExchangeStrategies},
* mutating them if they were {@link #exchangeStrategies(ExchangeStrategies) set},
* or starting from {@link ExchangeStrategies#withDefaults() defaults}.
* @since 5.1.12
*/
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
/** /**
* Provide an {@link ExchangeFunction} pre-configured with * Provide an {@link ExchangeFunction} pre-configured with
* {@link ClientHttpConnector} and {@link ExchangeStrategies}. * {@link ClientHttpConnector} and {@link ExchangeStrategies}.

View File

@ -39,4 +39,15 @@ public class ExchangeStrategiesTests {
assertThat(strategies.messageWriters().isEmpty()).isFalse(); assertThat(strategies.messageWriters().isEmpty()).isFalse();
} }
@Test
@SuppressWarnings("deprecation")
public void mutate() {
ExchangeStrategies strategies = ExchangeStrategies.empty().build();
assertThat(strategies.messageReaders().isEmpty()).isTrue();
assertThat(strategies.messageWriters().isEmpty()).isTrue();
ExchangeStrategies mutated = strategies.mutate().codecs(codecs -> codecs.registerDefaults(true)).build();
assertThat(mutated.messageReaders().isEmpty()).isFalse();
assertThat(mutated.messageWriters().isEmpty()).isFalse();
}
} }

View File

@ -41,28 +41,26 @@ The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs
[source,java,indent=0,subs="verbatim,quotes",role="primary"] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java .Java
---- ----
ExchangeStrategies strategies = ExchangeStrategies.builder() Consumer<ExchangeStrategies.Builder> customizeCodecs = builder -> {
.codecs(configurer -> { builder.codecs(configurer -> {
// ... //...
}) });
.build(); };
WebClient client = WebClient.builder() WebClient client = WebClient.builder()
.exchangeStrategies(strategies) .exchangeStrategies(customizeCodecs)
.build(); .build();
---- ----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin .Kotlin
---- ----
val strategies = ExchangeStrategies.builder() val webClient = WebClient.builder()
.codecs { .exchangeStrategies { strategies ->
// ... strategies.codecs {
//...
}
} }
.build() .build()
val client = WebClient.builder()
.exchangeStrategies(strategies)
.build()
---- ----
Once built, a `WebClient` instance is immutable. However, you can clone it and build a Once built, a `WebClient` instance is immutable. However, you can clone it and build a
@ -95,7 +93,44 @@ modified copy without affecting the original instance, as the following example
// client2 has filterA, filterB, filterC, filterD // client2 has filterA, filterB, filterC, filterD
---- ----
[[webflux-client-builder-maxinmemorysize]]
=== MaxInMemorySize
Spring WebFlux configures by default a maximum size for buffering data in-memory when decoding
HTTP responses with the `WebClient`. This avoids application memory issues if the received
response is much larger than expected.
The default configured value of 256KB might not be enough for your use case, and your application
might hit that limit with the following:
----
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
----
You can configure this limit on all default codecs with the following code sample:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebClient webClient = WebClient.builder()
.exchangeStrategies(configurer ->
configurer.codecs(codecs ->
codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
)
)
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val webClient = WebClient.builder()
.exchangeStrategies { strategies ->
strategies.codecs {
it.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
}
}
.build()
----
[[webflux-client-builder-reactor]] [[webflux-client-builder-reactor]]
=== Reactor Netty === Reactor Netty