Allow ExchangeStrategies customizations in WebClient

Backport of d4209392 and acfeb77d

Closes gh-23961
This commit is contained in:
Brian Clozel
2019-11-29 22:26:52 +01:00
committed by Rossen Stoyanchev
parent 59165dd526
commit 83683a13bb
21 changed files with 498 additions and 46 deletions

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");
* you may not use this file except in compliance with the License.
@@ -142,6 +142,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
return this;
}
@Override
public WebTestClient.Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer) {
this.webClientBuilder.exchangeStrategies(configurer);
return this;
}
@Override
public WebTestClient.Builder responseTimeout(Duration timeout) {
this.responseTimeout = timeout;

View File

@@ -77,6 +77,7 @@ import org.springframework.web.util.UriBuilderFactory;
* and Spring Kotlin extensions to perform integration tests on an embedded WebFlux server.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
* @see StatusAssertions
* @see HeaderAssertions
@@ -436,11 +437,25 @@ public interface WebTestClient {
/**
* Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
* <p>Note that in a scenario where the builder is configured by
* multiple parties, it is preferable to use
* {@link #exchangeStrategies(Consumer)} in order to customize the same
* {@code ExchangeStrategies}. This method here sets the strategies that
* everyone else then can customize.
* <p>By default this is {@link ExchangeStrategies#withDefaults()}.
* @param strategies the strategies to use
*/
Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Customize the strategies configured via
* {@link #exchangeStrategies(ExchangeStrategies)}. This method is
* designed for use in scenarios where multiple parties wish to update
* the {@code ExchangeStrategies}.
* @since 5.1.12
*/
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
/**
* Max amount of time to wait for responses.
* <p>By default 5 seconds.
@@ -877,7 +892,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);
}
@@ -891,7 +906,7 @@ public interface WebTestClient {
* @param args arguments to parameterize the expression
* @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}.

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");
* you may not use this file except in compliance with the License.
@@ -63,6 +63,12 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
@Override
ClientDefaultCodecs defaultCodecs();
/**
* {@inheritDoc}.
*/
@Override
ClientCodecConfigurer clone();
/**
* Static factory method for a {@code ClientCodecConfigurer}.

View File

@@ -87,6 +87,15 @@ public interface CodecConfigurer {
*/
List<HttpMessageWriter<?>> getWriters();
/**
* Create a copy of this {@link CodecConfigurer}. The returned clone has its
* own lists of default and custom codecs and generally can be configured
* independently. Keep in mind however that codec instances (if any are
* configured) are themselves not cloned.
* @since 5.1.12
*/
CodecConfigurer clone();
/**
* Customize or replace the HTTP message readers and writers registered by

View File

@@ -62,6 +62,12 @@ public interface ServerCodecConfigurer extends CodecConfigurer {
@Override
ServerDefaultCodecs defaultCodecs();
/**
* {@inheritDoc}.
*/
@Override
ServerCodecConfigurer clone();
/**
* Static factory method for a {@code ServerCodecConfigurer}.

View File

@@ -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 {
abstract class BaseCodecConfigurer implements CodecConfigurer {
private final BaseDefaultCodecs defaultCodecs;
protected final BaseDefaultCodecs defaultCodecs;
private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs();
protected final DefaultCustomCodecs customCodecs;
/**
@@ -50,8 +51,25 @@ class BaseCodecConfigurer implements CodecConfigurer {
BaseCodecConfigurer(BaseDefaultCodecs defaultCodecs) {
Assert.notNull(defaultCodecs, "'defaultCodecs' is required");
this.defaultCodecs = defaultCodecs;
this.customCodecs = new DefaultCustomCodecs();
}
/**
* Create a deep copy of the given {@link BaseCodecConfigurer}.
* @since 5.1.12
*/
protected BaseCodecConfigurer(BaseCodecConfigurer other) {
this.defaultCodecs = other.cloneDefaultCodecs();
this.customCodecs = new DefaultCustomCodecs(other.customCodecs);
}
/**
* Sub-classes should override this to create deep copy of
* {@link BaseDefaultCodecs} which can can be client or server specific.
* @since 5.1.12
*/
protected abstract BaseDefaultCodecs cloneDefaultCodecs();
@Override
public DefaultCodecs defaultCodecs() {
@@ -87,6 +105,7 @@ class BaseCodecConfigurer implements CodecConfigurer {
return getWritersInternal(false);
}
/**
* Internal method that returns the configured writers.
* @param forMultipart whether to returns writers for general use ("false"),
@@ -106,11 +125,14 @@ class BaseCodecConfigurer implements CodecConfigurer {
return result;
}
@Override
public abstract CodecConfigurer clone();
/**
* 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<>();
@@ -121,6 +143,20 @@ class BaseCodecConfigurer implements CodecConfigurer {
private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>();
DefaultCustomCodecs() {
}
/**
* Create a deep copy of the given {@link DefaultCustomCodecs}.
* @since 5.1.12
*/
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));

View File

@@ -105,6 +105,24 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
private boolean registerDefaults = true;
BaseDefaultCodecs() {
}
/**
* Create a deep copy of the given {@link 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;

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");
* 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;
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<HttpMessageReader<?>> objectReaders) {
@@ -116,6 +135,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private final List<HttpMessageWriter<?>> 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));

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");
* 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());
}
}

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");
* 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());
}
}

View File

@@ -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;

View File

@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.junit.Test;
import reactor.core.publisher.Flux;
@@ -59,7 +60,11 @@ import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.util.MimeTypeUtils;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
/**
@@ -122,6 +127,47 @@ public class ClientCodecConfigurerTests {
.filter(e -> e == decoder).orElse(null));
}
@Test
public void cloneConfigurer() {
ClientCodecConfigurer clone = this.configurer.clone();
Jackson2JsonDecoder jackson2Decoder = new Jackson2JsonDecoder();
clone.defaultCodecs().serverSentEventDecoder(jackson2Decoder);
clone.defaultCodecs().multipartCodecs().encoder(new Jackson2SmileEncoder());
clone.defaultCodecs().multipartCodecs().writer(new ResourceHttpMessageWriter());
// Clone has the customizations
Decoder<?> sseDecoder = clone.getReaders().stream()
.filter(reader -> reader instanceof ServerSentEventHttpMessageReader)
.map(reader -> ((ServerSentEventHttpMessageReader) reader).getDecoder())
.findFirst()
.get();
List<HttpMessageWriter<?>> multipartWriters = clone.getWriters().stream()
.filter(writer -> writer instanceof MultipartHttpMessageWriter)
.flatMap(writer -> ((MultipartHttpMessageWriter) writer).getPartWriters().stream())
.collect(Collectors.toList());
assertSame(jackson2Decoder, sseDecoder);
assertEquals(2, multipartWriters.size());
// Original does not have the customizations
sseDecoder = this.configurer.getReaders().stream()
.filter(reader -> reader instanceof ServerSentEventHttpMessageReader)
.map(reader -> ((ServerSentEventHttpMessageReader) reader).getDecoder())
.findFirst()
.get();
multipartWriters = this.configurer.getWriters().stream()
.filter(writer -> writer instanceof MultipartHttpMessageWriter)
.flatMap(writer -> ((MultipartHttpMessageWriter) writer).getPartWriters().stream())
.collect(Collectors.toList());
assertNotSame(jackson2Decoder, sseDecoder);
assertEquals(10, multipartWriters.size());
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());

View File

@@ -16,8 +16,10 @@
package org.springframework.http.codec.support;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import com.google.protobuf.ExtensionRegistry;
import org.junit.Test;
@@ -42,6 +44,8 @@ import org.springframework.http.codec.FormHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder;
@@ -267,6 +271,71 @@ public class CodecConfigurerTests {
assertEncoderInstance(jaxb2Encoder);
}
@Test
public void cloneCustomCodecs() {
this.configurer.registerDefaults(false);
CodecConfigurer clone = this.configurer.clone();
clone.customCodecs().encoder(new Jackson2JsonEncoder());
clone.customCodecs().decoder(new Jackson2JsonDecoder());
clone.customCodecs().reader(new ServerSentEventHttpMessageReader());
clone.customCodecs().writer(new ServerSentEventHttpMessageWriter());
assertEquals(0, this.configurer.getReaders().size());
assertEquals(0, this.configurer.getWriters().size());
assertEquals(2, clone.getReaders().size());
assertEquals(2, clone.getWriters().size());
}
@Test
public void cloneDefaultCodecs() {
CodecConfigurer clone = this.configurer.clone();
Jackson2JsonDecoder jacksonDecoder = new Jackson2JsonDecoder();
Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder();
Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder();
ProtobufDecoder protoDecoder = new ProtobufDecoder();
ProtobufEncoder protoEncoder = new ProtobufEncoder();
clone.defaultCodecs().jackson2JsonDecoder(jacksonDecoder);
clone.defaultCodecs().jackson2JsonEncoder(jacksonEncoder);
clone.defaultCodecs().jaxb2Decoder(jaxb2Decoder);
clone.defaultCodecs().jaxb2Encoder(jaxb2Encoder);
clone.defaultCodecs().protobufDecoder(protoDecoder);
clone.defaultCodecs().protobufEncoder(protoEncoder);
// Clone has the customized the customizations
List<Decoder<?>> decoders = clone.getReaders().stream()
.filter(reader -> reader instanceof DecoderHttpMessageReader)
.map(reader -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
.collect(Collectors.toList());
List<Encoder<?>> encoders = clone.getWriters().stream()
.filter(writer -> writer instanceof EncoderHttpMessageWriter)
.map(reader -> ((EncoderHttpMessageWriter<?>) reader).getEncoder())
.collect(Collectors.toList());
assertTrue(decoders.containsAll(Arrays.asList(jacksonDecoder, jaxb2Decoder, protoDecoder)));
assertTrue(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder)));
// Original does not have the customizations
decoders = this.configurer.getReaders().stream()
.filter(reader -> reader instanceof DecoderHttpMessageReader)
.map(reader -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
.collect(Collectors.toList());
encoders = this.configurer.getWriters().stream()
.filter(writer -> writer instanceof EncoderHttpMessageWriter)
.map(reader -> ((EncoderHttpMessageWriter<?>) reader).getEncoder())
.collect(Collectors.toList());
assertFalse(decoders.containsAll(Arrays.asList(jacksonDecoder, jaxb2Decoder, protoDecoder)));
assertFalse(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder)));
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());
assertEquals(DecoderHttpMessageReader.class, reader.getClass());
@@ -313,10 +382,21 @@ public class CodecConfigurerTests {
private static class TestCodecConfigurer extends BaseCodecConfigurer {
TestCodecConfigurer() {
super(new TestDefaultCodecs());
super(new BaseDefaultCodecs());
}
private static class TestDefaultCodecs extends BaseDefaultCodecs {
TestCodecConfigurer(TestCodecConfigurer other) {
super(other);
}
@Override
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new BaseDefaultCodecs((BaseDefaultCodecs) defaultCodecs());
}
@Override
public CodecConfigurer clone() {
return new TestCodecConfigurer(this);
}
}

View File

@@ -60,7 +60,11 @@ import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.util.MimeTypeUtils;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
/**
@@ -149,6 +153,50 @@ public class ServerCodecConfigurerTests {
assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize());
}
@Test
public void cloneConfigurer() {
ServerCodecConfigurer clone = this.configurer.clone();
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader());
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
clone.defaultCodecs().multipartReader(reader);
clone.defaultCodecs().serverSentEventEncoder(encoder);
// Clone has the customizations
HttpMessageReader<?> actualReader = clone.getReaders().stream()
.filter(r -> r instanceof MultipartHttpMessageReader)
.findFirst()
.get();
Encoder<?> actualEncoder = clone.getWriters().stream()
.filter(writer -> writer instanceof ServerSentEventHttpMessageWriter)
.map(writer -> ((ServerSentEventHttpMessageWriter) writer).getEncoder())
.findFirst()
.get();
assertSame(reader, actualReader);
assertSame(encoder, actualEncoder);
// Original does not have the customizations
actualReader = this.configurer.getReaders().stream()
.filter(r -> r instanceof MultipartHttpMessageReader)
.findFirst()
.get();
actualEncoder = this.configurer.getWriters().stream()
.filter(writer -> writer instanceof ServerSentEventHttpMessageWriter)
.map(writer -> ((ServerSentEventHttpMessageWriter) writer).getEncoder())
.findFirst()
.get();
assertNotSame(reader, actualReader);
assertNotSame(encoder, actualEncoder);
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = nextReader(readers);
assertEquals(DecoderHttpMessageReader.class, reader.getClass());

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");
* 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<HttpMessageReader<?>> readers;
private final List<HttpMessageWriter<?>> writers;
public DefaultExchangeStrategies(List<HttpMessageReader<?>> readers, List<HttpMessageWriter<?>> 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 <T> List<T> unmodifiableCopy(List<? extends T> list) {
@@ -93,6 +100,11 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build
public List<HttpMessageWriter<?>> messageWriters() {
return this.writers;
}
@Override
public Builder mutate() {
return new DefaultExchangeStrategiesBuilder(this);
}
}
}

View File

@@ -25,9 +25,11 @@ import java.util.function.Consumer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -38,10 +40,22 @@ 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 {
private static final boolean reactorClientPresent;
private static final boolean jettyClientPresent;
static {
ClassLoader loader = DefaultWebClientBuilder.class.getClassLoader();
reactorClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader);
jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader);
}
@Nullable
private String baseUrl;
@@ -66,14 +80,17 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
@Nullable
private ClientHttpConnector connector;
private ExchangeStrategies exchangeStrategies;
@Nullable
private ExchangeStrategies strategies;
@Nullable
private List<Consumer<ExchangeStrategies.Builder>> strategiesConfigurers;
@Nullable
private ExchangeFunction exchangeFunction;
public DefaultWebClientBuilder() {
this.exchangeStrategies = ExchangeStrategies.withDefaults();
}
public DefaultWebClientBuilder(DefaultWebClientBuilder other) {
@@ -95,7 +112,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;
}
@@ -191,8 +208,16 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
@Override
public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
Assert.notNull(strategies, "ExchangeStrategies must not be null");
this.exchangeStrategies = strategies;
this.strategies = strategies;
return this;
}
@Override
public WebClient.Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer) {
if (this.strategiesConfigurers == null) {
this.strategiesConfigurers = new ArrayList<>(4);
}
this.strategiesConfigurers.add(configurer);
return this;
}
@@ -215,7 +240,9 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
@Override
public WebClient build() {
ExchangeFunction exchange = initExchangeFunction();
ExchangeFunction exchange = (this.exchangeFunction == null ?
ExchangeFunctions.create(getOrInitConnector(), initExchangeStrategies()) :
this.exchangeFunction);
ExchangeFunction filteredExchange = (this.filters != null ? this.filters.stream()
.reduce(ExchangeFilterFunction::andThen)
.map(filter -> filter.apply(exchange))
@@ -226,16 +253,29 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
this.defaultRequest, new DefaultWebClientBuilder(this));
}
private ExchangeFunction initExchangeFunction() {
if (this.exchangeFunction != null) {
return this.exchangeFunction;
private ClientHttpConnector getOrInitConnector() {
if (this.connector != null) {
return this.connector;
}
else if (this.connector != null) {
return ExchangeFunctions.create(this.connector, this.exchangeStrategies);
else if (reactorClientPresent) {
return new ReactorClientHttpConnector();
}
else {
return ExchangeFunctions.create(new ReactorClientHttpConnector(), this.exchangeStrategies);
else if (jettyClientPresent) {
return new JettyClientHttpConnector();
}
throw new IllegalStateException("No suitable default ClientHttpConnector found");
}
private ExchangeStrategies initExchangeStrategies() {
if (CollectionUtils.isEmpty(this.strategiesConfigurers)) {
return this.strategies != null ? this.strategies : ExchangeStrategies.withDefaults();
}
ExchangeStrategies.Builder builder =
this.strategies != null ? this.strategies.mutate() : ExchangeStrategies.builder();
this.strategiesConfigurers.forEach(configurer -> configurer.accept(builder));
return builder.build();
}
private UriBuilderFactory initUriBuilderFactory() {

View File

@@ -47,6 +47,15 @@ public interface ExchangeStrategies {
*/
List<HttpMessageWriter<?>> messageWriters();
/**
* Return a builder to create a new {@link ExchangeStrategies} instance
* replicated from the current instance.
* @since 5.1.12
*/
default Builder mutate() {
throw new UnsupportedOperationException();
}
// Static builder methods

View File

@@ -64,6 +64,7 @@ import org.springframework.web.util.UriBuilderFactory;
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @author Brian Clozel
* @since 5.0
*/
public interface WebClient {
@@ -289,11 +290,25 @@ public interface WebClient {
/**
* Configure the {@link ExchangeStrategies} to use.
* <p>By default this is obtained from {@link ExchangeStrategies#withDefaults()}.
* <p>Note that in a scenario where the builder is configured by
* multiple parties, it is preferable to use
* {@link #exchangeStrategies(Consumer)} in order to customize the same
* {@code ExchangeStrategies}. This method here sets the strategies that
* everyone else then can customize.
* <p>By default this is {@link ExchangeStrategies#withDefaults()}.
* @param strategies the strategies to use
*/
Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Customize the strategies configured via
* {@link #exchangeStrategies(ExchangeStrategies)}. This method is
* designed for use in scenarios where multiple parties wish to update
* the {@code ExchangeStrategies}.
* @since 5.1.12
*/
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
/**
* Provide an {@link ExchangeFunction} pre-configured with
* {@link ClientHttpConnector} and {@link ExchangeStrategies}.

View File

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

View File

@@ -42,17 +42,14 @@ The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs
[source,java,intent=0]
[subs="verbatim,quotes"]
----
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> {
// ...
WebClient client = WebClient.builder()
.exchangeStrategies(builder -> {
return builder.codecs(codecConfigurer -> {
//...
});
})
.build();
WebClient client = WebClient.builder()
.exchangeStrategies(strategies)
.build();
----
====
Once built, a `WebClient` instance is immutable. However, you can clone it and build a
modified copy without affecting the original instance, as the following example shows:
@@ -73,6 +70,31 @@ modified copy without affecting the original instance, as the following example
----
====
[[webflux-client-builder-maxinmemorysize]]
=== MaxInMemorySize
Spring WebFlux configures <<web-reactive.adoc#webflux-codecs-limits,limits>> for buffering
data in-memory in codec to avoid application memory issues. By the default this is
configured to 256KB and if that's not enough for your use case, you'll see 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(builder ->
builder.codecs(codecs ->
codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
)
)
.build();
----
[[webflux-client-builder-reactor]]

View File

@@ -776,7 +776,8 @@ To configure buffer sizes, you can check if a given `Decoder` or `HttpMessageRea
exposes a `maxInMemorySize` property and if so the Javadoc will have details about default
values. In WebFlux, the `ServerCodecConfigurer` provides a
<<webflux-config-message-codecs,single place>> from where to set all codecs, through the
`maxInMemorySize` property for default codecs.
`maxInMemorySize` property for default codecs. On the client side, the limit can be changed
in <<web-reactive.adoc#webflux-client-builder-maxinmemorysize, WebClient.Builder>>.
For <<webflux-codecs-multipart,Multipart parsing>> the `maxInMemorySize` property limits
the size of non-file parts. For file parts it determines the threshold at which the part