From b3020bc484bc79865892b67952090b796277e2c4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 29 Nov 2019 22:26:52 +0100 Subject: [PATCH] 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 --- .../messaging/rsocket/RSocketRequester.java | 15 ++--- .../server/DefaultWebTestClientBuilder.java | 15 ++++- .../web/reactive/server/WebTestClient.java | 30 ++++++++- .../http/codec/ClientCodecConfigurer.java | 7 ++- .../http/codec/CodecConfigurer.java | 6 ++ .../codec/support/BaseCodecConfigurer.java | 39 ++++++++++-- .../http/codec/support/BaseDefaultCodecs.java | 15 +++++ .../support/ClientDefaultCodecsImpl.java | 32 +++++++++- .../support/DefaultClientCodecConfigurer.java | 18 +++++- .../support/DefaultServerCodecConfigurer.java | 17 +++++- .../support/ServerDefaultCodecsImpl.java | 10 +++ .../codec/support/CodecConfigurerTests.java | 8 +++ .../DefaultExchangeStrategiesBuilder.java | 27 +++++--- .../client/DefaultWebClientBuilder.java | 40 ++++++++++-- .../function/client/ExchangeStrategies.java | 12 ++++ .../reactive/function/client/WebClient.java | 28 ++++++++- .../client/ExchangeStrategiesTests.java | 11 ++++ src/docs/asciidoc/web/webflux-webclient.adoc | 61 +++++++++++++++---- 18 files changed, 345 insertions(+), 46 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index 320b45f351..3cccb61408 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -169,18 +169,19 @@ public interface RSocketRequester { RSocketRequester.Builder setupMetadata(Object value, @Nullable MimeType mimeType); /** - * Provide {@link RSocketStrategies} to use. - *

By default this is based on default settings of - * {@link RSocketStrategies.Builder} but may be further customized via - * {@link #rsocketStrategies(Consumer)}. + * Provide the {@link RSocketStrategies} to use. + *

This is useful for changing the default settings, yet still allowing + * further customizations via {@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); /** * Customize the {@link RSocketStrategies}. - *

By default this starts out as {@link RSocketStrategies#builder()}. - * However if strategies were {@link #rsocketStrategies(RSocketStrategies) set} - * explicitly, then they are {@link RSocketStrategies#mutate() mutated}. + *

Allows further customization on {@link RSocketStrategies}, + * mutating them if they were {@link #rsocketStrategies(RSocketStrategies) set}, + * or starting from {@link RSocketStrategies#builder()} defaults}. */ RSocketRequester.Builder rsocketStrategies(Consumer configurer); 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 f314b4455d..4d5aeca17e 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 @@ -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"); * you may not use this file except in compliance with the License. @@ -137,11 +137,24 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } @Override + @Deprecated public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) { this.webClientBuilder.exchangeStrategies(strategies); return this; } + @Override + public WebTestClient.Builder exchangeStrategies(ExchangeStrategies.Builder strategies) { + this.webClientBuilder.exchangeStrategies(strategies); + return this; + } + + @Override + public WebTestClient.Builder exchangeStrategies(Consumer configurer) { + this.webClientBuilder.exchangeStrategies(configurer); + return this; + } + @Override public WebTestClient.Builder responseTimeout(Duration timeout) { this.responseTimeout = timeout; 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 7111b8d528..f7a0e78824 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 @@ -84,6 +84,7 @@ import org.springframework.web.util.UriBuilderFactory; * perform integration tests on an embedded WebFlux server. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 * @see StatusAssertions * @see HeaderAssertions @@ -443,11 +444,34 @@ public interface WebTestClient { /** * Configure the {@link ExchangeStrategies} to use. - *

By default {@link ExchangeStrategies#withDefaults()} is used. + *

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 + * @deprecated as of 5.1 in favor of {@link #exchangeStrategies(ExchangeStrategies.Builder)} */ + @Deprecated Builder exchangeStrategies(ExchangeStrategies strategies); + /** + * Configure the {@link ExchangeStrategies.Builder} to use. + *

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}. + *

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 configurer); + /** * Max amount of time to wait for responses. *

By default 5 seconds. @@ -928,7 +952,7 @@ public interface WebTestClient { * @since 5.1 * @see #xpath(String, Map, Object...) */ - default XpathAssertions xpath(String expression, Object... args){ + default XpathAssertions xpath(String expression, Object... args) { return xpath(expression, null, args); } @@ -942,7 +966,7 @@ public interface WebTestClient { * @param args arguments to parameterize the expression * @since 5.1 */ - XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); + XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); /** * Assert the response body content with the given {@link Consumer}. diff --git a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java index db31e97218..028d85af38 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java @@ -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"); * you may not use this file except in compliance with the License. @@ -63,6 +63,11 @@ public interface ClientCodecConfigurer extends CodecConfigurer { @Override ClientDefaultCodecs defaultCodecs(); + /** + * Clone this {@link ClientCodecConfigurer}. + */ + @Override + ClientCodecConfigurer clone(); /** * Static factory method for a {@code ClientCodecConfigurer}. diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index 4e1ca8bd01..55184522c5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -87,6 +87,12 @@ public interface CodecConfigurer { */ List> getWriters(); + /** + * Clone this {@link CodecConfigurer}. + * @since 5.1.12 + */ + CodecConfigurer clone(); + /** * Customize or replace the HTTP message readers and writers registered by diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java index 3c6b367c22..6d7c619388 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java @@ -34,13 +34,14 @@ import org.springframework.util.Assert; * client and server specific variants. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 */ 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) { Assert.notNull(defaultCodecs, "'defaultCodecs' is required"); 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); } + + @Override + public CodecConfigurer clone() { + return new BaseCodecConfigurer(this); + } + + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new BaseDefaultCodecs(this.defaultCodecs); + } + + /** * Internal method that returns the configured writers. * @param forMultipart whether to returns writers for general use ("false"), @@ -110,7 +132,7 @@ class BaseCodecConfigurer implements CodecConfigurer { /** * Default implementation of {@code CustomCodecs}. */ - private static final class DefaultCustomCodecs implements CustomCodecs { + protected static final class DefaultCustomCodecs implements CustomCodecs { private final List> typedReaders = new ArrayList<>(); @@ -121,6 +143,16 @@ class BaseCodecConfigurer implements CodecConfigurer { private final List> 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 public void decoder(Decoder decoder) { reader(new DecoderHttpMessageReader<>(decoder)); @@ -143,7 +175,6 @@ class BaseCodecConfigurer implements CodecConfigurer { (canWriteObject ? this.objectWriters : this.typedWriters).add(writer); } - // Package private accessors... List> getTypedReaders() { diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 39d76ad0dd..1020db44d9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -106,6 +106,21 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { 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 public void jackson2JsonDecoder(Decoder decoder) { this.jackson2JsonDecoder = decoder; diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java index 9f578b7320..e764cb9696 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java @@ -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"); * you may not use this file except in compliance with the License. @@ -49,6 +49,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo private Supplier>> 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 * {@link #multipartCodecs()} are not explicitly configured. @@ -73,6 +84,14 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo 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 protected void extendObjectReaders(List> objectReaders) { @@ -116,6 +135,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo private final List> writers = new ArrayList<>(); + + DefaultMultipartCodecs() { + } + + DefaultMultipartCodecs(@Nullable DefaultMultipartCodecs other) { + if (other != null) { + this.writers.addAll(other.writers); + } + } + + @Override public ClientCodecConfigurer.MultipartCodecs encoder(Encoder encoder) { writer(new EncoderHttpMessageWriter<>(encoder)); diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java index 9875ded1b9..737282eecd 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java @@ -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"); * 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 DefaultClientCodecConfigurer() { super(new ClientDefaultCodecsImpl()); ((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true)); } + private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) { + super(other); + } + + @Override public ClientDefaultCodecs defaultCodecs() { return (ClientDefaultCodecs) super.defaultCodecs(); } + @Override + public DefaultClientCodecConfigurer clone() { + return new DefaultClientCodecConfigurer(this); + } + + @Override + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new ClientDefaultCodecsImpl((ClientDefaultCodecsImpl) defaultCodecs()); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java index 2623d5a7f7..661d45d666 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java @@ -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"); * 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 DefaultServerCodecConfigurer() { super(new ServerDefaultCodecsImpl()); } + private DefaultServerCodecConfigurer(BaseCodecConfigurer other) { + super(other); + } + + @Override public ServerDefaultCodecs defaultCodecs() { return (ServerDefaultCodecs) super.defaultCodecs(); } + @Override + public DefaultServerCodecConfigurer clone() { + return new DefaultServerCodecConfigurer(this); + } + + @Override + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new ServerDefaultCodecsImpl((ServerDefaultCodecsImpl) defaultCodecs()); + } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java index 37e924cd7e..1d997c3777 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java @@ -46,6 +46,16 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo private Encoder sseEncoder; + ServerDefaultCodecsImpl() { + } + + ServerDefaultCodecsImpl(ServerDefaultCodecsImpl other) { + super(other); + this.multipartReader = other.multipartReader; + this.sseEncoder = other.sseEncoder; + } + + @Override public void multipartReader(HttpMessageReader reader) { this.multipartReader = reader; diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 1a27f64000..16164e24c5 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -268,6 +268,14 @@ public class CodecConfigurerTests { 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> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); assertThat(reader.getClass()).isEqualTo(DecoderHttpMessageReader.class); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java index aa1523d9ac..02b0cc5e55 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java @@ -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"); * you may not use this file except in compliance with the License. @@ -42,13 +42,18 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build } - private final ClientCodecConfigurer codecConfigurer = ClientCodecConfigurer.create(); + private final ClientCodecConfigurer codecConfigurer; public DefaultExchangeStrategiesBuilder() { + this.codecConfigurer = ClientCodecConfigurer.create(); this.codecConfigurer.registerDefaults(false); } + private DefaultExchangeStrategiesBuilder(DefaultExchangeStrategies other) { + this.codecConfigurer = other.codecConfigurer.clone(); + } + public void defaultConfiguration() { this.codecConfigurer.registerDefaults(true); @@ -62,21 +67,23 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build @Override public ExchangeStrategies build() { - return new DefaultExchangeStrategies( - this.codecConfigurer.getReaders(), this.codecConfigurer.getWriters()); + return new DefaultExchangeStrategies(this.codecConfigurer); } private static class DefaultExchangeStrategies implements ExchangeStrategies { + private final ClientCodecConfigurer codecConfigurer; + private final List> readers; private final List> writers; - public DefaultExchangeStrategies(List> readers, List> writers) { - this.readers = unmodifiableCopy(readers); - this.writers = unmodifiableCopy(writers); + public DefaultExchangeStrategies(ClientCodecConfigurer codecConfigurer) { + this.codecConfigurer = codecConfigurer; + this.readers = unmodifiableCopy(this.codecConfigurer.getReaders()); + this.writers = unmodifiableCopy(this.codecConfigurer.getWriters()); } private static List unmodifiableCopy(List list) { @@ -84,6 +91,12 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build } + @Override + @Deprecated + public Builder mutate() { + return new DefaultExchangeStrategiesBuilder(this); + } + @Override public List> messageReaders() { return this.readers; 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 db699ddaae..82b4c49408 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 @@ -40,6 +40,7 @@ import org.springframework.web.util.UriBuilderFactory; * Default implementation of {@link WebClient.Builder}. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 */ final class DefaultWebClientBuilder implements WebClient.Builder { @@ -79,14 +80,16 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private ClientHttpConnector connector; - private ExchangeStrategies exchangeStrategies; + @Nullable + private ExchangeStrategies.Builder strategies; + + private List> strategiesConfigurers; @Nullable private ExchangeFunction exchangeFunction; public DefaultWebClientBuilder() { - this.exchangeStrategies = ExchangeStrategies.withDefaults(); } public DefaultWebClientBuilder(DefaultWebClientBuilder other) { @@ -108,7 +111,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder { this.defaultRequest = other.defaultRequest; this.filters = other.filters != null ? new ArrayList<>(other.filters) : null; this.connector = other.connector; - this.exchangeStrategies = other.exchangeStrategies; + this.strategies = other.strategies; this.exchangeFunction = other.exchangeFunction; } @@ -203,9 +206,23 @@ final class DefaultWebClientBuilder implements WebClient.Builder { } @Override + @Deprecated public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { 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 configurer) { + this.strategiesConfigurers.add(configurer); return this; } @@ -229,7 +246,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Override public WebClient build() { ExchangeFunction exchange = (this.exchangeFunction == null ? - ExchangeFunctions.create(getOrInitConnector(), this.exchangeStrategies) : + ExchangeFunctions.create(getOrInitConnector(), initExchangeStrategies()) : this.exchangeFunction); ExchangeFunction filteredExchange = (this.filters != null ? this.filters.stream() .reduce(ExchangeFilterFunction::andThen) @@ -254,6 +271,19 @@ final class DefaultWebClientBuilder implements WebClient.Builder { 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() { if (this.uriBuilderFactory != null) { return this.uriBuilderFactory; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java index 804fbd9a42..dfc2e1e14d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java @@ -47,6 +47,18 @@ public interface ExchangeStrategies { */ List> 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 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 57fb639724..79fdc5a8c1 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 @@ -66,6 +66,7 @@ import org.springframework.web.util.UriBuilderFactory; * @author Rossen Stoyanchev * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel * @since 5.0 */ public interface WebClient { @@ -290,12 +291,35 @@ public interface WebClient { Builder clientConnector(ClientHttpConnector connector); /** - * Configure the {@link ExchangeStrategies} to use. - *

By default this is obtained from {@link ExchangeStrategies#withDefaults()}. + * Provide the {@link ExchangeStrategies} to use. + *

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 + * @deprecated as of 5.1, in favor of {@link #exchangeStrategies(ExchangeStrategies.Builder)} */ + @Deprecated Builder exchangeStrategies(ExchangeStrategies strategies); + /** + * Provide the {@link ExchangeStrategies.Builder} to use. + *

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}. + *

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 configurer); + /** * Provide an {@link ExchangeFunction} pre-configured with * {@link ClientHttpConnector} and {@link ExchangeStrategies}. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java index eb37921f7a..af0eeb0f22 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java @@ -39,4 +39,15 @@ public class ExchangeStrategiesTests { 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(); + } + } diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 5ef50e1c19..7963da0087 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -41,28 +41,26 @@ The following example configures < { - // ... - }) - .build(); + Consumer customizeCodecs = builder -> { + builder.codecs(configurer -> { + //... + }); + }; WebClient client = WebClient.builder() - .exchangeStrategies(strategies) + .exchangeStrategies(customizeCodecs) .build(); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - val strategies = ExchangeStrategies.builder() - .codecs { - // ... + val webClient = WebClient.builder() + .exchangeStrategies { strategies -> + strategies.codecs { + //... + } } .build() - - val client = WebClient.builder() - .exchangeStrategies(strategies) - .build() ---- 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 ---- +[[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]] === Reactor Netty