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 f314b4455daf..82006d37b255 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. @@ -142,6 +142,12 @@ public WebTestClient.Builder exchangeStrategies(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 cc2e8ab96fc8..e8b109cbc014 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 @@ -77,6 +77,7 @@ * 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 @@ interface Builder { /** * Configure the {@link ExchangeStrategies} to use. - *

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

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

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

By default 5 seconds. @@ -877,7 +892,7 @@ interface BodyContentSpec { * @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 @@ default XpathAssertions xpath(String expression, Object... args){ * @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 db31e97218a4..e41ec7348117 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,12 @@ public interface ClientCodecConfigurer extends CodecConfigurer { @Override ClientDefaultCodecs defaultCodecs(); + /** + * {@inheritDoc}. + */ + @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 3d4c625b5d56..d16a0a9090fe 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,15 @@ public interface CodecConfigurer { */ List> 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 diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java index 59a209ac59a7..ba8501b3b297 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -62,6 +62,12 @@ public interface ServerCodecConfigurer extends CodecConfigurer { @Override ServerDefaultCodecs defaultCodecs(); + /** + * {@inheritDoc}. + */ + @Override + ServerCodecConfigurer clone(); + /** * Static factory method for a {@code ServerCodecConfigurer}. 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 e86ac954f9f6..505d19d0e284 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 @@ * 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 @@ public List> getWriters() { return getWritersInternal(false); } + /** * Internal method that returns the configured writers. * @param forMultipart whether to returns writers for general use ("false"), @@ -106,11 +125,14 @@ protected List> getWritersInternal(boolean forMultipart) { 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> typedReaders = new ArrayList<>(); @@ -121,6 +143,20 @@ private static final class DefaultCustomCodecs implements CustomCodecs { private final List> 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)); 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 f3034ad9354c..9be10da0951b 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 @@ -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; 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 9f578b7320ab..e764cb969612 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 @@ public void serverSentEventDecoder(Decoder 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 protected void extendObjectReaders(List> objectReaders) { @@ -116,6 +135,17 @@ private static class DefaultMultipartCodecs implements ClientCodecConfigurer.Mul 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 9875ded1b98d..737282eecd5e 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 @@ */ 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 2623d5a7f7b2..661d45d66693 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 @@ */ 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 37e924cd7e91..1d997c3777b1 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/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java index afa5f4cec37b..1b98450036e5 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java @@ -22,6 +22,7 @@ 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.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 void jackson2EncoderOverride() { .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> 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> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); 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 48e20a9074da..eb4a3a0d4589 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 @@ -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.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 void encoderDecoderOverrides() { 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> decoders = clone.getReaders().stream() + .filter(reader -> reader instanceof DecoderHttpMessageReader) + .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder()) + .collect(Collectors.toList()); + + List> 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> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); @@ -313,10 +382,21 @@ private void assertEncoderInstance(Encoder encoder) { private static class TestCodecConfigurer extends BaseCodecConfigurer { TestCodecConfigurer() { - super(new TestDefaultCodecs()); + super(new BaseDefaultCodecs()); + } + + TestCodecConfigurer(TestCodecConfigurer other) { + super(other); + } + + @Override + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new BaseDefaultCodecs((BaseDefaultCodecs) defaultCodecs()); } - private static class TestDefaultCodecs extends BaseDefaultCodecs { + @Override + public CodecConfigurer clone() { + return new TestCodecConfigurer(this); } } diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index b36cdd0ca7d6..6373ff6b515e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -60,7 +60,11 @@ 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 void maxInMemorySize() { 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> readers) { HttpMessageReader reader = nextReader(readers); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); 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 aa1523d9ace5..e5703203fc5d 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 @@ public ExchangeStrategies.Builder codecs(Consumer consume @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) { @@ -93,6 +100,11 @@ public List> messageReaders() { public List> messageWriters() { return this.writers; } + + @Override + public Builder mutate() { + return new DefaultExchangeStrategiesBuilder(this); + } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index c8796e91f782..44c3164b701b 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 @@ -25,9 +25,11 @@ 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 @@ * 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> strategiesConfigurers; @Nullable private ExchangeFunction exchangeFunction; public DefaultWebClientBuilder() { - this.exchangeStrategies = ExchangeStrategies.withDefaults(); } public DefaultWebClientBuilder(DefaultWebClientBuilder other) { @@ -95,7 +112,7 @@ public DefaultWebClientBuilder(DefaultWebClientBuilder other) { 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 @@ public WebClient.Builder clientConnector(ClientHttpConnector connector) { @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 configurer) { + if (this.strategiesConfigurers == null) { + this.strategiesConfigurers = new ArrayList<>(4); + } + this.strategiesConfigurers.add(configurer); return this; } @@ -215,7 +240,9 @@ public WebClient.Builder clone() { @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 @@ public WebClient build() { 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() { 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 804fbd9a42fd..acf32d0959ae 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,15 @@ public interface ExchangeStrategies { */ List> 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 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 8dc2a17c0127..f60a822597cc 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 @@ -64,6 +64,7 @@ * * @author Rossen Stoyanchev * @author Arjen Poutsma + * @author Brian Clozel * @since 5.0 */ public interface WebClient { @@ -289,11 +290,25 @@ interface Builder { /** * Configure the {@link ExchangeStrategies} to use. - *

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

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

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 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 b08662c8fb9c..b25bfe9dd4b7 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,16 @@ public void withDefaults() { 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()); + } + } diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 20980df9dd85..0bc6fdce2981 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -42,17 +42,14 @@ The following example configures < { - // ... - }) - .build(); - WebClient client = WebClient.builder() - .exchangeStrategies(strategies) + .exchangeStrategies(builder -> { + return builder.codecs(codecConfigurer -> { + //... + }); + }) .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 <> 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]] diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 5789f295273c..172a48b8cd4e 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -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 <> 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 <>. For <> the `maxInMemorySize` property limits the size of non-file parts. For file parts it determines the threshold at which the part