From 871464811cc1b18d684408f71725cead20c70796 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 28 Oct 2019 14:26:26 +0000 Subject: [PATCH 001/322] Limits on input stream in codecs - Add maxInMemorySize property to Decoder and HttpMessageReader implementations that aggregate input to trigger DataBufferLimitException when reached. - For codecs that call DataBufferUtils#join, there is now an overloaded variant with a maxInMemorySize extra argument. Internally, a custom LimitedDataBufferList is used to count and enforce the limit. - Jackson2Tokenizer and XmlEventDecoder support those limits per streamed JSON object. - Configurable limits for multipart requests with Synchronoss NIO. - Centralized maxInMemorySize exposed via CodecConfigurer along with ability to plug in an instance of MultipartHttpMessageWrite. Closes gh-23884 --- .../core/codec/AbstractDataBufferDecoder.java | 30 +- .../core/codec/StringDecoder.java | 40 +++ .../io/buffer/DataBufferLimitException.java | 37 +++ .../core/io/buffer/DataBufferUtils.java | 30 +- .../core/io/buffer/LimitedDataBufferList.java | 157 ++++++++++ .../core/codec/StringDecoderTests.java | 15 + .../core/io/buffer/DataBufferUtilsTests.java | 28 +- .../io/buffer/LimitedDataBufferListTests.java | 63 ++++ .../http/codec/CodecConfigurer.java | 17 +- .../http/codec/FormHttpMessageReader.java | 27 +- .../http/codec/ServerCodecConfigurer.java | 16 +- .../codec/json/AbstractJackson2Decoder.java | 31 +- .../http/codec/json/Jackson2Tokenizer.java | 51 +++- .../multipart/MultipartHttpMessageReader.java | 8 + .../SynchronossPartHttpMessageReader.java | 287 +++++++++++++----- .../http/codec/protobuf/ProtobufDecoder.java | 23 +- .../http/codec/support/BaseDefaultCodecs.java | 62 +++- .../support/ServerDefaultCodecsImpl.java | 18 +- .../http/codec/xml/Jaxb2XmlDecoder.java | 26 ++ .../http/codec/xml/XmlEventDecoder.java | 77 ++++- .../codec/json/Jackson2TokenizerTests.java | 94 ++++-- ...SynchronossPartHttpMessageReaderTests.java | 111 +++++-- .../support/ServerCodecConfigurerTests.java | 32 +- .../http/codec/xml/XmlEventDecoderTests.java | 53 +++- src/docs/asciidoc/web/webflux.adoc | 27 ++ 25 files changed, 1169 insertions(+), 191 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java create mode 100644 spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java create mode 100644 spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java index b03d8079db7..8fedf475763 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java @@ -47,12 +47,40 @@ */ public abstract class AbstractDataBufferDecoder extends AbstractDecoder { + private int maxInMemorySize = -1; + protected AbstractDataBufferDecoder(MimeType... supportedMimeTypes) { super(supportedMimeTypes); } + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

By default in 5.1 this is set to -1, unlimited. In 5.2 the default + * value for this limit is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + + @Override public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { @@ -64,7 +92,7 @@ public Flux decode(Publisher input, ResolvableType elementType, public Mono decodeToMono(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - return DataBufferUtils.join(input) + return DataBufferUtils.join(input, this.maxInMemorySize) .map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints)); } diff --git a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java index 28cf7df55e8..c3100490dde 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -25,14 +25,17 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.LimitedDataBufferList; import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.core.log.LogFormatUtils; import org.springframework.lang.Nullable; @@ -92,10 +95,16 @@ public Flux decode(Publisher input, ResolvableType elementTy List delimiterBytes = getDelimiterBytes(mimeType); + // TODO: Drop Consumer and use bufferUntil with Supplier (reactor-core#1925) + // TODO: Drop doOnDiscard(LimitedDataBufferList.class, ...) (reactor-core#1924) + LimitedDataBufferConsumer limiter = new LimitedDataBufferConsumer(getMaxInMemorySize()); + Flux inputFlux = Flux.from(input) .flatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes)) + .doOnNext(limiter) .bufferUntil(buffer -> buffer == END_FRAME) .map(StringDecoder::joinUntilEndFrame) + .doOnDiscard(LimitedDataBufferList.class, LimitedDataBufferList::releaseAndClear) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); return super.decode(inputFlux, elementType, mimeType, hints); @@ -283,4 +292,35 @@ public static StringDecoder allMimeTypes(List delimiters, boolean stripD new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL); } + + /** + * Temporary measure for reactor-core#1925. + * Consumer that adds to a {@link LimitedDataBufferList} to enforce limits. + */ + private static class LimitedDataBufferConsumer implements Consumer { + + private final LimitedDataBufferList bufferList; + + + public LimitedDataBufferConsumer(int maxInMemorySize) { + this.bufferList = new LimitedDataBufferList(maxInMemorySize); + } + + + @Override + public void accept(DataBuffer buffer) { + if (buffer == END_FRAME) { + this.bufferList.clear(); + } + else { + try { + this.bufferList.add(buffer); + } + catch (DataBufferLimitException ex) { + DataBufferUtils.release(buffer); + throw ex; + } + } + } + } } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java new file mode 100644 index 00000000000..ee606aed57f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java @@ -0,0 +1,37 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core.io.buffer; + +/** + * Exception that indicates the cumulative number of bytes consumed from a + * stream of {@link DataBuffer DataBuffer}'s exceeded some pre-configured limit. + * This can be raised when data buffers are cached and aggregated, e.g. + * {@link DataBufferUtils#join}. Or it could also be raised when data buffers + * have been released but a parsed representation is being aggregated, e.g. async + * parsing with Jackson. + * + * @author Rossen Stoyanchev + * @since 5.1.11 + */ +@SuppressWarnings("serial") +public class DataBufferLimitException extends IllegalStateException { + + + public DataBufferLimitException(String message) { + super(message); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 9ac2d80b77d..c756efd735a 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -437,14 +437,36 @@ public static Consumer releaseConsumer() { * @since 5.0.3 */ public static Mono join(Publisher dataBuffers) { - Assert.notNull(dataBuffers, "'dataBuffers' must not be null"); + return join(dataBuffers, -1); + } + + /** + * Variant of {@link #join(Publisher)} that behaves the same way up until + * the specified max number of bytes to buffer. Once the limit is exceeded, + * {@link DataBufferLimitException} is raised. + * @param buffers the data buffers that are to be composed + * @param maxByteCount the max number of bytes to buffer, or -1 for unlimited + * @return a buffer with the aggregated content, possibly an empty Mono if + * the max number of bytes to buffer is exceeded. + * @throws DataBufferLimitException if maxByteCount is exceeded + * @since 5.1.11 + */ + @SuppressWarnings("unchecked") + public static Mono join(Publisher buffers, int maxByteCount) { + Assert.notNull(buffers, "'dataBuffers' must not be null"); - return Flux.from(dataBuffers) - .collectList() + if (buffers instanceof Mono) { + return (Mono) buffers; + } + + // TODO: Drop doOnDiscard(LimitedDataBufferList.class, ...) (reactor-core#1924) + + return Flux.from(buffers) + .collect(() -> new LimitedDataBufferList(maxByteCount), LimitedDataBufferList::add) .filter(list -> !list.isEmpty()) .map(list -> list.get(0).factory().join(list)) + .doOnDiscard(LimitedDataBufferList.class, LimitedDataBufferList::releaseAndClear) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); - } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java new file mode 100644 index 00000000000..fb8c42aeeb0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java @@ -0,0 +1,157 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core.io.buffer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +import reactor.core.publisher.Flux; + +/** + * Custom {@link List} to collect data buffers with and enforce a + * limit on the total number of bytes buffered. For use with "collect" or + * other buffering operators in declarative APIs, e.g. {@link Flux}. + * + *

Adding elements increases the byte count and if the limit is exceeded, + * {@link DataBufferLimitException} is raised. {@link #clear()} resets the + * count. Remove and set are not supported. + * + *

Note: This class does not automatically release the + * buffers it contains. It is usually preferable to use hooks such as + * {@link Flux#doOnDiscard} that also take care of cancel and error signals, + * or otherwise {@link #releaseAndClear()} can be used. + * + * @author Rossen Stoyanchev + * @since 5.1.11 + */ +@SuppressWarnings("serial") +public class LimitedDataBufferList extends ArrayList { + + private final int maxByteCount; + + private int byteCount; + + + public LimitedDataBufferList(int maxByteCount) { + this.maxByteCount = maxByteCount; + } + + + @Override + public boolean add(DataBuffer buffer) { + boolean result = super.add(buffer); + if (result) { + updateCount(buffer.readableByteCount()); + } + return result; + } + + @Override + public void add(int index, DataBuffer buffer) { + super.add(index, buffer); + updateCount(buffer.readableByteCount()); + } + + @Override + public boolean addAll(Collection collection) { + boolean result = super.addAll(collection); + collection.forEach(buffer -> updateCount(buffer.readableByteCount())); + return result; + } + + @Override + public boolean addAll(int index, Collection collection) { + boolean result = super.addAll(index, collection); + collection.forEach(buffer -> updateCount(buffer.readableByteCount())); + return result; + } + + private void updateCount(int bytesToAdd) { + if (this.maxByteCount < 0) { + return; + } + if (bytesToAdd > Integer.MAX_VALUE - this.byteCount) { + raiseLimitException(); + } + else { + this.byteCount += bytesToAdd; + if (this.byteCount > this.maxByteCount) { + raiseLimitException(); + } + } + } + + private void raiseLimitException() { + // Do not release here, it's likely down via doOnDiscard.. + throw new DataBufferLimitException( + "Exceeded limit on max bytes to buffer : " + this.maxByteCount); + } + + @Override + public DataBuffer remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public DataBuffer set(int index, DataBuffer element) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + this.byteCount = 0; + super.clear(); + } + + /** + * Shortcut to {@link DataBufferUtils#release release} all data buffers and + * then {@link #clear()}. + */ + public void releaseAndClear() { + forEach(buf -> { + try { + DataBufferUtils.release(buf); + } + catch (Throwable ex) { + // Keep going.. + } + }); + clear(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java index ed10c96c835..b584080af23 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -29,6 +29,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -126,6 +127,20 @@ public void decodeNewLine() { .verify()); } + @Test + public void decodeNewLineWithLimit() { + Flux input = Flux.just( + stringBuffer("abc\n"), + stringBuffer("defg\n"), + stringBuffer("hijkl\n") + ); + this.decoder.setMaxInMemorySize(4); + + testDecode(input, String.class, step -> + step.expectNext("abc", "defg") + .verifyError(DataBufferLimitException.class)); + } + @Test public void decodeNewLineIncludeDelimiters() { this.decoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java index 115d0fce6b6..456c5881663 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java @@ -48,11 +48,14 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.support.DataBufferTestUtils; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.isA; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author Arjen Poutsma @@ -716,14 +719,25 @@ public void join() { Mono result = DataBufferUtils.join(flux); StepVerifier.create(result) - .consumeNextWith(dataBuffer -> { - assertEquals("foobarbaz", - DataBufferTestUtils.dumpString(dataBuffer, StandardCharsets.UTF_8)); - release(dataBuffer); + .consumeNextWith(buf -> { + assertEquals("foobarbaz", DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8)); + release(buf); }) .verifyComplete(); } + @Test + public void joinWithLimit() { + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + Flux flux = Flux.just(foo, bar, baz); + Mono result = DataBufferUtils.join(flux, 8); + + StepVerifier.create(result) + .verifyError(DataBufferLimitException.class); + } + @Test public void joinErrors() { DataBuffer foo = stringBuffer("foo"); diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java new file mode 100644 index 00000000000..baf348e5f95 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java @@ -0,0 +1,63 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core.io.buffer; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * Unit tests for {@link LimitedDataBufferList}. + * @author Rossen Stoyanchev + * @since 5.1.11 + */ +public class LimitedDataBufferListTests { + + private final static DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + + + @Test + public void limitEnforced() { + try { + new LimitedDataBufferList(5).add(toDataBuffer("123456")); + fail(); + } + catch (DataBufferLimitException ex) { + // Expected + } + } + + @Test + public void limitIgnored() { + new LimitedDataBufferList(-1).add(toDataBuffer("123456")); + } + + @Test + public void clearResetsCount() { + LimitedDataBufferList list = new LimitedDataBufferList(5); + list.add(toDataBuffer("12345")); + list.clear(); + list.add(toDataBuffer("12345")); + } + + + private static DataBuffer toDataBuffer(String value) { + return bufferFactory.wrap(value.getBytes(StandardCharsets.UTF_8)); + } + +} 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 17b820a9309..40a18a08d8f 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 @@ -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. @@ -143,6 +143,21 @@ interface DefaultCodecs { */ void jaxb2Encoder(Encoder encoder); + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

By default this is not set, in which case individual codec defaults + * apply. In 5.1 most codecs are not limited except {@code FormHttpMessageReader} + * which is limited to 256K. In 5.2 all codecs are limited to 256K by default. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @sine 5.1.11 + */ + void maxInMemorySize(int byteCount); /** * Whether to log form data at DEBUG level, and headers at TRACE level. * Both may contain sensitive information. diff --git a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java index 01c2d30b33b..39c75a8578b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageReader.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. @@ -30,6 +30,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.Hints; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.MediaType; @@ -62,6 +63,8 @@ public class FormHttpMessageReader extends LoggingCodecSupport private Charset defaultCharset = DEFAULT_CHARSET; + private int maxInMemorySize = 256 * 1024; + /** * Set the default character set to use for reading form data when the @@ -80,6 +83,26 @@ public Charset getDefaultCharset() { return this.defaultCharset; } + /** + * Set the max number of bytes for input form data. As form data is buffered + * before it is parsed, this helps to limit the amount of buffering. Once + * the limit is exceeded, {@link DataBufferLimitException} is raised. + *

By default this is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + @Override public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) { @@ -105,7 +128,7 @@ public Mono> readMono(ResolvableType elementType, MediaType contentType = message.getHeaders().getContentType(); Charset charset = getMediaTypeCharset(contentType); - return DataBufferUtils.join(message.getBody()) + return DataBufferUtils.join(message.getBody(), getMaxInMemorySize()) .map(buffer -> { CharBuffer charBuffer = charset.decode(buffer.asByteBuffer()); String body = charBuffer.toString(); 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 9e4350e72e7..59a209ac59a 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 @@ -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. @@ -76,6 +76,20 @@ static ServerCodecConfigurer create() { */ interface ServerDefaultCodecs extends DefaultCodecs { + /** + * Configure the {@code HttpMessageReader} to use for multipart requests. + *

By default, if + * Synchronoss NIO Multipart + * is present, this is set to + * {@link org.springframework.http.codec.multipart.MultipartHttpMessageReader + * MultipartHttpMessageReader} created with an instance of + * {@link org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader + * SynchronossPartHttpMessageReader}. + * @param reader the message reader to use for multipart requests. + * @since 5.1.11 + */ + void multipartReader(HttpMessageReader reader); + /** * Configure the {@code Encoder} to use for Server-Sent Events. *

By default if this is not set, and Jackson is available, the diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index b42e988d54d..95586080ae5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -38,6 +38,7 @@ import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.codec.HttpMessageDecoder; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -57,6 +58,9 @@ */ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder { + private int maxInMemorySize = -1; + + /** * Until https://github.com/FasterXML/jackson-core/issues/476 is resolved, * we need to ensure buffer recycling is off. @@ -74,6 +78,29 @@ protected AbstractJackson2Decoder(ObjectMapper mapper, MimeType... mimeTypes) { } + /** + * Set the max number of bytes that can be buffered by this decoder. This + * is either the size of the entire input when decoding as a whole, or the + * size of one top-level JSON object within a JSON stream. When the limit + * is exceeded, {@link DataBufferLimitException} is raised. + *

By default in 5.1 this is set to -1, unlimited. In 5.2 the default + * value for this limit is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + + @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { JavaType javaType = getObjectMapper().getTypeFactory().constructType(elementType.getType()); @@ -87,7 +114,7 @@ public Flux decode(Publisher input, ResolvableType elementTy @Nullable MimeType mimeType, @Nullable Map hints) { Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), this.jsonFactory, getObjectMapper(), true); + Flux.from(input), this.jsonFactory, getObjectMapper(), true, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints); } @@ -96,7 +123,7 @@ public Mono decodeToMono(Publisher input, ResolvableType ele @Nullable MimeType mimeType, @Nullable Map hints) { Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), this.jsonFactory, getObjectMapper(), false); + Flux.from(input), this.jsonFactory, getObjectMapper(), false, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 8549dee0bd9..606f0a34aa1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -34,6 +34,7 @@ import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; /** @@ -60,30 +61,39 @@ final class Jackson2Tokenizer { private int arrayDepth; + private final int maxInMemorySize; + + private int byteCount; + + // TODO: change to ByteBufferFeeder when supported by Jackson // See https://github.com/FasterXML/jackson-core/issues/478 private final ByteArrayFeeder inputFeeder; - private Jackson2Tokenizer( - JsonParser parser, DeserializationContext deserializationContext, boolean tokenizeArrayElements) { + private Jackson2Tokenizer(JsonParser parser, DeserializationContext deserializationContext, + boolean tokenizeArrayElements, int maxInMemorySize) { this.parser = parser; this.deserializationContext = deserializationContext; this.tokenizeArrayElements = tokenizeArrayElements; this.tokenBuffer = new TokenBuffer(parser, deserializationContext); this.inputFeeder = (ByteArrayFeeder) this.parser.getNonBlockingInputFeeder(); + this.maxInMemorySize = maxInMemorySize; } private Flux tokenize(DataBuffer dataBuffer) { + int bufferSize = dataBuffer.readableByteCount(); byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); try { this.inputFeeder.feedInput(bytes, 0, bytes.length); - return parseTokenBufferFlux(); + List result = parseTokenBufferFlux(); + assertInMemorySize(bufferSize, result); + return Flux.fromIterable(result); } catch (JsonProcessingException ex) { return Flux.error(new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex)); @@ -96,7 +106,8 @@ private Flux tokenize(DataBuffer dataBuffer) { private Flux endOfInput() { this.inputFeeder.endOfInput(); try { - return parseTokenBufferFlux(); + List result = parseTokenBufferFlux(); + return Flux.fromIterable(result); } catch (JsonProcessingException ex) { return Flux.error(new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex)); @@ -106,7 +117,7 @@ private Flux endOfInput() { } } - private Flux parseTokenBufferFlux() throws IOException { + private List parseTokenBufferFlux() throws IOException { List result = new ArrayList<>(); while (true) { @@ -124,7 +135,7 @@ private Flux parseTokenBufferFlux() throws IOException { processTokenArray(token, result); } } - return Flux.fromIterable(result); + return result; } private void updateDepth(JsonToken token) { @@ -171,18 +182,40 @@ private boolean isTopLevelArrayToken(JsonToken token) { (token == JsonToken.END_ARRAY && this.arrayDepth == 0)); } + private void assertInMemorySize(int currentBufferSize, List result) { + if (this.maxInMemorySize >= 0) { + if (!result.isEmpty()) { + this.byteCount = 0; + } + else if (currentBufferSize > Integer.MAX_VALUE - this.byteCount) { + raiseLimitException(); + } + else { + this.byteCount += currentBufferSize; + if (this.byteCount > this.maxInMemorySize) { + raiseLimitException(); + } + } + } + } + + private void raiseLimitException() { + throw new DataBufferLimitException( + "Exceeded limit on max bytes per JSON object: " + this.maxInMemorySize); + } + /** * Tokenize the given {@code Flux} into {@code Flux}. * @param dataBuffers the source data buffers * @param jsonFactory the factory to use * @param objectMapper the current mapper instance - * @param tokenizeArrayElements if {@code true} and the "top level" JSON object is + * @param tokenizeArrays if {@code true} and the "top level" JSON object is * an array, each element is returned individually immediately after it is received * @return the resulting token buffers */ public static Flux tokenize(Flux dataBuffers, JsonFactory jsonFactory, - ObjectMapper objectMapper, boolean tokenizeArrayElements) { + ObjectMapper objectMapper, boolean tokenizeArrays, int maxInMemorySize) { try { JsonParser parser = jsonFactory.createNonBlockingByteArrayParser(); @@ -191,7 +224,7 @@ public static Flux tokenize(Flux dataBuffers, JsonFacto context = ((DefaultDeserializationContext) context).createInstance( objectMapper.getDeserializationConfig(), parser, objectMapper.getInjectableValues()); } - Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrayElements); + Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrays, maxInMemorySize); return dataBuffers.flatMap(tokenizer::tokenize, Flux::error, tokenizer::endOfInput); } catch (IOException ex) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java index ffecd7c5188..3c8c4b483e7 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java @@ -65,6 +65,14 @@ public MultipartHttpMessageReader(HttpMessageReader partReader) { } + /** + * Return the configured parts reader. + * @since 5.1.11 + */ + public HttpMessageReader getPartReader() { + return this.partReader; + } + @Override public List getReadableMediaTypes() { return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index 642e16688f7..855b9fb047c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.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. @@ -40,14 +40,18 @@ import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener; import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory; import org.synchronoss.cloud.nio.stream.storage.StreamStorage; +import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.log.LogFormatUtils; @@ -69,15 +73,83 @@ * @author Sebastien Deleuze * @author Rossen Stoyanchev * @author Arjen Poutsma + * @author Brian Clozel * @since 5.0 * @see Synchronoss NIO Multipart * @see MultipartHttpMessageReader */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + // Static DataBufferFactory to copy from FileInputStream or wrap bytes[]. + private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); - private final PartBodyStreamStorageFactory streamStorageFactory = new DefaultPartBodyStreamStorageFactory(); + + private int maxInMemorySize = -1; + + private long maxDiskUsagePerPart = -1; + + private long maxParts = -1; + + + /** + * Configure the maximum amount of memory that is allowed to use per part. + * When the limit is exceeded: + *
    + *
  • file parts are written to a temporary file. + *
  • non-file parts are rejected with {@link DataBufferLimitException}. + *
+ *

By default in 5.1 this is set to -1 in which case this limit is + * not enforced and all parts may be written to disk and are limited only + * by the {@link #setMaxDiskUsagePerPart(long) maxDiskUsagePerPart} property. + * In 5.2 this default value for this limit is set to 256K. + * @param byteCount the in-memory limit in bytes, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + /** + * Get the {@link #setMaxInMemorySize configured} maximum in-memory size. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + + /** + * Configure the maximum amount of disk space allowed for file parts. + *

By default this is set to -1. + * @param maxDiskUsagePerPart the disk limit in bytes, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxDiskUsagePerPart(long maxDiskUsagePerPart) { + this.maxDiskUsagePerPart = maxDiskUsagePerPart; + } + + /** + * Get the {@link #setMaxDiskUsagePerPart configured} maximum disk usage. + * @since 5.1.11 + */ + public long getMaxDiskUsagePerPart() { + return this.maxDiskUsagePerPart; + } + + /** + * Specify the maximum number of parts allowed in a given multipart request. + * @since 5.1.11 + */ + public void setMaxParts(long maxParts) { + this.maxParts = maxParts; + } + + /** + * Return the {@link #setMaxParts configured} limit on the number of parts. + * @since 5.1.11 + */ + public long getMaxParts() { + return this.maxParts; + } @Override @@ -94,7 +166,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory)) + return Flux.create(new SynchronossPartGenerator(message)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -107,33 +179,36 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess @Override - public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); + public Mono readMono( + ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { + + return Mono.error(new UnsupportedOperationException( + "Cannot read multipart request body into single Part")); } /** - * Consume and feed input to the Synchronoss parser, then listen for parser - * output events and adapt to {@code Flux>}. + * Subscribe to the input stream and feed the Synchronoss parser. Then listen + * for parser output, creating parts, and pushing them into the FluxSink. */ - private static class SynchronossPartGenerator implements Consumer> { + private class SynchronossPartGenerator extends BaseSubscriber implements Consumer> { private final ReactiveHttpInputMessage inputMessage; - private final DataBufferFactory bufferFactory; + private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); - private final PartBodyStreamStorageFactory streamStorageFactory; + private NioMultipartParserListener listener; - SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory, - PartBodyStreamStorageFactory streamStorageFactory) { + private NioMultipartParser parser; + + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { this.inputMessage = inputMessage; - this.bufferFactory = bufferFactory; - this.streamStorageFactory = streamStorageFactory; } + @Override - public void accept(FluxSink emitter) { + public void accept(FluxSink sink) { HttpHeaders headers = this.inputMessage.getHeaders(); MediaType mediaType = headers.getContentType(); Assert.state(mediaType != null, "No content type set"); @@ -142,40 +217,57 @@ public void accept(FluxSink emitter) { Charset charset = Optional.ofNullable(mediaType.getCharset()).orElse(StandardCharsets.UTF_8); MultipartContext context = new MultipartContext(mediaType.toString(), length, charset.name()); - NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter, this.bufferFactory, context); - NioMultipartParser parser = Multipart + this.listener = new FluxSinkAdapterListener(sink, context, this.storageFactory); + + this.parser = Multipart .multipart(context) - .usePartBodyStreamStorageFactory(this.streamStorageFactory) - .forNIO(listener); - - this.inputMessage.getBody().subscribe(buffer -> { - byte[] resultBytes = new byte[buffer.readableByteCount()]; - buffer.read(resultBytes); - try { - parser.write(resultBytes); - } - catch (IOException ex) { - listener.onError("Exception thrown providing input to the parser", ex); - } - finally { - DataBufferUtils.release(buffer); - } - }, ex -> { - try { - listener.onError("Request body input error", ex); - parser.close(); - } - catch (IOException ex2) { - listener.onError("Exception thrown while closing the parser", ex2); - } - }, () -> { - try { - parser.close(); - } - catch (IOException ex) { - listener.onError("Exception thrown while closing the parser", ex); - } - }); + .usePartBodyStreamStorageFactory(this.storageFactory) + .forNIO(this.listener); + + this.inputMessage.getBody().subscribe(this); + } + + @Override + protected void hookOnNext(DataBuffer buffer) { + int size = buffer.readableByteCount(); + this.storageFactory.increaseByteCount(size); + byte[] resultBytes = new byte[size]; + buffer.read(resultBytes); + try { + this.parser.write(resultBytes); + } + catch (IOException ex) { + cancel(); + int index = this.storageFactory.getCurrentPartIndex(); + this.listener.onError("Parser error for part [" + index + "]", ex); + } + finally { + DataBufferUtils.release(buffer); + } + } + + @Override + protected void hookOnError(Throwable ex) { + try { + this.parser.close(); + } + catch (IOException ex2) { + // ignore + } + finally { + int index = this.storageFactory.getCurrentPartIndex(); + this.listener.onError("Failure while parsing part[" + index + "]", ex); + } + } + + @Override + protected void hookFinally(SignalType type) { + try { + this.parser.close(); + } + catch (IOException ex) { + this.listener.onError("Error while closing parser", ex); + } } private int getContentLength(HttpHeaders headers) { @@ -186,6 +278,54 @@ private int getContentLength(HttpHeaders headers) { } + private class LimitedPartBodyStreamStorageFactory implements PartBodyStreamStorageFactory { + + private final PartBodyStreamStorageFactory storageFactory = maxInMemorySize > 0 ? + new DefaultPartBodyStreamStorageFactory(maxInMemorySize) : + new DefaultPartBodyStreamStorageFactory(); + + private int index = 1; + + private boolean isFilePart; + + private long partSize; + + + public int getCurrentPartIndex() { + return this.index; + } + + @Override + public StreamStorage newStreamStorageForPartBody(Map> headers, int index) { + this.index = index; + this.isFilePart = (MultipartUtils.getFileName(headers) != null); + this.partSize = 0; + if (maxParts > 0 && index > maxParts) { + throw new DecodingException("Too many parts (" + index + " allowed)"); + } + return this.storageFactory.newStreamStorageForPartBody(headers, index); + } + + public void increaseByteCount(long byteCount) { + this.partSize += byteCount; + if (maxInMemorySize > 0 && !this.isFilePart && this.partSize >= maxInMemorySize) { + throw new DataBufferLimitException("Part[" + this.index + "] " + + "exceeded the in-memory limit of " + maxInMemorySize + " bytes"); + } + if (maxDiskUsagePerPart > 0 && this.isFilePart && this.partSize > maxDiskUsagePerPart) { + throw new DecodingException("Part[" + this.index + "] " + + "exceeded the disk usage limit of " + maxDiskUsagePerPart + " bytes"); + } + } + + public void partFinished() { + this.index++; + this.isFilePart = false; + this.partSize = 0; + } + } + + /** * Listen for parser output and adapt to {@code Flux>}. */ @@ -193,43 +333,48 @@ private static class FluxSinkAdapterListener implements NioMultipartParserListen private final FluxSink sink; - private final DataBufferFactory bufferFactory; - private final MultipartContext context; + private final LimitedPartBodyStreamStorageFactory storageFactory; + private final AtomicInteger terminated = new AtomicInteger(0); - FluxSinkAdapterListener(FluxSink sink, DataBufferFactory factory, MultipartContext context) { + + FluxSinkAdapterListener( + FluxSink sink, MultipartContext context, LimitedPartBodyStreamStorageFactory factory) { + this.sink = sink; - this.bufferFactory = factory; this.context = context; + this.storageFactory = factory; } + @Override public void onPartFinished(StreamStorage storage, Map> headers) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(headers); + this.storageFactory.partFinished(); this.sink.next(createPart(storage, httpHeaders)); } private Part createPart(StreamStorage storage, HttpHeaders httpHeaders) { String filename = MultipartUtils.getFileName(httpHeaders); if (filename != null) { - return new SynchronossFilePart(httpHeaders, filename, storage, this.bufferFactory); + return new SynchronossFilePart(httpHeaders, filename, storage); } else if (MultipartUtils.isFormField(httpHeaders, this.context)) { String value = MultipartUtils.readFormParameterValue(storage, httpHeaders); - return new SynchronossFormFieldPart(httpHeaders, this.bufferFactory, value); + return new SynchronossFormFieldPart(httpHeaders, value); } else { - return new SynchronossPart(httpHeaders, storage, this.bufferFactory); + return new SynchronossPart(httpHeaders, storage); } } @Override public void onError(String message, Throwable cause) { if (this.terminated.getAndIncrement() == 0) { - this.sink.error(new RuntimeException(message, cause)); + this.sink.error(new DecodingException(message, cause)); } } @@ -256,14 +401,10 @@ private abstract static class AbstractSynchronossPart implements Part { private final HttpHeaders headers; - private final DataBufferFactory bufferFactory; - - AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) { + AbstractSynchronossPart(HttpHeaders headers) { Assert.notNull(headers, "HttpHeaders is required"); - Assert.notNull(bufferFactory, "DataBufferFactory is required"); this.name = MultipartUtils.getFieldName(headers); this.headers = headers; - this.bufferFactory = bufferFactory; } @Override @@ -276,10 +417,6 @@ public HttpHeaders headers() { return this.headers; } - DataBufferFactory getBufferFactory() { - return this.bufferFactory; - } - @Override public String toString() { return "Part '" + this.name + "', headers=" + this.headers; @@ -291,15 +428,15 @@ private static class SynchronossPart extends AbstractSynchronossPart { private final StreamStorage storage; - SynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) { - super(headers, factory); + SynchronossPart(HttpHeaders headers, StreamStorage storage) { + super(headers); Assert.notNull(storage, "StreamStorage is required"); this.storage = storage; } @Override public Flux content() { - return DataBufferUtils.readInputStream(getStorage()::getInputStream, getBufferFactory(), 4096); + return DataBufferUtils.readInputStream(getStorage()::getInputStream, bufferFactory, 4096); } protected StreamStorage getStorage() { @@ -315,8 +452,8 @@ private static class SynchronossFilePart extends SynchronossPart implements File private final String filename; - SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage, DataBufferFactory factory) { - super(headers, storage, factory); + SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage) { + super(headers, storage); this.filename = filename; } @@ -375,8 +512,8 @@ private static class SynchronossFormFieldPart extends AbstractSynchronossPart im private final String content; - SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) { - super(headers, bufferFactory); + SynchronossFormFieldPart(HttpHeaders headers, String content) { + super(headers); this.content = content; } @@ -388,9 +525,7 @@ public String value() { @Override public Flux content() { byte[] bytes = this.content.getBytes(getCharset()); - DataBuffer buffer = getBufferFactory().allocateBuffer(bytes.length); - buffer.write(bytes); - return Flux.just(buffer); + return Flux.just(bufferFactory.wrap(bytes)); } private Charset getCharset() { diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java index c1f0e17bf7b..a2aba3addd5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java @@ -36,6 +36,7 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -101,10 +102,24 @@ public ProtobufDecoder(ExtensionRegistry extensionRegistry) { } + /** + * The max size allowed per message. + *

By default in 5.1 this is set to 64K. In 5.2 the default for this limit + * is set to 256K. + * @param maxMessageSize the max size per message, or -1 for unlimited + */ public void setMaxMessageSize(int maxMessageSize) { this.maxMessageSize = maxMessageSize; } + /** + * Return the {@link #setMaxMessageSize configured} message size limit. + * @since 5.1.11 + */ + public int getMaxMessageSize() { + return this.maxMessageSize; + } + @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { @@ -127,7 +142,7 @@ public Flux decode(Publisher inputStream, ResolvableType el public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - return DataBufferUtils.join(inputStream).map(dataBuffer -> { + return DataBufferUtils.join(inputStream, getMaxMessageSize()).map(dataBuffer -> { try { Message.Builder builder = getMessageBuilder(elementType.toClass()); ByteBuffer buffer = dataBuffer.asByteBuffer(); @@ -198,9 +213,9 @@ public Iterable apply(DataBuffer input) { if (!readMessageSize(input)) { return messages; } - if (this.messageBytesToRead > this.maxMessageSize) { - throw new DecodingException( - "The number of bytes to read from the incoming stream " + + if (this.maxMessageSize > 0 && this.messageBytesToRead > this.maxMessageSize) { + throw new DataBufferLimitException( + "The number of bytes to read for message " + "(" + this.messageBytesToRead + ") exceeds " + "the configured limit (" + this.maxMessageSize + ")"); } 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 055a0ed29b5..f3034ad9354 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 @@ -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. @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; +import org.springframework.core.codec.AbstractDataBufferDecoder; import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.ByteBufferDecoder; @@ -38,6 +39,7 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.json.AbstractJackson2Decoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; @@ -95,6 +97,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { @Nullable private Encoder jaxb2Encoder; + @Nullable + private Integer maxInMemorySize; + private boolean enableLoggingRequestDetails = false; private boolean registerDefaults = true; @@ -130,6 +135,16 @@ public void jaxb2Encoder(Encoder encoder) { this.jaxb2Encoder = encoder; } + @Override + public void maxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + @Nullable + protected Integer maxInMemorySize() { + return this.maxInMemorySize; + } + @Override public void enableLoggingRequestDetails(boolean enable) { this.enableLoggingRequestDetails = enable; @@ -155,17 +170,20 @@ final List> getTypedReaders() { return Collections.emptyList(); } List> readers = new ArrayList<>(); - readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); - readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); - readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder())); - readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder())); - readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly())); + readers.add(new DecoderHttpMessageReader<>(init(new ByteArrayDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(new ByteBufferDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(new DataBufferDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(new ResourceDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(StringDecoder.textPlainOnly()))); if (protobufPresent) { - Decoder decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder(); + Decoder decoder = this.protobufDecoder != null ? this.protobufDecoder : init(new ProtobufDecoder()); readers.add(new DecoderHttpMessageReader<>(decoder)); } FormHttpMessageReader formReader = new FormHttpMessageReader(); + if (this.maxInMemorySize != null) { + formReader.setMaxInMemorySize(this.maxInMemorySize); + } formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails); readers.add(formReader); @@ -174,6 +192,28 @@ final List> getTypedReaders() { return readers; } + private > T init(T decoder) { + if (this.maxInMemorySize != null) { + if (decoder instanceof AbstractDataBufferDecoder) { + ((AbstractDataBufferDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + } + if (decoder instanceof ProtobufDecoder) { + ((ProtobufDecoder) decoder).setMaxMessageSize(this.maxInMemorySize); + } + if (jackson2Present) { + if (decoder instanceof AbstractJackson2Decoder) { + ((AbstractJackson2Decoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + } + } + if (jaxb2Present) { + if (decoder instanceof Jaxb2XmlDecoder) { + ((Jaxb2XmlDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + } + } + } + return decoder; + } + /** * Hook for client or server specific typed readers. */ @@ -189,13 +229,13 @@ final List> getObjectReaders() { } List> readers = new ArrayList<>(); if (jackson2Present) { - readers.add(new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); + readers.add(new DecoderHttpMessageReader<>(init(getJackson2JsonDecoder()))); } if (jackson2SmilePresent) { - readers.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder())); + readers.add(new DecoderHttpMessageReader<>(init(new Jackson2SmileDecoder()))); } if (jaxb2Present) { - Decoder decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder(); + Decoder decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : init(new Jaxb2XmlDecoder()); readers.add(new DecoderHttpMessageReader<>(decoder)); } extendObjectReaders(readers); @@ -216,7 +256,7 @@ final List> getCatchAllReaders() { return Collections.emptyList(); } List> result = new ArrayList<>(); - result.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + result.add(new DecoderHttpMessageReader<>(init(StringDecoder.allMimeTypes()))); return result; } 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 15461d11f48..37e924cd7e9 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 @@ -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. @@ -39,10 +39,18 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo DefaultServerCodecConfigurer.class.getClassLoader()); + @Nullable + private HttpMessageReader multipartReader; + @Nullable private Encoder sseEncoder; + @Override + public void multipartReader(HttpMessageReader reader) { + this.multipartReader = reader; + } + @Override public void serverSentEventEncoder(Encoder encoder) { this.sseEncoder = encoder; @@ -51,10 +59,18 @@ public void serverSentEventEncoder(Encoder encoder) { @Override protected void extendTypedReaders(List> typedReaders) { + if (this.multipartReader != null) { + typedReaders.add(this.multipartReader); + return; + } if (synchronossMultipartPresent) { boolean enable = isEnableLoggingRequestDetails(); SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader(); + Integer size = maxInMemorySize(); + if (size != null) { + partReader.setMaxInMemorySize(size); + } partReader.setEnableLoggingRequestDetails(enable); typedReaders.add(partReader); diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java index 1b87b0e6d5a..7fd886fb0dd 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java @@ -43,6 +43,7 @@ import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.log.LogFormatUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -78,6 +79,8 @@ public class Jaxb2XmlDecoder extends AbstractDecoder { private Function unmarshallerProcessor = Function.identity(); + private int maxInMemorySize = -1; + public Jaxb2XmlDecoder() { super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); @@ -110,6 +113,29 @@ public Function getUnmarshallerProcessor() { return this.unmarshallerProcessor; } + /** + * Set the max number of bytes that can be buffered by this decoder. + * This is either the size of the entire input when decoding as a whole, or when + * using async parsing with Aalto XML, it is the size of one top-level XML tree. + * When the limit is exceeded, {@link DataBufferLimitException} is raised. + *

By default in 5.1 this is set to -1, unlimited. In 5.2 the default + * value for this limit is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + this.xmlEventDecoder.setMaxInMemorySize(byteCount); + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java index 5f1665399f9..46d3c4e1391 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java @@ -40,6 +40,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -88,26 +89,51 @@ public class XmlEventDecoder extends AbstractDecoder { boolean useAalto = aaltoPresent; + private int maxInMemorySize = -1; + public XmlEventDecoder() { super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } + /** + * Set the max number of bytes that can be buffered by this decoder. This + * is either the size the entire input when decoding as a whole, or when + * using async parsing via Aalto XML, it is size one top-level XML tree. + * When the limit is exceeded, {@link DataBufferLimitException} is raised. + *

By default in 5.1 this is set to -1, unlimited. In 5.2 the default + * value for this limit is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + + @Override @SuppressWarnings({"rawtypes", "unchecked"}) // on JDK 9 where XMLEventReader is Iterator public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { if (this.useAalto) { - AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(); + AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(this.maxInMemorySize); return Flux.from(input) .flatMapIterable(mapper) .doFinally(signalType -> mapper.endOfInput()); } else { - return DataBufferUtils.join(input). - flatMapIterable(buffer -> { + return DataBufferUtils.join(input, getMaxInMemorySize()) + .flatMapIterable(buffer -> { try { InputStream is = buffer.asInputStream(); Iterator eventReader = inputFactory.createXMLEventReader(is); @@ -139,10 +165,22 @@ private static class AaltoDataBufferToXmlEvent implements Function apply(DataBuffer dataBuffer) { try { + increaseByteCount(dataBuffer); this.streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer()); List events = new ArrayList<>(); while (true) { @@ -156,8 +194,12 @@ public List apply(DataBuffer dataBuffer) { if (event.isEndDocument()) { break; } + checkDepthAndResetByteCount(event); } } + if (this.maxInMemorySize > 0 && this.byteCount > this.maxInMemorySize) { + raiseLimitException(); + } return events; } catch (XMLStreamException ex) { @@ -168,6 +210,35 @@ public List apply(DataBuffer dataBuffer) { } } + private void increaseByteCount(DataBuffer dataBuffer) { + if (this.maxInMemorySize > 0) { + if (dataBuffer.readableByteCount() > Integer.MAX_VALUE - this.byteCount) { + raiseLimitException(); + } + else { + this.byteCount += dataBuffer.readableByteCount(); + } + } + } + + private void checkDepthAndResetByteCount(XMLEvent event) { + if (this.maxInMemorySize > 0) { + if (event.isStartElement()) { + this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount; + this.elementDepth++; + } + else if (event.isEndElement()) { + this.elementDepth--; + this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount; + } + } + } + + private void raiseLimitException() { + throw new DataBufferLimitException( + "Exceeded limit on max bytes per XML top-level node: " + this.maxInMemorySize); + } + public void endOfInput() { this.streamReader.getInputFeeder().endOfInput(); } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index 6e9ef255691..5c08550c07e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -20,7 +20,6 @@ import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.function.Consumer; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.TreeNode; @@ -36,6 +35,7 @@ import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; import static java.util.Arrays.*; import static java.util.Collections.*; @@ -181,11 +181,68 @@ public void tokenizeArrayElements() { testTokenize(asList("[1", ",2,", "3]"), asList("1", "2", "3"), true); } + private void testTokenize(List input, List output, boolean tokenize) { + StepVerifier.FirstStep builder = StepVerifier.create(decode(input, tokenize, -1)); + output.forEach(expected -> builder.assertNext(actual -> { + try { + JSONAssert.assertEquals(expected, actual, true); + } + catch (JSONException ex) { + throw new RuntimeException(ex); + } + })); + builder.verifyComplete(); + } + + @Test + public void testLimit() { + + List source = asList("[", + "{", "\"id\":1,\"name\":\"Dan\"", "},", + "{", "\"id\":2,\"name\":\"Ron\"", "},", + "{", "\"id\":3,\"name\":\"Bartholomew\"", "}", + "]"); + + String expected = String.join("", source); + int maxInMemorySize = expected.length(); + + StepVerifier.create(decode(source, false, maxInMemorySize)) + .expectNext(expected) + .verifyComplete(); + + StepVerifier.create(decode(source, false, maxInMemorySize - 1)) + .expectError(DataBufferLimitException.class); + } + + @Test + public void testLimitTokenized() { + + List source = asList("[", + "{", "\"id\":1, \"name\":\"Dan\"", "},", + "{", "\"id\":2, \"name\":\"Ron\"", "},", + "{", "\"id\":3, \"name\":\"Bartholomew\"", "}", + "]"); + + String expected = "{\"id\":3,\"name\":\"Bartholomew\"}"; + int maxInMemorySize = expected.length(); + + StepVerifier.create(decode(source, true, maxInMemorySize)) + .expectNext("{\"id\":1,\"name\":\"Dan\"}") + .expectNext("{\"id\":2,\"name\":\"Ron\"}") + .expectNext(expected) + .verifyComplete(); + + StepVerifier.create(decode(source, true, maxInMemorySize - 1)) + .expectNext("{\"id\":1,\"name\":\"Dan\"}") + .expectNext("{\"id\":2,\"name\":\"Ron\"}") + .verifyError(DataBufferLimitException.class); + } + @Test public void errorInStream() { DataBuffer buffer = stringBuffer("{\"id\":1,\"name\":"); Flux source = Flux.just(buffer).concatWith(Flux.error(new RuntimeException())); - Flux result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true); + Flux result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true, -1); StepVerifier.create(result) .expectError(RuntimeException.class) @@ -195,7 +252,7 @@ public void errorInStream() { @Test // SPR-16521 public void jsonEOFExceptionIsWrappedAsDecodingError() { Flux source = Flux.just(stringBuffer("{\"status\": \"noClosingQuote}")); - Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false); + Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1); StepVerifier.create(tokens) .expectError(DecodingException.class) @@ -203,12 +260,13 @@ public void jsonEOFExceptionIsWrappedAsDecodingError() { } - private void testTokenize(List source, List expected, boolean tokenizeArrayElements) { + private Flux decode(List source, boolean tokenize, int maxInMemorySize) { + Flux tokens = Jackson2Tokenizer.tokenize( Flux.fromIterable(source).map(this::stringBuffer), - this.jsonFactory, this.objectMapper, tokenizeArrayElements); + this.jsonFactory, this.objectMapper, tokenize, maxInMemorySize); - Flux result = tokens + return tokens .map(tokenBuffer -> { try { TreeNode root = this.objectMapper.readTree(tokenBuffer.asParser()); @@ -218,10 +276,6 @@ private void testTokenize(List source, List expected, boolean to throw new UncheckedIOException(ex); } }); - - StepVerifier.FirstStep builder = StepVerifier.create(result); - expected.forEach(s -> builder.assertNext(new JSONAssertConsumer(s))); - builder.verifyComplete(); } private DataBuffer stringBuffer(String value) { @@ -231,24 +285,4 @@ private DataBuffer stringBuffer(String value) { return buffer; } - - private static class JSONAssertConsumer implements Consumer { - - private final String expected; - - JSONAssertConsumer(String expected) { - this.expected = expected; - } - - @Override - public void accept(String s) { - try { - JSONAssert.assertEquals(this.expected, s, true); - } - catch (JSONException ex) { - throw new RuntimeException(ex); - } - } - } - } diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java index d5052aa24e6..74d3fb0db0c 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.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. @@ -17,15 +17,20 @@ package org.springframework.http.codec.multipart; import java.io.File; +import java.io.IOException; import java.time.Duration; import java.util.Map; +import java.util.function.Consumer; import org.junit.Test; +import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; @@ -38,23 +43,31 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.util.MultiValueMap; -import static java.util.Collections.*; -import static org.junit.Assert.*; -import static org.springframework.core.ResolvableType.*; -import static org.springframework.http.HttpHeaders.*; -import static org.springframework.http.MediaType.*; +import static java.util.Collections.emptyMap; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClassWithGenerics; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; /** * Unit tests for {@link SynchronossPartHttpMessageReader}. * * @author Sebastien Deleuze * @author Rossen Stoyanchev + * @author Brian Clozel */ public class SynchronossPartHttpMessageReaderTests { private final MultipartHttpMessageReader reader = new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader()); + private static final ResolvableType PARTS_ELEMENT_TYPE = + forClassWithGenerics(MultiValueMap.class, String.class, Part.class); @Test public void canRead() { @@ -86,10 +99,10 @@ public void resolveParts() { MultiValueMap parts = this.reader.readMono(elementType, request, emptyMap()).block(); assertEquals(2, parts.size()); - assertTrue(parts.containsKey("fooPart")); - Part part = parts.getFirst("fooPart"); + assertTrue(parts.containsKey("filePart")); + Part part = parts.getFirst("filePart"); assertTrue(part instanceof FilePart); - assertEquals("fooPart", part.name()); + assertEquals("filePart", part.name()); assertEquals("foo.txt", ((FilePart) part).filename()); DataBuffer buffer = DataBufferUtils.join(part.content()).block(); assertEquals(12, buffer.readableByteCount()); @@ -97,24 +110,23 @@ public void resolveParts() { buffer.read(byteContent); assertEquals("Lorem Ipsum.", new String(byteContent)); - assertTrue(parts.containsKey("barPart")); - part = parts.getFirst("barPart"); + assertTrue(parts.containsKey("textPart")); + part = parts.getFirst("textPart"); assertTrue(part instanceof FormFieldPart); - assertEquals("barPart", part.name()); - assertEquals("bar", ((FormFieldPart) part).value()); + assertEquals("textPart", part.name()); + assertEquals("sample-text", ((FormFieldPart) part).value()); } @Test // SPR-16545 - public void transferTo() { + public void transferTo() throws IOException { ServerHttpRequest request = generateMultipartRequest(); - ResolvableType elementType = forClassWithGenerics(MultiValueMap.class, String.class, Part.class); - MultiValueMap parts = this.reader.readMono(elementType, request, emptyMap()).block(); + MultiValueMap parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap()).block(); assertNotNull(parts); - FilePart part = (FilePart) parts.getFirst("fooPart"); + FilePart part = (FilePart) parts.getFirst("filePart"); assertNotNull(part); - File dest = new File(System.getProperty("java.io.tmpdir") + "/" + part.filename()); + File dest = File.createTempFile(part.filename(), "multipart"); part.transferTo(dest).block(Duration.ofSeconds(5)); assertTrue(dest.exists()); @@ -125,22 +137,65 @@ public void transferTo() { @Test public void bodyError() { ServerHttpRequest request = generateErrorMultipartRequest(); - ResolvableType elementType = forClassWithGenerics(MultiValueMap.class, String.class, Part.class); - StepVerifier.create(this.reader.readMono(elementType, request, emptyMap())).verifyError(); + StepVerifier.create(this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap())).verifyError(); + } + + @Test + public void readPartsWithoutDemand() { + ServerHttpRequest request = generateMultipartRequest(); + Mono> parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap()); + ZeroDemandSubscriber subscriber = new ZeroDemandSubscriber(); + parts.subscribe(subscriber); + subscriber.cancel(); } + @Test + public void readTooManyParts() { + testMultipartExceptions(reader -> reader.setMaxParts(1), ex -> { + assertEquals(DecodingException.class, ex.getClass()); + assertThat(ex.getMessage(), startsWith("Failure while parsing part[2]")); + assertEquals("Too many parts (2 allowed)", ex.getCause().getMessage()); + }); + } - private ServerHttpRequest generateMultipartRequest() { + @Test + public void readFilePartTooBig() { + testMultipartExceptions(reader -> reader.setMaxDiskUsagePerPart(5), ex -> { + assertEquals(DecodingException.class, ex.getClass()); + assertThat(ex.getMessage(), startsWith("Failure while parsing part[1]")); + assertEquals("Part[1] exceeded the disk usage limit of 5 bytes", ex.getCause().getMessage()); + }); + } + @Test + public void readPartHeadersTooBig() { + testMultipartExceptions(reader -> reader.setMaxInMemorySize(1), ex -> { + assertEquals(DecodingException.class, ex.getClass()); + assertThat(ex.getMessage(), startsWith("Failure while parsing part[1]")); + assertEquals("Part[1] exceeded the in-memory limit of 1 bytes", ex.getCause().getMessage()); + }); + } + + private void testMultipartExceptions( + Consumer configurer, Consumer assertions) { + + SynchronossPartHttpMessageReader reader = new SynchronossPartHttpMessageReader(); + configurer.accept(reader); + MultipartHttpMessageReader multipartReader = new MultipartHttpMessageReader(reader); + StepVerifier.create(multipartReader.readMono(PARTS_ELEMENT_TYPE, generateMultipartRequest(), emptyMap())) + .consumeErrorWith(assertions) + .verify(); + } + + private ServerHttpRequest generateMultipartRequest() { MultipartBodyBuilder partsBuilder = new MultipartBodyBuilder(); - partsBuilder.part("fooPart", new ClassPathResource("org/springframework/http/codec/multipart/foo.txt")); - partsBuilder.part("barPart", "bar"); + partsBuilder.part("filePart", new ClassPathResource("org/springframework/http/codec/multipart/foo.txt")); + partsBuilder.part("textPart", "sample-text"); MockClientHttpRequest outputMessage = new MockClientHttpRequest(HttpMethod.POST, "/"); new MultipartHttpMessageWriter() .write(Mono.just(partsBuilder.build()), null, MediaType.MULTIPART_FORM_DATA, outputMessage, null) .block(Duration.ofSeconds(5)); - return MockServerHttpRequest.post("/") .contentType(outputMessage.getHeaders().getContentType()) .body(outputMessage.getBody()); @@ -152,4 +207,12 @@ private ServerHttpRequest generateErrorMultipartRequest() { .body(Flux.just(new DefaultDataBufferFactory().wrap("invalid content".getBytes()))); } + private static class ZeroDemandSubscriber extends BaseSubscriber> { + + @Override + protected void hookOnSubscribe(Subscription subscription) { + // Just subscribe without requesting + } + } + } 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 4416507571a..b36cdd0ca7d 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 @@ -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. @@ -124,13 +124,41 @@ public void jackson2EncoderOverride() { .filter(e -> e == encoder).orElse(null)); } + @Test + public void maxInMemorySize() { + int size = 99; + this.configurer.defaultCodecs().maxInMemorySize(size); + List> readers = this.configurer.getReaders(); + assertEquals(13, readers.size()); + assertEquals(size, ((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ResourceDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()); + assertEquals(size, ((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()); + assertEquals(size, ((SynchronossPartHttpMessageReader) nextReader(readers)).getMaxInMemorySize()); + + MultipartHttpMessageReader multipartReader = (MultipartHttpMessageReader) nextReader(readers); + SynchronossPartHttpMessageReader reader = (SynchronossPartHttpMessageReader) multipartReader.getPartReader(); + assertEquals(size, (reader).getMaxInMemorySize()); + + assertEquals(size, ((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + } private Decoder getNextDecoder(List> readers) { - HttpMessageReader reader = readers.get(this.index.getAndIncrement()); + HttpMessageReader reader = nextReader(readers); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); return ((DecoderHttpMessageReader) reader).getDecoder(); } + private HttpMessageReader nextReader(List> readers) { + return readers.get(this.index.getAndIncrement()); + } + private Encoder getNextEncoder(List> writers) { HttpMessageWriter writer = writers.get(this.index.getAndIncrement()); assertEquals(EncoderHttpMessageWriter.class, writer.getClass()); diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java index 8babb0b6147..5e4c3941453 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.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. @@ -28,8 +28,10 @@ import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @author Arjen Poutsma @@ -44,11 +46,12 @@ public class XmlEventDecoderTests extends AbstractLeakCheckingTestCase { private XmlEventDecoder decoder = new XmlEventDecoder(); + @Test public void toXMLEventsAalto() { Flux events = - this.decoder.decode(stringBuffer(XML), null, null, Collections.emptyMap()); + this.decoder.decode(stringBufferMono(XML), null, null, Collections.emptyMap()); StepVerifier.create(events) .consumeNextWith(e -> assertTrue(e.isStartDocument())) @@ -69,7 +72,7 @@ public void toXMLEventsNonAalto() { decoder.useAalto = false; Flux events = - this.decoder.decode(stringBuffer(XML), null, null, Collections.emptyMap()); + this.decoder.decode(stringBufferMono(XML), null, null, Collections.emptyMap()); StepVerifier.create(events) .consumeNextWith(e -> assertTrue(e.isStartDocument())) @@ -86,10 +89,32 @@ public void toXMLEventsNonAalto() { .verify(); } + @Test + public void toXMLEventsWithLimit() { + + this.decoder.setMaxInMemorySize(6); + + Flux source = Flux.just( + "", "", "foofoo", "", "", "barbarbar", "", ""); + + Flux events = this.decoder.decode( + source.map(this::stringBuffer), null, null, Collections.emptyMap()); + + StepVerifier.create(events) + .consumeNextWith(e -> assertTrue(e.isStartDocument())) + .consumeNextWith(e -> assertStartElement(e, "pojo")) + .consumeNextWith(e -> assertStartElement(e, "foo")) + .consumeNextWith(e -> assertCharacters(e, "foofoo")) + .consumeNextWith(e -> assertEndElement(e, "foo")) + .consumeNextWith(e -> assertStartElement(e, "bar")) + .expectError(DataBufferLimitException.class) + .verify(); + } + @Test public void decodeErrorAalto() { Flux source = Flux.concat( - stringBuffer(""), + stringBufferMono(""), Flux.error(new RuntimeException())); Flux events = @@ -107,7 +132,7 @@ public void decodeErrorNonAalto() { decoder.useAalto = false; Flux source = Flux.concat( - stringBuffer(""), + stringBufferMono(""), Flux.error(new RuntimeException())); Flux events = @@ -133,13 +158,15 @@ private static void assertCharacters(XMLEvent event, String expectedData) { assertEquals(expectedData, event.asCharacters().getData()); } - private Mono stringBuffer(String value) { - return Mono.defer(() -> { - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); - buffer.write(bytes); - return Mono.just(buffer); - }); + private DataBuffer stringBuffer(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + } + + private Mono stringBufferMono(String value) { + return Mono.defer(() -> Mono.just(stringBuffer(value))); } } diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 330ef756351..5789f295273 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -761,6 +761,33 @@ for repeated, map-like access to parts, or otherwise rely on the `SynchronossPartHttpMessageReader` for a one-time access to `Flux`. +[[webflux-codecs-limits]] +==== Limits + +`Decoder` and `HttpMessageReader` implementations that buffer some or all of the input +stream can be configured with a limit on the maximum number of bytes to buffer in memory. +In some cases buffering occurs because input is aggregated and represented as a single +object, e.g. controller method with `@RequestBody byte[]`, `x-www-form-urlencoded` data, +and so on. Buffering can also occurs with streaming, when splitting the input stream, +e.g. delimited text, a stream of JSON objects, and so on. For those streaming cases, the +limit applies to the number of bytes associted with one object in the stream. + +To configure buffer sizes, you can check if a given `Decoder` or `HttpMessageReader` +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. + +For <> the `maxInMemorySize` property limits +the size of non-file parts. For file parts it determines the threshold at which the part +is written to disk. For file parts written to disk, there is an additional +`maxDiskUsagePerPart` property to limit the amount of disk space per part. There is also +a `maxParts` property to limit the overall number of parts in a multipart request. +To configure all 3 in WebFlux, you'll need to supply a pre-configured instance of +`MultipartHttpMessageReader` to `ServerCodecConfigurer`. + + + [[webflux-codecs-streaming]] ==== Streaming [.small]#<># From ceb881ab05a407f410b3c11603aacb092080cf2b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 29 Oct 2019 13:32:18 +0100 Subject: [PATCH 002/322] Ignore projects not in this branch --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index a9136e7ebca..7a0dae433db 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ buildSrc/build /src/asciidoc/build target/ +# Projects not in this branch +integration-tests/ +spring-core/kotlin-coroutines/ + # Eclipse artifacts, including WTP generated manifests .classpath .project From 3814f12b67088db3e6c9da61809415b405b709ef Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Tue, 29 Oct 2019 13:45:20 +0100 Subject: [PATCH 003/322] Preserve expires attribute in MockCookie At present, MockCookie doesn't preserve expires attribute. This has a consequence that a cookie value set using MockHttpServletResponse#addHeader containing an expires attribute will not match the cookie value obtained from MockHttpServletResponse#getHeader, since the expires attribute will get calculated based on current time. This commit enhances MockCookie to preserve the expires attribute. Closes gh-23769 --- .../springframework/mock/web/MockCookie.java | 24 +++++++++++++++++++ .../mock/web/MockHttpServletResponse.java | 12 +++++++--- .../mock/web/MockCookieTests.java | 17 +++++++++---- .../web/MockHttpServletResponseTests.java | 9 +++++++ .../mock/web/test/MockCookie.java | 24 +++++++++++++++++++ .../web/test/MockHttpServletResponse.java | 12 +++++++--- 6 files changed, 87 insertions(+), 11 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java index 522a60038cb..a7565f180e7 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java @@ -16,6 +16,9 @@ package org.springframework.mock.web; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + import javax.servlet.http.Cookie; import org.springframework.lang.Nullable; @@ -35,6 +38,9 @@ public class MockCookie extends Cookie { private static final long serialVersionUID = 4312531139502726325L; + @Nullable + private ZonedDateTime expires; + @Nullable private String sameSite; @@ -49,6 +55,20 @@ public MockCookie(String name, String value) { super(name, value); } + /** + * Add the "Expires" attribute to the cookie. + */ + public void setExpires(@Nullable ZonedDateTime expires) { + this.expires = expires; + } + + /** + * Return the "Expires" attribute, or {@code null} if not set. + */ + @Nullable + public ZonedDateTime getExpires() { + return this.expires; + } /** * Add the "SameSite" attribute to the cookie. @@ -94,6 +114,10 @@ public static MockCookie parse(String setCookieHeader) { else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) { cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader))); } + else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) { + cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), + DateTimeFormatter.RFC_1123_DATE_TIME)); + } else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) { cookie.setPath(extractAttributeValue(attribute, setCookieHeader)); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 87b7c28db12..b933ba14bf3 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -26,6 +26,7 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -345,9 +346,14 @@ private String getCookieHeader(Cookie cookie) { if (maxAge >= 0) { buf.append("; Max-Age=").append(maxAge); buf.append("; Expires="); - HttpHeaders headers = new HttpHeaders(); - headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0); - buf.append(headers.getFirst(HttpHeaders.EXPIRES)); + if (cookie instanceof MockCookie && ((MockCookie) cookie).getExpires() != null) { + buf.append(((MockCookie) cookie).getExpires().format(DateTimeFormatter.RFC_1123_DATE_TIME)); + } + else { + HttpHeaders headers = new HttpHeaders(); + headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0); + buf.append(headers.getFirst(HttpHeaders.EXPIRES)); + } } if (cookie.getSecure()) { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java index 3180369f0a4..3253c86d329 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java @@ -20,6 +20,9 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + import static org.junit.Assert.*; /** @@ -67,8 +70,8 @@ public void parseHeaderWithoutAttributes() { @Test public void parseHeaderWithAttributes() { - MockCookie cookie = MockCookie.parse( - "SESSION=123; Domain=example.com; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax"); + MockCookie cookie = MockCookie.parse("SESSION=123; Domain=example.com; Max-Age=60; " + + "Expires=Tue, 8 Oct 2019 19:50:00 GMT; Path=/; Secure; HttpOnly; SameSite=Lax"); assertCookie(cookie, "SESSION", "123"); assertEquals("example.com", cookie.getDomain()); @@ -76,6 +79,8 @@ public void parseHeaderWithAttributes() { assertEquals("/", cookie.getPath()); assertTrue(cookie.getSecure()); assertTrue(cookie.isHttpOnly()); + assertEquals(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT", + DateTimeFormatter.RFC_1123_DATE_TIME), cookie.getExpires()); assertEquals("Lax", cookie.getSameSite()); } @@ -109,15 +114,17 @@ public void parseInvalidAttribute() { @Test public void parseHeaderWithAttributesCaseSensitivity() { - MockCookie cookie = MockCookie.parse( - "SESSION=123; domain=example.com; max-age=60; path=/; secure; httponly; samesite=Lax"); - + MockCookie cookie = MockCookie.parse("SESSION=123; domain=example.com; max-age=60; " + + "expires=Tue, 8 Oct 2019 19:50:00 GMT; path=/; secure; httponly; samesite=Lax"); + assertCookie(cookie, "SESSION", "123"); assertEquals("example.com", cookie.getDomain()); assertEquals(60, cookie.getMaxAge()); assertEquals("/", cookie.getPath()); assertTrue(cookie.getSecure()); assertTrue(cookie.isHttpOnly()); + assertEquals(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT", + DateTimeFormatter.RFC_1123_DATE_TIME), cookie.getExpires()); assertEquals("Lax", cookie.getSameSite()); } diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 7494203e8de..4218a79dc27 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -39,6 +39,7 @@ * @author Rob Winch * @author Sam Brannen * @author Brian Clozel + * @author Vedran Pavic * @since 19.02.2006 */ public class MockHttpServletResponseTests { @@ -351,6 +352,14 @@ public void addCookieHeader() { assertCookieValues("123", "999"); } + @Test + public void addCookieHeaderWithExpires() { + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=Tue, 8 Oct 2019 19:50:00 GMT; Secure; " + + "HttpOnly; SameSite=Lax"; + response.addHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertEquals(cookieValue, response.getHeader(HttpHeaders.SET_COOKIE)); + } + @Test public void addCookie() { MockCookie mockCookie = new MockCookie("SESSION", "123"); diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java index b7ac261942e..9b6ba944855 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java @@ -16,6 +16,9 @@ package org.springframework.mock.web.test; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + import javax.servlet.http.Cookie; import org.springframework.lang.Nullable; @@ -35,6 +38,9 @@ public class MockCookie extends Cookie { private static final long serialVersionUID = 4312531139502726325L; + @Nullable + private ZonedDateTime expires; + @Nullable private String sameSite; @@ -49,6 +55,20 @@ public MockCookie(String name, String value) { super(name, value); } + /** + * Add the "Expires" attribute to the cookie. + */ + public void setExpires(@Nullable ZonedDateTime expires) { + this.expires = expires; + } + + /** + * Return the "Expires" attribute, or {@code null} if not set. + */ + @Nullable + public ZonedDateTime getExpires() { + return this.expires; + } /** * Add the "SameSite" attribute to the cookie. @@ -94,6 +114,10 @@ public static MockCookie parse(String setCookieHeader) { else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) { cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader))); } + else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) { + cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), + DateTimeFormatter.RFC_1123_DATE_TIME)); + } else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) { cookie.setPath(extractAttributeValue(attribute, setCookieHeader)); } diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java index caa5209052c..c4ce94effd8 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java @@ -26,6 +26,7 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -345,9 +346,14 @@ private String getCookieHeader(Cookie cookie) { if (maxAge >= 0) { buf.append("; Max-Age=").append(maxAge); buf.append("; Expires="); - HttpHeaders headers = new HttpHeaders(); - headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0); - buf.append(headers.getFirst(HttpHeaders.EXPIRES)); + if (cookie instanceof MockCookie && ((MockCookie) cookie).getExpires() != null) { + buf.append(((MockCookie) cookie).getExpires().format(DateTimeFormatter.RFC_1123_DATE_TIME)); + } + else { + HttpHeaders headers = new HttpHeaders(); + headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0); + buf.append(headers.getFirst(HttpHeaders.EXPIRES)); + } } if (cookie.getSecure()) { From a3c9e8d4fca90ef1b4cdf500eaa15640f85d7d78 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 29 Oct 2019 13:27:39 +0100 Subject: [PATCH 004/322] Polish contribution See gh-23769 --- .../springframework/mock/web/MockCookie.java | 20 +++++++++++-------- .../web/MockHttpServletResponseTests.java | 17 +++++++++++++++- .../mock/web/test/MockCookie.java | 20 +++++++++++-------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java index a7565f180e7..17c3f2c027c 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java @@ -46,7 +46,7 @@ public class MockCookie extends Cookie { /** - * Constructor with the cookie name and value. + * Construct a new {@link MockCookie} with the supplied name and value. * @param name the name * @param value the value * @see Cookie#Cookie(String, String) @@ -56,14 +56,17 @@ public MockCookie(String name, String value) { } /** - * Add the "Expires" attribute to the cookie. + * Set the "Expires" attribute for this cookie. + * @since 5.1.11 */ public void setExpires(@Nullable ZonedDateTime expires) { this.expires = expires; } /** - * Return the "Expires" attribute, or {@code null} if not set. + * Get the "Expires" attribute for this cookie. + * @since 5.1.11 + * @return the "Expires" attribute for this cookie, or {@code null} if not set */ @Nullable public ZonedDateTime getExpires() { @@ -71,10 +74,10 @@ public ZonedDateTime getExpires() { } /** - * Add the "SameSite" attribute to the cookie. + * Set the "SameSite" attribute for this cookie. *

This limits the scope of the cookie such that it will only be attached - * to same site requests if {@code "Strict"} or cross-site requests if - * {@code "Lax"}. + * to same-site requests if the supplied value is {@code "Strict"} or cross-site + * requests if the supplied value is {@code "Lax"}. * @see RFC6265 bis */ public void setSameSite(@Nullable String sameSite) { @@ -82,7 +85,8 @@ public void setSameSite(@Nullable String sameSite) { } /** - * Return the "SameSite" attribute, or {@code null} if not set. + * Get the "SameSite" attribute for this cookie. + * @return the "SameSite" attribute for this cookie, or {@code null} if not set */ @Nullable public String getSameSite() { @@ -91,7 +95,7 @@ public String getSameSite() { /** - * Factory method that parses the value of a "Set-Cookie" header. + * Factory method that parses the value of the supplied "Set-Cookie" header. * @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty * @return the created cookie */ diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 4218a79dc27..70f6f002a04 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -339,6 +339,18 @@ public void setCookieHeader() { assertPrimarySessionCookie("999"); } + /** + * @since 5.1.11 + */ + @Test + public void setCookieHeaderWithExpiresAttribute() { + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=Tue, 8 Oct 2019 19:50:00 GMT; Secure; " + + "HttpOnly; SameSite=Lax"; + response.setHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertNumCookies(1); + assertEquals(cookieValue, response.getHeader(HttpHeaders.SET_COOKIE)); + } + @Test public void addCookieHeader() { response.addHeader(HttpHeaders.SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax"); @@ -352,8 +364,11 @@ public void addCookieHeader() { assertCookieValues("123", "999"); } + /** + * @since 5.1.11 + */ @Test - public void addCookieHeaderWithExpires() { + public void addCookieHeaderWithExpiresAttribute() { String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=Tue, 8 Oct 2019 19:50:00 GMT; Secure; " + "HttpOnly; SameSite=Lax"; response.addHeader(HttpHeaders.SET_COOKIE, cookieValue); diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java index 9b6ba944855..3bea81f01d4 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java @@ -46,7 +46,7 @@ public class MockCookie extends Cookie { /** - * Constructor with the cookie name and value. + * Construct a new {@link MockCookie} with the supplied name and value. * @param name the name * @param value the value * @see Cookie#Cookie(String, String) @@ -56,14 +56,17 @@ public MockCookie(String name, String value) { } /** - * Add the "Expires" attribute to the cookie. + * Set the "Expires" attribute for this cookie. + * @since 5.1.11 */ public void setExpires(@Nullable ZonedDateTime expires) { this.expires = expires; } /** - * Return the "Expires" attribute, or {@code null} if not set. + * Get the "Expires" attribute for this cookie. + * @since 5.1.11 + * @return the "Expires" attribute for this cookie, or {@code null} if not set */ @Nullable public ZonedDateTime getExpires() { @@ -71,10 +74,10 @@ public ZonedDateTime getExpires() { } /** - * Add the "SameSite" attribute to the cookie. + * Set the "SameSite" attribute for this cookie. *

This limits the scope of the cookie such that it will only be attached - * to same site requests if {@code "Strict"} or cross-site requests if - * {@code "Lax"}. + * to same-site requests if the supplied value is {@code "Strict"} or cross-site + * requests if the supplied value is {@code "Lax"}. * @see RFC6265 bis */ public void setSameSite(@Nullable String sameSite) { @@ -82,7 +85,8 @@ public void setSameSite(@Nullable String sameSite) { } /** - * Return the "SameSite" attribute, or {@code null} if not set. + * Get the "SameSite" attribute for this cookie. + * @return the "SameSite" attribute for this cookie, or {@code null} if not set */ @Nullable public String getSameSite() { @@ -91,7 +95,7 @@ public String getSameSite() { /** - * Factory method that parses the value of a "Set-Cookie" header. + * Factory method that parses the value of the supplied "Set-Cookie" header. * @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty * @return the created cookie */ From f16aa4a9b560266b5af82cdb36a18738e22e9b37 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 30 Oct 2019 00:26:11 +0100 Subject: [PATCH 005/322] Nullability refinements --- .../mock/web/MockHttpServletResponse.java | 6 ++- .../SynchronossPartHttpMessageReader.java | 41 ++++++++++--------- .../web/test/MockHttpServletResponse.java | 6 ++- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index b933ba14bf3..2135c6b7935 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -26,6 +26,7 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; @@ -346,8 +347,9 @@ private String getCookieHeader(Cookie cookie) { if (maxAge >= 0) { buf.append("; Max-Age=").append(maxAge); buf.append("; Expires="); - if (cookie instanceof MockCookie && ((MockCookie) cookie).getExpires() != null) { - buf.append(((MockCookie) cookie).getExpires().format(DateTimeFormatter.RFC_1123_DATE_TIME)); + ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null); + if (expires != null) { + buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); } else { HttpHeaders headers = new HttpHeaders(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index 855b9fb047c..558841b10a3 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -163,7 +163,6 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType (mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)); } - @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { return Flux.create(new SynchronossPartGenerator(message)) @@ -177,13 +176,9 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess }); } - @Override - public Mono readMono( - ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - - return Mono.error(new UnsupportedOperationException( - "Cannot read multipart request body into single Part")); + public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { + return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } @@ -197,16 +192,16 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + @Nullable private NioMultipartParserListener listener; + @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { this.inputMessage = inputMessage; } - @Override public void accept(FluxSink sink) { HttpHeaders headers = this.inputMessage.getHeaders(); @@ -229,10 +224,13 @@ public void accept(FluxSink sink) { @Override protected void hookOnNext(DataBuffer buffer) { + Assert.state(this.parser != null && this.listener != null, "Not initialized yet"); + int size = buffer.readableByteCount(); this.storageFactory.increaseByteCount(size); byte[] resultBytes = new byte[size]; buffer.read(resultBytes); + try { this.parser.write(resultBytes); } @@ -249,24 +247,32 @@ protected void hookOnNext(DataBuffer buffer) { @Override protected void hookOnError(Throwable ex) { try { - this.parser.close(); + if (this.parser != null) { + this.parser.close(); + } } catch (IOException ex2) { // ignore } finally { - int index = this.storageFactory.getCurrentPartIndex(); - this.listener.onError("Failure while parsing part[" + index + "]", ex); + if (this.listener != null) { + int index = this.storageFactory.getCurrentPartIndex(); + this.listener.onError("Failure while parsing part[" + index + "]", ex); + } } } @Override protected void hookFinally(SignalType type) { try { - this.parser.close(); + if (this.parser != null) { + this.parser.close(); + } } catch (IOException ex) { - this.listener.onError("Error while closing parser", ex); + if (this.listener != null) { + this.listener.onError("Error while closing parser", ex); + } } } @@ -280,9 +286,9 @@ private int getContentLength(HttpHeaders headers) { private class LimitedPartBodyStreamStorageFactory implements PartBodyStreamStorageFactory { - private final PartBodyStreamStorageFactory storageFactory = maxInMemorySize > 0 ? + private final PartBodyStreamStorageFactory storageFactory = (maxInMemorySize > 0 ? new DefaultPartBodyStreamStorageFactory(maxInMemorySize) : - new DefaultPartBodyStreamStorageFactory(); + new DefaultPartBodyStreamStorageFactory()); private int index = 1; @@ -290,7 +296,6 @@ private class LimitedPartBodyStreamStorageFactory implements PartBodyStreamStora private long partSize; - public int getCurrentPartIndex() { return this.index; } @@ -339,7 +344,6 @@ private static class FluxSinkAdapterListener implements NioMultipartParserListen private final AtomicInteger terminated = new AtomicInteger(0); - FluxSinkAdapterListener( FluxSink sink, MultipartContext context, LimitedPartBodyStreamStorageFactory factory) { @@ -348,7 +352,6 @@ private static class FluxSinkAdapterListener implements NioMultipartParserListen this.storageFactory = factory; } - @Override public void onPartFinished(StreamStorage storage, Map> headers) { HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java index c4ce94effd8..0fccb7fd096 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java @@ -26,6 +26,7 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; @@ -346,8 +347,9 @@ private String getCookieHeader(Cookie cookie) { if (maxAge >= 0) { buf.append("; Max-Age=").append(maxAge); buf.append("; Expires="); - if (cookie instanceof MockCookie && ((MockCookie) cookie).getExpires() != null) { - buf.append(((MockCookie) cookie).getExpires().format(DateTimeFormatter.RFC_1123_DATE_TIME)); + ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null); + if (expires != null) { + buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); } else { HttpHeaders headers = new HttpHeaders(); From 03bd02a62791d0b399398530d3a3548dc39dc026 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 30 Oct 2019 01:04:41 +0100 Subject: [PATCH 006/322] Expose primary flag on BeanDefinitionBuilder Closes gh-23794 --- .../beans/factory/support/BeanDefinitionBuilder.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java index cfddff43e09..eb0993b9ab8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java @@ -302,6 +302,15 @@ public BeanDefinitionBuilder addDependsOn(String beanName) { return this; } + /** + * Set whether this bean is a primary autowire candidate. + * @since 5.1.11 + */ + public BeanDefinitionBuilder setPrimary(boolean primary) { + this.beanDefinition.setPrimary(primary); + return this; + } + /** * Set the role of this definition. */ From 80a5019534dc27dc4f8a9666d5fac25b690a81a0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 30 Oct 2019 11:07:07 +0100 Subject: [PATCH 007/322] Verify read-only propagation in DataSourceTransactionManagerTests See gh-23747 --- .../jdbc/datasource/DataSourceTransactionManagerTests.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java index 3412f6294c9..f49aa9a37cc 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -858,6 +858,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run public void testTransactionWithIsolationAndReadOnly() throws Exception { given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); given(con.getAutoCommit()).willReturn(true); + given(con.isReadOnly()).willReturn(true); TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); @@ -875,11 +876,13 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds)); InOrder ordered = inOrder(con); + ordered.verify(con).setReadOnly(true); ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); ordered.verify(con).setAutoCommit(false); ordered.verify(con).commit(); ordered.verify(con).setAutoCommit(true); ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + ordered.verify(con).setReadOnly(false); verify(con).close(); } @@ -890,6 +893,7 @@ public void testTransactionWithEnforceReadOnly() throws Exception { given(con.getAutoCommit()).willReturn(true); Statement stmt = mock(Statement.class); given(con.createStatement()).willReturn(stmt); + given(con.isReadOnly()).willReturn(true); TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); @@ -906,11 +910,13 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds)); InOrder ordered = inOrder(con, stmt); + ordered.verify(con).setReadOnly(true); ordered.verify(con).setAutoCommit(false); ordered.verify(stmt).executeUpdate("SET TRANSACTION READ ONLY"); ordered.verify(stmt).close(); ordered.verify(con).commit(); ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setReadOnly(false); ordered.verify(con).close(); } From 6faf61ba40118efe0c282293ec53cd3f8943a0b9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 30 Oct 2019 11:07:39 +0100 Subject: [PATCH 008/322] Latest applicable dependency updates (Tomcat 9.0.27, Undertow 2.0.27, RxJava 2.2.13, Hibernate ORM 5.3.13, POI 4.1.1, Checkstyle 8.26) --- build.gradle | 8 ++++---- spring-orm/spring-orm.gradle | 2 +- spring-webmvc/spring-webmvc.gradle | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 909a633aec5..ce556824abb 100644 --- a/build.gradle +++ b/build.gradle @@ -41,11 +41,11 @@ ext { reactorVersion = "Californium-SR13" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" - rxjava2Version = "2.2.12" + rxjava2Version = "2.2.13" slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" - tomcatVersion = "9.0.26" - undertowVersion = "2.0.26.Final" + tomcatVersion = "9.0.27" + undertowVersion = "2.0.27.Final" gradleScriptDir = "${rootProject.projectDir}/gradle" withoutJclOverSlf4j = { @@ -142,7 +142,7 @@ configure(allprojects) { project -> } checkstyle { - toolVersion = "8.24" + toolVersion = "8.26" configDir = rootProject.file("src/checkstyle") } diff --git a/spring-orm/spring-orm.gradle b/spring-orm/spring-orm.gradle index 9fa89d919d3..e173b8cbd6d 100644 --- a/spring-orm/spring-orm.gradle +++ b/spring-orm/spring-orm.gradle @@ -9,7 +9,7 @@ dependencies { optional(project(":spring-context")) optional(project(":spring-web")) optional("org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.4") - optional("org.hibernate:hibernate-core:5.3.12.Final") + optional("org.hibernate:hibernate-core:5.3.13.Final") optional("javax.servlet:javax.servlet-api:3.1.0") testCompile("org.aspectj:aspectjweaver:${aspectjVersion}") testCompile("org.hsqldb:hsqldb:${hsqldbVersion}") diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index ff1fd65c131..1c24771a654 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -24,7 +24,7 @@ dependencies { optional("org.webjars:webjars-locator-core:0.37") optional("com.rometools:rome:1.12.2") optional("com.github.librepdf:openpdf:1.2.21") - optional("org.apache.poi:poi-ooxml:4.1.0") + optional("org.apache.poi:poi-ooxml:4.1.1") optional("org.freemarker:freemarker:${freemarkerVersion}") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}") From cc84533d85fce3cb33a9ef4cba016434eebbb4b8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 30 Oct 2019 06:53:25 +0000 Subject: [PATCH 009/322] Logging decorator for WebSocketStompClient handler Closes gh-23793 --- .../web/socket/messaging/WebSocketStompClient.java | 7 +++++-- .../socket/messaging/WebSocketStompClientTests.java | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java index dfdcc1830ba..59507459fd8 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.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. @@ -57,6 +57,7 @@ import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator; import org.springframework.web.socket.sockjs.transport.SockJsSession; import org.springframework.web.util.UriComponentsBuilder; @@ -265,7 +266,9 @@ public ListenableFuture connect(URI url, @Nullable WebSocketHttpHe Assert.notNull(url, "'url' must not be null"); ConnectionHandlingStompSession session = createSession(connectHeaders, sessionHandler); WebSocketTcpConnectionHandlerAdapter adapter = new WebSocketTcpConnectionHandlerAdapter(session); - getWebSocketClient().doHandshake(adapter, handshakeHeaders, url).addCallback(adapter); + getWebSocketClient() + .doHandshake(new LoggingWebSocketHandlerDecorator(adapter), handshakeHeaders, url) + .addCallback(adapter); return session.getSessionFuture(); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java index cf98cf0f0b1..026f535d505 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java @@ -45,6 +45,7 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.handler.WebSocketHandlerDecorator; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -317,9 +318,12 @@ private WebSocketHandler connect() { @SuppressWarnings("unchecked") private TcpConnection getTcpConnection() throws Exception { - WebSocketHandler webSocketHandler = connect(); - webSocketHandler.afterConnectionEstablished(this.webSocketSession); - return (TcpConnection) webSocketHandler; + WebSocketHandler handler = connect(); + handler.afterConnectionEstablished(this.webSocketSession); + if (handler instanceof WebSocketHandlerDecorator) { + handler = ((WebSocketHandlerDecorator) handler).getLastHandler(); + } + return (TcpConnection) handler; } private void testInactivityTaskScheduling(Runnable runnable, long delay, long sleepTime) From 614c7b0f8e331dcd665a17caa0ee567254cb8889 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 30 Oct 2019 12:10:45 +0000 Subject: [PATCH 010/322] ResponseStatusException associated headers A ResponseStatus exception now exposes extra method to return headers for the response. This is used in ResponseStatusExceptionHandler to apply the headers to the response. Closes gh-23741 --- .../web/server/MethodNotAllowedException.java | 23 ++++++++++--- .../server/NotAcceptableStatusException.java | 13 ++++++++ .../web/server/ResponseStatusException.java | 17 ++++++++-- .../ResponseStatusExceptionHandler.java | 25 +++++++++----- .../ResponseStatusExceptionHandlerTests.java | 33 +++++++++++++++++-- 5 files changed, 95 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java index 4c10051fcbf..14dcd2a62b8 100644 --- a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java +++ b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.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. @@ -19,12 +19,15 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * Exception for errors that fit response status 405 (method not allowed). @@ -37,7 +40,7 @@ public class MethodNotAllowedException extends ResponseStatusException { private final String method; - private final Set supportedMethods; + private final Set httpMethods; public MethodNotAllowedException(HttpMethod method, Collection supportedMethods) { @@ -51,10 +54,21 @@ public MethodNotAllowedException(String method, @Nullable Collection supportedMethods = Collections.emptySet(); } this.method = method; - this.supportedMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods)); + this.httpMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods)); } + /** + * Return a Map with an "Allow" header. + * @since 5.1.11 + */ + @Override + public Map getHeaders() { + return !CollectionUtils.isEmpty(this.httpMethods) ? + Collections.singletonMap("Allow", StringUtils.collectionToDelimitedString(this.httpMethods, ", ")) : + Collections.emptyMap(); + } + /** * Return the HTTP method for the failed request. */ @@ -66,6 +80,7 @@ public String getHttpMethod() { * Return the list of supported HTTP methods. */ public Set getSupportedMethods() { - return this.supportedMethods; + return this.httpMethods; } + } diff --git a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java index a93651f9722..3e3a25285b2 100644 --- a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java @@ -18,9 +18,11 @@ import java.util.Collections; import java.util.List; +import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; /** * Exception for errors that fit response status 406 (not acceptable). @@ -51,6 +53,17 @@ public NotAcceptableStatusException(List supportedMediaTypes) { } + /** + * Return a Map with an "Accept" header, or an empty map. + * @since 5.1.11 + */ + @Override + public Map getHeaders() { + return !CollectionUtils.isEmpty(this.supportedMediaTypes) ? + Collections.singletonMap("Accept", MediaType.toString(this.supportedMediaTypes)) : + Collections.emptyMap(); + } + /** * Return the list of supported content types in cases when the Accept * header is parsed but not supported, or an empty list otherwise. diff --git a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java index af5e14c60b1..a0245686a08 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.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. @@ -16,6 +16,9 @@ package org.springframework.web.server; +import java.util.Collections; +import java.util.Map; + import org.springframework.core.NestedExceptionUtils; import org.springframework.core.NestedRuntimeException; import org.springframework.http.HttpStatus; @@ -72,12 +75,21 @@ public ResponseStatusException(HttpStatus status, @Nullable String reason, @Null /** - * The HTTP status that fits the exception (never {@code null}). + * Return the HTTP status associated with this exception. */ public HttpStatus getStatus() { return this.status; } + /** + * Return response headers associated with the exception, possibly required + * for the given status code (e.g. "Allow", "Accept"). + * @since 5.1.11 + */ + public Map getHeaders() { + return Collections.emptyMap(); + } + /** * The reason explaining the exception (potentially {@code null} or empty). */ @@ -86,6 +98,7 @@ public String getReason() { return this.reason; } + @Override public String getMessage() { String msg = this.status + (this.reason != null ? " \"" + this.reason + "\"" : ""); diff --git a/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java b/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java index f8388669cb9..263e9cf1afe 100644 --- a/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java +++ b/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.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. @@ -22,6 +22,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; @@ -62,8 +63,7 @@ public void setWarnLogCategory(String loggerName) { @Override public Mono handle(ServerWebExchange exchange, Throwable ex) { - HttpStatus status = resolveStatus(ex); - if (status == null || !exchange.getResponse().setStatusCode(status)) { + if (!updateResponse(exchange.getResponse(), ex)) { return Mono.error(ex); } @@ -86,16 +86,25 @@ private String formatError(Throwable ex, ServerHttpRequest request) { return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path; } - @Nullable - private HttpStatus resolveStatus(Throwable ex) { + private boolean updateResponse(ServerHttpResponse response, Throwable ex) { + boolean result = false; HttpStatus status = determineStatus(ex); - if (status == null) { + if (status != null) { + if (response.setStatusCode(status)) { + if (ex instanceof ResponseStatusException) { + ((ResponseStatusException) ex).getHeaders() + .forEach((name, value) -> response.getHeaders().add(name, value)); + } + result = true; + } + } + else { Throwable cause = ex.getCause(); if (cause != null) { - status = resolveStatus(cause); + result = updateResponse(response, cause); } } - return status; + return result; } /** diff --git a/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java b/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java index fd5579f4507..c7528ef2a42 100644 --- a/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.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. @@ -17,18 +17,27 @@ package org.springframework.web.server.handler; import java.time.Duration; +import java.util.Arrays; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.web.test.server.MockServerWebExchange; +import org.springframework.web.server.MethodNotAllowedException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ResponseStatusException; -import static org.junit.Assert.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; /** * Unit tests for {@link ResponseStatusExceptionHandler}. @@ -67,6 +76,26 @@ public void handleNestedResponseStatusException() { assertEquals(HttpStatus.BAD_REQUEST, this.exchange.getResponse().getStatusCode()); } + @Test // gh-23741 + public void handleMethodNotAllowed() { + Throwable ex = new MethodNotAllowedException(HttpMethod.PATCH, Arrays.asList(HttpMethod.POST, HttpMethod.PUT)); + this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); + + MockServerHttpResponse response = this.exchange.getResponse(); + assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode()); + assertThat(response.getHeaders().getAllow(), contains(HttpMethod.POST, HttpMethod.PUT)); + } + + @Test // gh-23741 + public void handleResponseStatusExceptionWithHeaders() { + Throwable ex = new NotAcceptableStatusException(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML)); + this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); + + MockServerHttpResponse response = this.exchange.getResponse(); + assertEquals(HttpStatus.NOT_ACCEPTABLE, response.getStatusCode()); + assertThat(response.getHeaders().getAccept(), contains(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML)); + } + @Test public void unresolvedException() { Throwable expected = new IllegalStateException(); From 9f43ee3304ba9fe88b17bc54d8229ec088233c74 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 30 Oct 2019 16:19:40 +0100 Subject: [PATCH 011/322] Treat InvalidPathException like an IOException in MockServletContext Prior to this commit, if MockServletContext was configured with a FileSystemResourceLoader, invocations of the following methods on a Microsoft Windows operating system resulted in an InvalidPathException if the supplied path contained a colon (such as "C:\\temp"). This is inconsistent with the behavior on non-Windows operating systems. In addition, for comparable errors resulting in an IOException, those methods (except getRealPath()) return null instead of throwing the exception. - getResourcePaths() - getResource() - getResourceAsStream() - getRealPath() This commit makes handling of InvalidPathException and IOException consistent for these methods: both exceptions now result in null be returned by these methods. Closes gh-23717 --- .../mock/web/MockServletContext.java | 51 +++++++++------ .../mock/web/MockServletContextTests.java | 62 +++++++++++++++++-- .../mock/web/test/MockServletContext.java | 51 +++++++++------ 3 files changed, 121 insertions(+), 43 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 97fed0c5d14..7dab1c8c21b 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.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. @@ -21,6 +21,7 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.InvalidPathException; import java.util.Collections; import java.util.Enumeration; import java.util.EventListener; @@ -294,8 +295,10 @@ public void addMimeType(String fileExtension, MediaType mimeType) { @Nullable public Set getResourcePaths(String path) { String actualPath = (path.endsWith("/") ? path : path + "/"); - Resource resource = this.resourceLoader.getResource(getResourceLocation(actualPath)); + String resourceLocation = getResourceLocation(actualPath); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); File file = resource.getFile(); String[] fileList = file.list(); if (ObjectUtils.isEmpty(fileList)) { @@ -311,9 +314,10 @@ public Set getResourcePaths(String path) { } return resourcePaths; } - catch (IOException ex) { + catch (InvalidPathException | IOException ex ) { if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + resource, ex); + logger.warn("Could not get resource paths for " + + (resource != null ? resource : resourceLocation), ex); } return null; } @@ -322,19 +326,22 @@ public Set getResourcePaths(String path) { @Override @Nullable public URL getResource(String path) throws MalformedURLException { - Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); - if (!resource.exists()) { - return null; - } + String resourceLocation = getResourceLocation(path); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); + if (!resource.exists()) { + return null; + } return resource.getURL(); } catch (MalformedURLException ex) { throw ex; } - catch (IOException ex) { + catch (InvalidPathException | IOException ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for " + resource, ex); + logger.warn("Could not get URL for resource " + + (resource != null ? resource : resourceLocation), ex); } return null; } @@ -343,16 +350,19 @@ public URL getResource(String path) throws MalformedURLException { @Override @Nullable public InputStream getResourceAsStream(String path) { - Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); - if (!resource.exists()) { - return null; - } + String resourceLocation = getResourceLocation(path); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); + if (!resource.exists()) { + return null; + } return resource.getInputStream(); } - catch (IOException ex) { + catch (InvalidPathException | IOException ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for " + resource, ex); + logger.warn("Could not open InputStream for resource " + + (resource != null ? resource : resourceLocation), ex); } return null; } @@ -459,13 +469,16 @@ public void log(String message, Throwable ex) { @Override @Nullable public String getRealPath(String path) { - Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + String resourceLocation = getResourceLocation(path); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); return resource.getFile().getAbsolutePath(); } - catch (IOException ex) { + catch (InvalidPathException | IOException ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + resource, ex); + logger.warn("Could not determine real path of resource " + + (resource != null ? resource : resourceLocation), ex); } return null; } diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java index b1669dbea56..66b61e94a79 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockServletContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -16,6 +16,8 @@ package org.springframework.mock.web; +import java.io.InputStream; +import java.net.URL; import java.util.Map; import java.util.Set; @@ -24,7 +26,9 @@ import javax.servlet.ServletRegistration; import org.junit.Test; +import org.junit.jupiter.api.condition.OS; +import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.http.MediaType; import static org.junit.Assert.assertEquals; @@ -34,6 +38,8 @@ import static org.junit.Assert.assertTrue; /** + * Unit tests for {@link MockServletContext}. + * * @author Juergen Hoeller * @author Chris Beams * @author Sam Brannen @@ -45,27 +51,27 @@ public class MockServletContextTests { @Test - public void listFiles() { + public void getResourcePaths() { Set paths = sc.getResourcePaths("/web"); assertNotNull(paths); assertTrue(paths.contains("/web/MockServletContextTests.class")); } @Test - public void listSubdirectories() { + public void getResourcePathsWithSubdirectories() { Set paths = sc.getResourcePaths("/"); assertNotNull(paths); assertTrue(paths.contains("/web/")); } @Test - public void listNonDirectory() { + public void getResourcePathsWithNonDirectory() { Set paths = sc.getResourcePaths("/web/MockServletContextTests.class"); assertNull(paths); } @Test - public void listInvalidPath() { + public void getResourcePathsWithInvalidPath() { Set paths = sc.getResourcePaths("/web/invalid"); assertNull(paths); } @@ -194,4 +200,50 @@ public void getFilterRegistrations() { assertEquals(0, filterRegistrations.size()); } + /** + * @since 5.1.11 + */ + @Test + public void getResourcePathsWithRelativePathToWindowsCDrive() { + MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader()); + Set paths = servletContext.getResourcePaths("C:\\temp"); + assertNull(paths); + } + + /** + * @since 5.1.11 + */ + @Test + public void getResourceWithRelativePathToWindowsCDrive() throws Exception { + MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader()); + URL resource = servletContext.getResource("C:\\temp"); + assertNull(resource); + } + + /** + * @since 5.1.11 + */ + @Test + public void getResourceAsStreamWithRelativePathToWindowsCDrive() { + MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader()); + InputStream inputStream = servletContext.getResourceAsStream("C:\\temp"); + assertNull(inputStream); + } + + /** + * @since 5.1.11 + */ + @Test + public void getRealPathWithRelativePathToWindowsCDrive() { + MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader()); + String realPath = servletContext.getRealPath("C:\\temp"); + + if (OS.WINDOWS.isCurrentOs()) { + assertNull(realPath); + } + else { + assertNotNull(realPath); + } + } + } diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java index 3b78ed50ee6..32fa355b866 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.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. @@ -21,6 +21,7 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.InvalidPathException; import java.util.Collections; import java.util.Enumeration; import java.util.EventListener; @@ -294,8 +295,10 @@ public void addMimeType(String fileExtension, MediaType mimeType) { @Nullable public Set getResourcePaths(String path) { String actualPath = (path.endsWith("/") ? path : path + "/"); - Resource resource = this.resourceLoader.getResource(getResourceLocation(actualPath)); + String resourceLocation = getResourceLocation(actualPath); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); File file = resource.getFile(); String[] fileList = file.list(); if (ObjectUtils.isEmpty(fileList)) { @@ -311,9 +314,10 @@ public Set getResourcePaths(String path) { } return resourcePaths; } - catch (IOException ex) { + catch (InvalidPathException | IOException ex ) { if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + resource, ex); + logger.warn("Could not get resource paths for " + + (resource != null ? resource : resourceLocation), ex); } return null; } @@ -322,19 +326,22 @@ public Set getResourcePaths(String path) { @Override @Nullable public URL getResource(String path) throws MalformedURLException { - Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); - if (!resource.exists()) { - return null; - } + String resourceLocation = getResourceLocation(path); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); + if (!resource.exists()) { + return null; + } return resource.getURL(); } catch (MalformedURLException ex) { throw ex; } - catch (IOException ex) { + catch (InvalidPathException | IOException ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for " + resource, ex); + logger.warn("Could not get URL for resource " + + (resource != null ? resource : resourceLocation), ex); } return null; } @@ -343,16 +350,19 @@ public URL getResource(String path) throws MalformedURLException { @Override @Nullable public InputStream getResourceAsStream(String path) { - Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); - if (!resource.exists()) { - return null; - } + String resourceLocation = getResourceLocation(path); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); + if (!resource.exists()) { + return null; + } return resource.getInputStream(); } - catch (IOException ex) { + catch (InvalidPathException | IOException ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for " + resource, ex); + logger.warn("Could not open InputStream for resource " + + (resource != null ? resource : resourceLocation), ex); } return null; } @@ -459,13 +469,16 @@ public void log(String message, Throwable ex) { @Override @Nullable public String getRealPath(String path) { - Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); + String resourceLocation = getResourceLocation(path); + Resource resource = null; try { + resource = this.resourceLoader.getResource(resourceLocation); return resource.getFile().getAbsolutePath(); } - catch (IOException ex) { + catch (InvalidPathException | IOException ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + resource, ex); + logger.warn("Could not determine real path of resource " + + (resource != null ? resource : resourceLocation), ex); } return null; } From 82751141ac622576d2753b57c116f4a423eff368 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 30 Oct 2019 14:31:34 +0100 Subject: [PATCH 012/322] Revise concurrent JAXBContext creation towards computeIfAbsent Closes gh-23879 --- .../http/codec/xml/Jaxb2XmlDecoder.java | 2 +- .../http/codec/xml/Jaxb2XmlEncoder.java | 2 +- .../http/codec/xml/JaxbContextContainer.java | 27 ++++++++++--------- .../AbstractJaxb2HttpMessageConverter.java | 15 ++++------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java index 7fd886fb0dd..d2dc6106e66 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java @@ -192,7 +192,7 @@ private Object unmarshal(List events, Class outputClass) { } } - private Unmarshaller initUnmarshaller(Class outputClass) throws JAXBException { + private Unmarshaller initUnmarshaller(Class outputClass) throws CodecException, JAXBException { Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass); return this.unmarshallerProcessor.apply(unmarshaller); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlEncoder.java index 108260571ab..8441d1afa32 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlEncoder.java @@ -135,7 +135,7 @@ protected Flux encode(Object value, DataBufferFactory bufferFactory, }).flux(); } - private Marshaller initMarshaller(Class clazz) throws JAXBException { + private Marshaller initMarshaller(Class clazz) throws CodecException, JAXBException { Marshaller marshaller = this.jaxbContexts.createMarshaller(clazz); marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); marshaller = this.marshallerProcessor.apply(marshaller); diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java b/spring-web/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java index 2c205c1ddeb..49441c498c3 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -24,12 +24,13 @@ import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -import org.springframework.util.Assert; +import org.springframework.core.codec.CodecException; /** * Holder for {@link JAXBContext} instances. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 */ final class JaxbContextContainer { @@ -37,24 +38,26 @@ final class JaxbContextContainer { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); - public Marshaller createMarshaller(Class clazz) throws JAXBException { + public Marshaller createMarshaller(Class clazz) throws CodecException, JAXBException { JAXBContext jaxbContext = getJaxbContext(clazz); return jaxbContext.createMarshaller(); } - public Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + public Unmarshaller createUnmarshaller(Class clazz) throws CodecException, JAXBException { JAXBContext jaxbContext = getJaxbContext(clazz); return jaxbContext.createUnmarshaller(); } - private JAXBContext getJaxbContext(Class clazz) throws JAXBException { - Assert.notNull(clazz, "Class must not be null"); - JAXBContext jaxbContext = this.jaxbContexts.get(clazz); - if (jaxbContext == null) { - jaxbContext = JAXBContext.newInstance(clazz); - this.jaxbContexts.putIfAbsent(clazz, jaxbContext); - } - return jaxbContext; + private JAXBContext getJaxbContext(Class clazz) throws CodecException { + return this.jaxbContexts.computeIfAbsent(clazz, key -> { + try { + return JAXBContext.newInstance(clazz); + } + catch (JAXBException ex) { + throw new CodecException( + "Could not create JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + } + }); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java index 913839b001d..811536c3165 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.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. @@ -25,7 +25,6 @@ import javax.xml.bind.Unmarshaller; import org.springframework.http.converter.HttpMessageConversionException; -import org.springframework.util.Assert; /** * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} @@ -106,19 +105,15 @@ protected void customizeUnmarshaller(Unmarshaller unmarshaller) { * @throws HttpMessageConversionException in case of JAXB errors */ protected final JAXBContext getJaxbContext(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - JAXBContext jaxbContext = this.jaxbContexts.get(clazz); - if (jaxbContext == null) { + return this.jaxbContexts.computeIfAbsent(clazz, key -> { try { - jaxbContext = JAXBContext.newInstance(clazz); - this.jaxbContexts.putIfAbsent(clazz, jaxbContext); + return JAXBContext.newInstance(clazz); } catch (JAXBException ex) { throw new HttpMessageConversionException( - "Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + "Could not create JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); } - } - return jaxbContext; + }); } } From b4cf471021bca3a65256a6181ba51d20ce90fe4d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 30 Oct 2019 16:45:35 +0100 Subject: [PATCH 013/322] Polishing --- .../annotation/DeferredImportSelector.java | 14 +++--- .../springframework/util/unit/DataUnit.java | 43 +++++++++++++------ .../simp/user/MultiServerUserRegistry.java | 1 - .../TransactionInterceptorTests.java | 5 ++- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java index 52cdb75dfd8..6bca9156781 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -16,8 +16,6 @@ package org.springframework.context.annotation; -import java.util.Objects; - import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.Nullable; @@ -100,7 +98,7 @@ public String getImportClassName() { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } @@ -108,13 +106,17 @@ public boolean equals(Object other) { return false; } Entry entry = (Entry) other; - return (Objects.equals(this.metadata, entry.metadata) && - Objects.equals(this.importClassName, entry.importClassName)); + return (this.metadata.equals(entry.metadata) && this.importClassName.equals(entry.importClassName)); } @Override public int hashCode() { - return Objects.hash(this.metadata, this.importClassName); + return (this.metadata.hashCode() * 31 + this.importClassName.hashCode()); + } + + @Override + public String toString() { + return this.importClassName; } } } diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java b/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java index dc00e2ded28..8b5dc4354d8 100644 --- a/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java +++ b/spring-core/src/main/java/org/springframework/util/unit/DataUnit.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. @@ -16,38 +16,53 @@ package org.springframework.util.unit; -import java.util.Objects; - /** - * A standard set of data size units. + * A standard set of {@link DataSize} units. + * + *

The unit prefixes used in this class are + * binary prefixes + * indicating multiplication by powers of 2. The following table displays the + * enum constants defined in this class and corresponding values. + * + *

+ * + * + * + * + * + * + * + *
ConstantData SizePower of 2Size in Bytes
{@link #BYTES}1B2^01
{@link #KILOBYTES}1KB2^101,024
{@link #MEGABYTES}1MB2^201,048,576
{@link #GIGABYTES}1GB2^301,073,741,824
{@link #TERABYTES}1TB2^401,099,511,627,776
* * @author Stephane Nicoll + * @author Sam Brannen * @since 5.1 + * @see DataSize */ public enum DataUnit { /** - * Bytes. + * Bytes, represented by suffix {@code B}. */ BYTES("B", DataSize.ofBytes(1)), /** - * Kilobytes. + * Kilobytes, represented by suffix {@code KB}. */ KILOBYTES("KB", DataSize.ofKilobytes(1)), /** - * Megabytes. + * Megabytes, represented by suffix {@code MB}. */ MEGABYTES("MB", DataSize.ofMegabytes(1)), /** - * Gigabytes. + * Gigabytes, represented by suffix {@code GB}. */ GIGABYTES("GB", DataSize.ofGigabytes(1)), /** - * Terabytes. + * Terabytes, represented by suffix {@code TB}. */ TERABYTES("TB", DataSize.ofTerabytes(1)); @@ -68,18 +83,18 @@ DataSize size() { /** * Return the {@link DataUnit} matching the specified {@code suffix}. - * @param suffix one of the standard suffix + * @param suffix one of the standard suffixes * @return the {@link DataUnit} matching the specified {@code suffix} - * @throws IllegalArgumentException if the suffix does not match any - * of this enum's constants + * @throws IllegalArgumentException if the suffix does not match the suffix + * of any of this enum's constants */ public static DataUnit fromSuffix(String suffix) { for (DataUnit candidate : values()) { - if (Objects.equals(candidate.suffix, suffix)) { + if (candidate.suffix.equals(suffix)) { return candidate; } } - throw new IllegalArgumentException("Unknown unit '" + suffix + "'"); + throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'"); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java index d3585c82cf3..dd30f769594 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java @@ -560,7 +560,6 @@ public Map findSessions(String userName) { } return map; } - } } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java index 1279d6c4abd..44fc6c641f9 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.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. @@ -40,6 +40,7 @@ * Mock object based tests for TransactionInterceptor. * * @author Rod Johnson + * @author Juergen Hoeller * @since 16.03.2003 */ public class TransactionInterceptorTests extends AbstractTransactionAspectTests { @@ -49,7 +50,7 @@ public class TransactionInterceptorTests extends AbstractTransactionAspectTests @Override - protected Object advised(Object target, PlatformTransactionManager ptm, TransactionAttributeSource[] tas) throws Exception { + protected Object advised(Object target, PlatformTransactionManager ptm, TransactionAttributeSource[] tas) { TransactionInterceptor ti = new TransactionInterceptor(); ti.setTransactionManager(ptm); ti.setTransactionAttributeSources(tas); From feeeab1761d211da36a80972ada0ffb63b524278 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 30 Oct 2019 13:45:32 +0100 Subject: [PATCH 014/322] Reorder date formatting converter in registrar Prior to this commit, the `DateFormatterRegistrar` would register the annotation-based formatter before the pattern-based formatter. This would create an issue when an application tries to convert a `String` to an annotated `@DateTimeFormat Date`: since the converters are considered in reversed order of registration in `GenericConversionServicei#ConvertersForPair`, the pattern-based variant would always be considered before the annotation-based variant, overriding the developer's opinion. This commit aligns the `DateFormatterRegistrar` with the `DateTimeFormatterRegistrar` and registers the annotation-based variant last. Closes gh-23896 --- .../datetime/DateFormatterRegistrar.java | 3 +-- .../format/datetime/DateFormattingTests.java | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java index bc2ff0548dc..3e7a01b2401 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java @@ -61,14 +61,13 @@ public void setFormatter(DateFormatter dateFormatter) { @Override public void registerFormatters(FormatterRegistry registry) { addDateConverters(registry); - registry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory()); - // In order to retain back compatibility we only register Date/Calendar // types when a user defined formatter is specified (see SPR-10105) if (this.dateFormatter != null) { registry.addFormatter(this.dateFormatter); registry.addFormatterForFieldType(Calendar.class, this.dateFormatter); } + registry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory()); } /** diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index 7a6cd3df0ce..e2cbe51ca32 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.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. @@ -46,7 +46,7 @@ */ public class DateFormattingTests { - private final FormattingConversionService conversionService = new FormattingConversionService(); + private FormattingConversionService conversionService; private DataBinder binder; @@ -58,6 +58,7 @@ public void setup() { } private void setup(DateFormatterRegistrar registrar) { + conversionService = new FormattingConversionService(); DefaultConversionService.addDefaultConverters(conversionService); registrar.registerFormatters(conversionService); @@ -148,6 +149,20 @@ public void testBindDateAnnotatedPattern() { assertEquals("10/31/09 1:05", binder.getBindingResult().getFieldValue("dateAnnotatedPattern")); } + @Test + public void testBindDateAnnotatedPatternWithGlobalFormat() { + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + DateFormatter dateFormatter = new DateFormatter(); + dateFormatter.setIso(ISO.DATE_TIME); + registrar.setFormatter(dateFormatter); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotatedPattern", "10/31/09 1:05"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("10/31/09 1:05", binder.getBindingResult().getFieldValue("dateAnnotatedPattern")); + } + @Test public void testBindDateTimeOverflow() { MutablePropertyValues propertyValues = new MutablePropertyValues(); From 0f2efdbe979e4928c60654d45d3e53575ec83e8e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 30 Oct 2019 18:51:41 +0100 Subject: [PATCH 015/322] Polish See gh-23884 --- .../java/org/springframework/http/codec/CodecConfigurer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 40a18a08d8f..3d4c625b5d5 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 @@ -155,9 +155,10 @@ interface DefaultCodecs { * apply. In 5.1 most codecs are not limited except {@code FormHttpMessageReader} * which is limited to 256K. In 5.2 all codecs are limited to 256K by default. * @param byteCount the max number of bytes to buffer, or -1 for unlimited - * @sine 5.1.11 + * @since 5.1.11 */ void maxInMemorySize(int byteCount); + /** * Whether to log form data at DEBUG level, and headers at TRACE level. * Both may contain sensitive information. From e731a0a164ba36a419596a98556193508329812c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 31 Oct 2019 14:16:48 +0000 Subject: [PATCH 016/322] Use int for maxParts instead of long --- .../codec/multipart/SynchronossPartHttpMessageReader.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index 558841b10a3..f52638fbfad 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -88,7 +88,7 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem private long maxDiskUsagePerPart = -1; - private long maxParts = -1; + private int maxParts = -1; /** @@ -139,7 +139,7 @@ public long getMaxDiskUsagePerPart() { * Specify the maximum number of parts allowed in a given multipart request. * @since 5.1.11 */ - public void setMaxParts(long maxParts) { + public void setMaxParts(int maxParts) { this.maxParts = maxParts; } @@ -147,7 +147,7 @@ public void setMaxParts(long maxParts) { * Return the {@link #setMaxParts configured} limit on the number of parts. * @since 5.1.11 */ - public long getMaxParts() { + public int getMaxParts() { return this.maxParts; } From 38a1caefb8c53ae30f1c3cfbc3cf34b537617348 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Sat, 2 Nov 2019 07:17:14 +0000 Subject: [PATCH 017/322] Next Development Version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6eb5fa85e67..52cd8c3529f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.1.11.BUILD-SNAPSHOT +version=5.1.12.BUILD-SNAPSHOT From 2e4944198df840ee93e466478489dc614104693f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Nov 2019 15:36:25 +0000 Subject: [PATCH 018/322] Fix condition in ServletInvocableHandlerMethod Closes gh-23775 --- .../ServletInvocableHandlerMethod.java | 2 +- .../ServletInvocableHandlerMethodTests.java | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index 9c383993e30..aa780968c27 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -165,7 +165,7 @@ private boolean isRequestNotModified(ServletWebRequest webRequest) { } private void disableContentCachingIfNecessary(ServletWebRequest webRequest) { - if (!isRequestNotModified(webRequest)) { + if (isRequestNotModified(webRequest)) { HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); Assert.notNull(response, "Expected HttpServletResponse"); if (StringUtils.hasText(response.getHeader("ETag"))) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java index 04c3214e7c1..993765669ac 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -31,6 +31,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AliasFor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; @@ -46,6 +47,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.filter.ShallowEtagHeaderFilter; import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -139,6 +141,21 @@ public void invokeAndHandle_VoidRequestNotModified() throws Exception { this.mavContainer.isRequestHandled()); } + @Test // gh-23775 + public void invokeAndHandle_VoidNotModifiedWithEtag() throws Exception { + String etag = "\"deadb33f8badf00d\""; + this.request.addHeader(HttpHeaders.IF_NONE_MATCH, etag); + this.webRequest.checkNotModified(etag); + + ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "notModified"); + handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer); + + assertTrue("Null return value + 'not modified' request should result in 'request handled'", + this.mavContainer.isRequestHandled()); + + assertEquals(true, this.request.getAttribute(ShallowEtagHeaderFilter.class.getName() + ".STREAMING")); + } + @Test // SPR-9159 public void invokeAndHandle_NotVoidWithResponseStatusAndReason() throws Exception { ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "responseStatusWithReason"); From e0faaa4807b9c612fb6a4a0ea59d73bf7eea3153 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Nov 2019 16:17:41 +0000 Subject: [PATCH 019/322] Relax domain name checks in ResponseCookie Closes gh-23924 --- .../springframework/http/ResponseCookie.java | 2 +- .../http/ResponseCookieTests.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java index 65afc80a506..68373c46388 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java @@ -374,7 +374,7 @@ public static void validateDomain(@Nullable String domain) { } int char1 = domain.charAt(0); int charN = domain.charAt(domain.length() - 1); - if (char1 == '.' || char1 == '-' || charN == '.' || charN == '-') { + if (char1 == '-' || charN == '.' || charN == '-') { throw new IllegalArgumentException("Invalid first/last char in cookie domain: " + domain); } for (int i = 0, c = -1; i < domain.length(); i++) { diff --git a/spring-web/src/test/java/org/springframework/http/ResponseCookieTests.java b/spring-web/src/test/java/org/springframework/http/ResponseCookieTests.java index 79affb0daf3..a1a021f6cb2 100644 --- a/spring-web/src/test/java/org/springframework/http/ResponseCookieTests.java +++ b/spring-web/src/test/java/org/springframework/http/ResponseCookieTests.java @@ -85,6 +85,31 @@ public void valueChecks() { }); } + @Test + public void domainChecks() { + + Arrays.asList("abc", "abc.org", "abc-def.org", "abc3.org", ".abc.org") + .forEach(domain -> ResponseCookie.from("n", "v").domain(domain).build()); + + Arrays.asList("-abc.org", "abc.org.", "abc.org-", "-abc.org", "abc.org-") + .forEach(domain -> { + try { + ResponseCookie.from("n", "v").domain(domain).build(); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), Matchers.containsString("Invalid first/last char")); + } + }); + Arrays.asList("abc..org", "abc.-org", "abc-.org") + .forEach(domain -> { + try { + ResponseCookie.from("n", "v").domain(domain).build(); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), Matchers.containsString("invalid cookie domain char")); + } + }); + } } From 48b22292ff513229a12185110f9acca8b95a4f04 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Nov 2019 17:39:41 +0000 Subject: [PATCH 020/322] Fix issue with path matching options Closes gh-23907 --- .../RequestMappingHandlerMapping.java | 2 +- .../RequestMappingHandlerMapping.java | 2 +- .../RequestMappingHandlerMappingTests.java | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index 40fca4b8ffc..a6c5c656a59 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -153,7 +153,7 @@ protected RequestMappingInfo getMappingForMethod(Method method, Class handler if (this.embeddedValueResolver != null) { prefix = this.embeddedValueResolver.resolveStringValue(prefix); } - info = RequestMappingInfo.paths(prefix).build().combine(info); + info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); break; } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 7363ed4b139..6979a9e09af 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -226,7 +226,7 @@ protected RequestMappingInfo getMappingForMethod(Method method, Class handler } String prefix = getPathPrefix(handlerType); if (prefix != null) { - info = RequestMappingInfo.paths(prefix).build().combine(info); + info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); } } return info; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 67d056d8200..8d836b69bda 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -33,6 +33,7 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.http.MediaType; +import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.stereotype.Controller; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; @@ -153,6 +154,24 @@ public void pathPrefix() throws NoSuchMethodException { assertEquals(Collections.singleton("/api/user/{id}"), info.getPatternsCondition().getPatterns()); } + @Test // gh-23907 + public void pathPrefixPreservesPathMatchingSettings() throws NoSuchMethodException { + this.handlerMapping.setUseSuffixPatternMatch(false); + this.handlerMapping.setPathPrefixes(Collections.singletonMap("/api", HandlerTypePredicate.forAnyHandlerType())); + this.handlerMapping.afterPropertiesSet(); + + Method method = ComposedAnnotationController.class.getMethod("get"); + RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, ComposedAnnotationController.class); + + assertNotNull(info); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/get"); + assertNotNull(info.getPatternsCondition().getMatchingCondition(request)); + + request = new MockHttpServletRequest("GET", "/api/get.pdf"); + assertNull(info.getPatternsCondition().getMatchingCondition(request)); + } + @Test public void resolveRequestMappingViaComposedAnnotation() throws Exception { RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); From ca3440cb42881ab6fe209f65b2eeed1b1cfa9984 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 6 Nov 2019 22:23:20 +0100 Subject: [PATCH 021/322] Re-enable support for invalid Expires attributes in MockCookie Changes introduced in commit 9b2087618bba6cd546066e2f27deb166f6870a64 caused a regression for Cookie support in MockHttpServletResponse. Specifically, an Expires attribute that cannot be parsed using `ZonedDateTime.parse()` now results in an exception; whereas, previously an entry such as `Expires=0` was allowed. This commit fixes this issue in MockCookie by catching and ignoring any DateTimeException thrown while attempting to parse an Expires attribute. Closes gh-23911 --- .../springframework/mock/web/MockCookie.java | 11 ++++++-- .../mock/web/MockCookieTests.java | 24 +++++++++++++--- .../web/MockHttpServletResponseTests.java | 28 +++++++++++++++++++ .../mock/web/test/MockCookie.java | 11 ++++++-- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java index 17c3f2c027c..5f67f51177c 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java @@ -16,6 +16,7 @@ package org.springframework.mock.web; +import java.time.DateTimeException; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -31,6 +32,7 @@ * * @author Vedran Pavic * @author Juergen Hoeller + * @author Sam Brannen * @since 5.1 */ public class MockCookie extends Cookie { @@ -119,8 +121,13 @@ else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) { cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader))); } else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) { - cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), - DateTimeFormatter.RFC_1123_DATE_TIME)); + try { + cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), + DateTimeFormatter.RFC_1123_DATE_TIME)); + } + catch (DateTimeException ex) { + // ignore invalid date formats + } } else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) { cookie.setPath(extractAttributeValue(attribute, setCookieHeader)); diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java index 3253c86d329..d846031a1f5 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java @@ -16,13 +16,13 @@ package org.springframework.mock.web; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - import static org.junit.Assert.*; /** @@ -84,6 +84,22 @@ public void parseHeaderWithAttributes() { assertEquals("Lax", cookie.getSameSite()); } + @Test + public void parseHeaderWithZeroExpiresAttribute() { + MockCookie cookie = MockCookie.parse("SESSION=123; Expires=0"); + + assertCookie(cookie, "SESSION", "123"); + assertNull(cookie.getExpires()); + } + + @Test + public void parseHeaderWithBogusExpiresAttribute() { + MockCookie cookie = MockCookie.parse("SESSION=123; Expires=bogus"); + + assertCookie(cookie, "SESSION", "123"); + assertNull(cookie.getExpires()); + } + private void assertCookie(MockCookie cookie, String name, String value) { assertEquals(name, cookie.getName()); assertEquals(value, cookie.getValue()); @@ -116,7 +132,7 @@ public void parseInvalidAttribute() { public void parseHeaderWithAttributesCaseSensitivity() { MockCookie cookie = MockCookie.parse("SESSION=123; domain=example.com; max-age=60; " + "expires=Tue, 8 Oct 2019 19:50:00 GMT; path=/; secure; httponly; samesite=Lax"); - + assertCookie(cookie, "SESSION", "123"); assertEquals("example.com", cookie.getDomain()); assertEquals(60, cookie.getMaxAge()); diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 70f6f002a04..b7a0df5dbc7 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -351,6 +351,20 @@ public void setCookieHeaderWithExpiresAttribute() { assertEquals(cookieValue, response.getHeader(HttpHeaders.SET_COOKIE)); } + /** + * @since 5.1.12 + */ + @Test + public void setCookieHeaderWithZeroExpiresAttribute() { + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0"; + response.setHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertNumCookies(1); + String header = response.getHeader(HttpHeaders.SET_COOKIE); + assertNotEquals(cookieValue, header); + // We don't assert the actual Expires value since it is based on the current time. + assertTrue(header.startsWith("SESSION=123; Path=/; Max-Age=100; Expires=")); + } + @Test public void addCookieHeader() { response.addHeader(HttpHeaders.SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax"); @@ -375,6 +389,20 @@ public void addCookieHeaderWithExpiresAttribute() { assertEquals(cookieValue, response.getHeader(HttpHeaders.SET_COOKIE)); } + /** + * @since 5.1.12 + */ + @Test + public void addCookieHeaderWithZeroExpiresAttribute() { + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0"; + response.addHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertNumCookies(1); + String header = response.getHeader(HttpHeaders.SET_COOKIE); + assertNotEquals(cookieValue, header); + // We don't assert the actual Expires value since it is based on the current time. + assertTrue(header.startsWith("SESSION=123; Path=/; Max-Age=100; Expires=")); + } + @Test public void addCookie() { MockCookie mockCookie = new MockCookie("SESSION", "123"); diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java index 3bea81f01d4..408e2ecefdd 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockCookie.java @@ -16,6 +16,7 @@ package org.springframework.mock.web.test; +import java.time.DateTimeException; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -31,6 +32,7 @@ * * @author Vedran Pavic * @author Juergen Hoeller + * @author Sam Brannen * @since 5.1 */ public class MockCookie extends Cookie { @@ -119,8 +121,13 @@ else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) { cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader))); } else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) { - cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), - DateTimeFormatter.RFC_1123_DATE_TIME)); + try { + cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), + DateTimeFormatter.RFC_1123_DATE_TIME)); + } + catch (DateTimeException ex) { + // ignore invalid date formats + } } else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) { cookie.setPath(extractAttributeValue(attribute, setCookieHeader)); From 1a057654b2daa461ddbf0979bd450f8a6fd65a01 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Nov 2019 21:31:42 +0000 Subject: [PATCH 022/322] Defer ExchangeFilterFunction to subscription time Previously fixed in 5.2 via d46359. Now also backported to 5.1.x. Closes gh-23909 --- .../function/client/DefaultWebClient.java | 3 +- .../client/ExchangeFilterFunction.java | 6 +- .../client/DefaultWebClientTests.java | 62 ++++++++++++++----- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 932747b0494..b1edc344e4f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -317,7 +317,8 @@ public Mono exchange() { ClientRequest request = (this.inserter != null ? initRequestBuilder().body(this.inserter).build() : initRequestBuilder().build()); - return exchangeFunction.exchange(request).switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR); + return Mono.defer(() -> exchangeFunction.exchange(request)) + .switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR); } private ClientRequest.Builder initRequestBuilder() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index d2d35a6f755..12fb186a539 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.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. @@ -23,7 +23,9 @@ import org.springframework.util.Assert; /** - * Represents a function that filters an{@linkplain ExchangeFunction exchange function}. + * Represents a function that filters an {@linkplain ExchangeFunction exchange function}. + *

The filter is executed when a {@code Subscriber} subscribes to the + * {@code Publisher} returned by the {@code WebClient}. * * @author Arjen Poutsma * @since 5.0 diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 4460baf368a..718eba96730 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.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. @@ -34,13 +34,20 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; /** * Unit tests for {@link DefaultWebClient}. * * @author Rossen Stoyanchev + * @author Brian Clozel */ public class DefaultWebClientTests { @@ -56,14 +63,15 @@ public class DefaultWebClientTests { public void setup() { MockitoAnnotations.initMocks(this); this.exchangeFunction = mock(ExchangeFunction.class); - when(this.exchangeFunction.exchange(this.captor.capture())).thenReturn(Mono.empty()); + ClientResponse mockResponse = mock(ClientResponse.class); + when(this.exchangeFunction.exchange(this.captor.capture())).thenReturn(Mono.just(mockResponse)); this.builder = WebClient.builder().baseUrl("/base").exchangeFunction(this.exchangeFunction); } @Test public void basic() { - this.builder.build().get().uri("/path").exchange(); + this.builder.build().get().uri("/path").exchange().block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("/base/path", request.url().toString()); @@ -75,7 +83,8 @@ public void basic() { public void uriBuilder() { this.builder.build().get() .uri(builder -> builder.path("/path").queryParam("q", "12").build()) - .exchange(); + .exchange() + .block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("/base/path?q=12", request.url().toString()); @@ -86,7 +95,8 @@ public void uriBuilder() { public void uriBuilderWithPathOverride() { this.builder.build().get() .uri(builder -> builder.replacePath("/path").build()) - .exchange(); + .exchange() + .block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("/path", request.url().toString()); @@ -97,7 +107,8 @@ public void uriBuilderWithPathOverride() { public void requestHeaderAndCookie() { this.builder.build().get().uri("/path").accept(MediaType.APPLICATION_JSON) .cookies(cookies -> cookies.add("id", "123")) // SPR-16178 - .exchange(); + .exchange() + .block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("application/json", request.headers().getFirst("Accept")); @@ -111,7 +122,7 @@ public void defaultHeaderAndCookie() { .defaultHeader("Accept", "application/json").defaultCookie("id", "123") .build(); - client.get().uri("/path").exchange(); + client.get().uri("/path").exchange().block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("application/json", request.headers().getFirst("Accept")); @@ -126,7 +137,8 @@ public void defaultHeaderAndCookieOverrides() { .defaultCookie("id", "123") .build(); - client.get().uri("/path").header("Accept", "application/xml").cookie("id", "456").exchange(); + client.get().uri("/path").header("Accept", "application/xml").cookie("id", "456") + .exchange().block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("application/xml", request.headers().getFirst("Accept")); @@ -151,7 +163,7 @@ public void defaultRequest() { try { context.set("bar"); - client.get().uri("/path").attribute("foo", "bar").exchange(); + client.get().uri("/path").attribute("foo", "bar").exchange().block(Duration.ofSeconds(10)); } finally { context.remove(); @@ -219,7 +231,8 @@ public void withStringAttribute() { this.builder.filter(filter).build() .get().uri("/path") .attribute("foo", "bar") - .exchange(); + .exchange() + .block(Duration.ofSeconds(10)); assertEquals("bar", actual.get("foo")); @@ -238,7 +251,8 @@ public void withNullAttribute() { this.builder.filter(filter).build() .get().uri("/path") .attribute("foo", null) - .exchange(); + .exchange() + .block(Duration.ofSeconds(10)); assertNull(actual.get("foo")); @@ -254,7 +268,7 @@ public void apply() { .defaultCookie("id", "123")) .build(); - client.get().uri("/path").exchange(); + client.get().uri("/path").exchange().block(Duration.ofSeconds(10)); ClientRequest request = verifyAndGetRequest(); assertEquals("application/json", request.headers().getFirst("Accept")); @@ -264,11 +278,31 @@ public void apply() { @Test public void switchToErrorOnEmptyClientResponseMono() { + ExchangeFunction exchangeFunction = mock(ExchangeFunction.class); + when(exchangeFunction.exchange(any())).thenReturn(Mono.empty()); + WebClient.Builder builder = WebClient.builder().baseUrl("/base").exchangeFunction(exchangeFunction); StepVerifier.create(builder.build().get().uri("/path").exchange()) .expectErrorMessage("The underlying HTTP client completed without emitting a response.") .verify(Duration.ofSeconds(5)); } + @Test // gh-23909 + public void shouldApplyFiltersAtSubscription() { + WebClient client = this.builder + .filter((request, next) -> + next.exchange(ClientRequest + .from(request) + .header("Custom", "value") + .build())) + .build(); + Mono exchange = client.get().uri("/path").exchange(); + verifyZeroInteractions(this.exchangeFunction); + exchange.block(Duration.ofSeconds(10)); + ClientRequest request = verifyAndGetRequest(); + assertEquals("value", request.headers().getFirst("Custom")); + } + + private ClientRequest verifyAndGetRequest() { ClientRequest request = this.captor.getValue(); Mockito.verify(this.exchangeFunction).exchange(request); From 1301c7e95d3fade15b39036d64d4a5d4101f2d9b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Nov 2019 21:37:59 +0000 Subject: [PATCH 023/322] Update WebMvcConfigurer#addInterceptors Javadoc Closes gh-23908 --- .../config/annotation/WebMvcConfigurer.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index 6bda23a5ea6..a6e8ee90578 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.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. @@ -90,15 +90,9 @@ default void addFormatters(FormatterRegistry registry) { /** * Add Spring MVC lifecycle interceptors for pre- and post-processing of - * controller method invocations. Interceptors can be registered to apply - * to all requests or be limited to a subset of URL patterns. - *

Note that interceptors registered here only apply to - * controllers and not to resource handler requests. To intercept requests for - * static resources either declare a - * {@link org.springframework.web.servlet.handler.MappedInterceptor MappedInterceptor} - * bean or switch to advanced configuration mode by extending - * {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport - * WebMvcConfigurationSupport} and then override {@code resourceHandlerMapping}. + * controller method invocations and resource handler requests. + * Interceptors can be registered to apply to all requests or be limited + * to a subset of URL patterns. */ default void addInterceptors(InterceptorRegistry registry) { } From 32adf77b221e659576b6fdfc7af0a73bf7019063 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 7 Nov 2019 09:42:36 +0100 Subject: [PATCH 024/322] Corrupt multipart should not hang SynchronossPartHttpMessageReader This commit notifies the Synchronoss listener that the buffer stream has ended. See gh-23768 --- .../codec/multipart/SynchronossPartHttpMessageReader.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index f52638fbfad..3c226407466 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -262,6 +262,13 @@ protected void hookOnError(Throwable ex) { } } + @Override + protected void hookOnComplete() { + if (this.listener != null) { + this.listener.onAllPartsFinished(); + } + } + @Override protected void hookFinally(SignalType type) { try { From d3d40983d694f4e522f73025681bff3464c4beba Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 7 Nov 2019 09:09:42 +0100 Subject: [PATCH 025/322] Corrupt multipart should not hang SynchronossPartHttpMessageReader This commit notifies the Synchronoss listener that the buffer stream has ended. Closes gh-23768 (cherry picked from commit bf36f49dc5454dae4d21ead33474eb3783d34a05) --- ...SynchronossPartHttpMessageReaderTests.java | 20 +++++++++++++++++++ .../http/codec/multipart/invalid.multipart | 5 +++++ 2 files changed, 25 insertions(+) create mode 100644 spring-web/src/test/resources/org/springframework/http/codec/multipart/invalid.multipart diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java index 74d3fb0db0c..03f848a1a4c 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; +import java.nio.channels.ReadableByteChannel; import java.time.Duration; import java.util.Map; import java.util.function.Consumer; @@ -44,6 +45,7 @@ import org.springframework.util.MultiValueMap; import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -149,6 +151,24 @@ public void readPartsWithoutDemand() { subscriber.cancel(); } + @Test + public void gh23768() throws IOException { + ReadableByteChannel channel = new ClassPathResource("invalid.multipart", getClass()).readableChannel(); + Flux body = DataBufferUtils.readByteChannel(() -> channel, this.bufferFactory, 1024); + + MediaType contentType = new MediaType("multipart", "form-data", + singletonMap("boundary", "NbjrKgjbsaMLdnMxMfDpD6myWomYc0qNX0w")); + ServerHttpRequest request = MockServerHttpRequest.post("/") + .contentType(contentType) + .body(body); + + Mono> parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap()); + + StepVerifier.create(parts) + .assertNext(result -> assertThat(result).isEmpty()) + .verifyComplete(); + } + @Test public void readTooManyParts() { testMultipartExceptions(reader -> reader.setMaxParts(1), ex -> { diff --git a/spring-web/src/test/resources/org/springframework/http/codec/multipart/invalid.multipart b/spring-web/src/test/resources/org/springframework/http/codec/multipart/invalid.multipart new file mode 100644 index 00000000000..9f09680d043 --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/codec/multipart/invalid.multipart @@ -0,0 +1,5 @@ +--NbjrKgjbsaMLdnMxMfDpD6myWomYc0qNX0w +Content-Disposition: form-data; name="part-00-name" + +post-payload-text-23456789ABCDEF:post-payload-0001-3456789ABCDEF:post-payload-0002-3456789ABCDEF:post-payload-0003-3456789ABCDEF +--NbjrKgjbsaMLdnMxMfDpD6myWomYc From e75556bc7b0a232c0b809208af5908727dcf3915 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 7 Nov 2019 14:29:47 +0100 Subject: [PATCH 026/322] Polishing --- .../SynchronossPartHttpMessageReader.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index 3c226407466..f4194071b9f 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -246,19 +246,9 @@ protected void hookOnNext(DataBuffer buffer) { @Override protected void hookOnError(Throwable ex) { - try { - if (this.parser != null) { - this.parser.close(); - } - } - catch (IOException ex2) { - // ignore - } - finally { - if (this.listener != null) { - int index = this.storageFactory.getCurrentPartIndex(); - this.listener.onError("Failure while parsing part[" + index + "]", ex); - } + if (this.listener != null) { + int index = this.storageFactory.getCurrentPartIndex(); + this.listener.onError("Failure while parsing part[" + index + "]", ex); } } @@ -277,9 +267,7 @@ protected void hookFinally(SignalType type) { } } catch (IOException ex) { - if (this.listener != null) { - this.listener.onError("Error while closing parser", ex); - } + // ignore } } From c2e7b6341c8e1dd0404689c56e460bc856290cb7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 7 Nov 2019 15:03:28 +0100 Subject: [PATCH 027/322] Polishing --- .../multipart/SynchronossPartHttpMessageReaderTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java index 03f848a1a4c..0b2884450aa 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReaderTests.java @@ -154,7 +154,7 @@ public void readPartsWithoutDemand() { @Test public void gh23768() throws IOException { ReadableByteChannel channel = new ClassPathResource("invalid.multipart", getClass()).readableChannel(); - Flux body = DataBufferUtils.readByteChannel(() -> channel, this.bufferFactory, 1024); + Flux body = DataBufferUtils.readByteChannel(() -> channel, new DefaultDataBufferFactory(), 1024); MediaType contentType = new MediaType("multipart", "form-data", singletonMap("boundary", "NbjrKgjbsaMLdnMxMfDpD6myWomYc0qNX0w")); @@ -165,7 +165,7 @@ public void gh23768() throws IOException { Mono> parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap()); StepVerifier.create(parts) - .assertNext(result -> assertThat(result).isEmpty()) + .assertNext(result -> assertTrue(result.isEmpty())) .verifyComplete(); } From bdb1a81a39b9dd68186a805286c86109e6908567 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 7 Nov 2019 22:35:21 +0100 Subject: [PATCH 028/322] Use Method.getParameterCount() when full type array is never needed --- .../org/springframework/beans/PropertyDescriptorUtils.java | 5 ++--- .../beans/factory/support/DisposableBeanAdapter.java | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java index c5ac50cb4b4..aa9909822d1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.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. @@ -66,8 +66,7 @@ public static Class findPropertyType(@Nullable Method readMethod, @Nullable M Class propertyType = null; if (readMethod != null) { - Class[] params = readMethod.getParameterTypes(); - if (params.length != 0) { + if (readMethod.getParameterCount() != 0) { throw new IntrospectionException("Bad read method arg count: " + readMethod); } propertyType = readMethod.getReturnType(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index 36768418cdd..c506958cf28 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -311,9 +311,9 @@ private Method findDestroyMethod(String name) { * assuming a "force" parameter), else logging an error. */ private void invokeCustomDestroyMethod(final Method destroyMethod) { - Class[] paramTypes = destroyMethod.getParameterTypes(); - final Object[] args = new Object[paramTypes.length]; - if (paramTypes.length == 1) { + int paramCount = destroyMethod.getParameterCount(); + final Object[] args = new Object[paramCount]; + if (paramCount == 1) { args[0] = Boolean.TRUE; } if (logger.isTraceEnabled()) { From 088a653318a2bcbc5eb85aabb5189438ef496c2a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 7 Nov 2019 22:35:30 +0100 Subject: [PATCH 029/322] Upgrade to RxJava 2.2.14, OkHttp 3.14.4, Awaitility 3.1.6 --- build.gradle | 2 +- spring-context/spring-context.gradle | 2 +- spring-web/spring-web.gradle | 4 ++-- spring-webflux/spring-webflux.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index ce556824abb..7b125b06deb 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ ext { reactorVersion = "Californium-SR13" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" - rxjava2Version = "2.2.13" + rxjava2Version = "2.2.14" slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" tomcatVersion = "9.0.27" diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 86d3f29a710..c9ae1662c3a 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -28,7 +28,7 @@ dependencies { testCompile("org.codehaus.groovy:groovy-xml:${groovyVersion}") testCompile("org.apache.commons:commons-pool2:2.6.0") testCompile("javax.inject:javax.inject-tck:1") - testCompile("org.awaitility:awaitility:3.1.3") + testCompile("org.awaitility:awaitility:3.1.6") testRuntime("javax.xml.bind:jaxb-api:2.3.1") testRuntime("org.glassfish:javax.el:3.0.1-b08") testRuntime("org.javamoney:moneta:1.3") diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 9e7a5c120ab..390f697127d 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -38,7 +38,7 @@ dependencies { exclude group: "javax.servlet", module: "javax.servlet-api" } optional("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3") - optional("com.squareup.okhttp3:okhttp:3.14.3") + optional("com.squareup.okhttp3:okhttp:3.14.4") optional("org.apache.httpcomponents:httpclient:4.5.10") { exclude group: "commons-logging", module: "commons-logging" } @@ -75,7 +75,7 @@ dependencies { testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") testCompile("org.eclipse.jetty:jetty-server") testCompile("org.eclipse.jetty:jetty-servlet") - testCompile("com.squareup.okhttp3:mockwebserver:3.14.3") + testCompile("com.squareup.okhttp3:mockwebserver:3.14.4") testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") testCompile("org.skyscreamer:jsonassert:1.5.0") testCompile("org.xmlunit:xmlunit-matchers:2.6.2") diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index acaf6c4d3a0..88483ef3517 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -51,7 +51,7 @@ dependencies { testCompile("org.eclipse.jetty:jetty-server") testCompile("org.eclipse.jetty:jetty-servlet") testCompile("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3") - testCompile("com.squareup.okhttp3:mockwebserver:3.14.3") + testCompile("com.squareup.okhttp3:mockwebserver:3.14.4") testCompile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-script-util:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}") From 3d83f869d9b11da87b9ac1ec48f0105862f2b80e Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 9 Nov 2019 18:03:07 +0100 Subject: [PATCH 030/322] Fix Javadoc for SmartLifecycle.DEFAULT_PHASE regarding ordering Closes gh-23956 --- .../context/SmartLifecycle.java | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index 4ee111d3d85..06f98a4048c 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.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. @@ -17,19 +17,21 @@ package org.springframework.context; /** - * An extension of the {@link Lifecycle} interface for those objects that require to - * be started upon ApplicationContext refresh and/or shutdown in a particular order. - * The {@link #isAutoStartup()} return value indicates whether this object should + * An extension of the {@link Lifecycle} interface for those objects that require + * to be started upon {@code ApplicationContext} refresh and/or shutdown in a + * particular order. + * + *

The {@link #isAutoStartup()} return value indicates whether this object should * be started at the time of a context refresh. The callback-accepting * {@link #stop(Runnable)} method is useful for objects that have an asynchronous * shutdown process. Any implementation of this interface must invoke the * callback's {@code run()} method upon shutdown completion to avoid unnecessary - * delays in the overall ApplicationContext shutdown. + * delays in the overall {@code ApplicationContext} shutdown. * *

This interface extends {@link Phased}, and the {@link #getPhase()} method's - * return value indicates the phase within which this Lifecycle component should - * be started and stopped. The startup process begins with the lowest phase - * value and ends with the highest phase value ({@code Integer.MIN_VALUE} + * return value indicates the phase within which this {@code Lifecycle} component + * should be started and stopped. The startup process begins with the lowest + * phase value and ends with the highest phase value ({@code Integer.MIN_VALUE} * is the lowest possible, and {@code Integer.MAX_VALUE} is the highest possible). * The shutdown process will apply the reverse order. Any components with the * same value will be arbitrarily ordered within the same phase. @@ -44,9 +46,11 @@ * *

Any {@code Lifecycle} components within the context that do not also * implement {@code SmartLifecycle} will be treated as if they have a phase - * value of 0. That way a {@code SmartLifecycle} implementation may start - * before those {@code Lifecycle} components if it has a negative phase value, - * or it may start after those components if it has a positive phase value. + * value of {@code 0}. This allows a {@code SmartLifecycle} component to start + * before those {@code Lifecycle} components if the {@code SmartLifecycle} + * component has a negative phase value, or the {@code SmartLifecycle} component + * may start after those {@code Lifecycle} components if the {@code SmartLifecycle} + * component has a positive phase value. * *

Note that, due to the auto-startup support in {@code SmartLifecycle}, a * {@code SmartLifecycle} bean instance will usually get initialized on startup @@ -55,6 +59,7 @@ * * @author Mark Fisher * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 * @see LifecycleProcessor * @see ConfigurableApplicationContext @@ -63,9 +68,10 @@ public interface SmartLifecycle extends Lifecycle, Phased { /** * The default phase for {@code SmartLifecycle}: {@code Integer.MAX_VALUE}. - *

This is different from the common phase 0 associated with regular + *

This is different from the common phase {@code 0} associated with regular * {@link Lifecycle} implementations, putting the typically auto-started - * {@code SmartLifecycle} beans into a separate later shutdown phase. + * {@code SmartLifecycle} beans into a later startup phase and an earlier + * shutdown phase. * @since 5.1 * @see #getPhase() * @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle) @@ -115,7 +121,8 @@ default void stop(Runnable callback) { /** * Return the phase that this lifecycle object is supposed to run in. *

The default implementation returns {@link #DEFAULT_PHASE} in order to - * let stop callbacks execute after regular {@code Lifecycle} implementations. + * let {@code stop()} callbacks execute after regular {@code Lifecycle} + * implementations. * @see #isAutoStartup() * @see #start() * @see #stop(Runnable) From 64db939e4abf1736f284f1c21921fae9a258a82d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Nov 2019 12:04:28 +0000 Subject: [PATCH 031/322] Javadoc update for content negotiation Closes gh-23409 --- .../ContentNegotiationManagerFactoryBean.java | 44 +++++++++++-------- .../ContentNegotiationConfigurer.java | 43 +++++++++--------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java index fe00ab162ad..f49cff63952 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.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. @@ -36,49 +36,55 @@ /** * Factory to create a {@code ContentNegotiationManager} and configure it with - * one or more {@link ContentNegotiationStrategy} instances. + * {@link ContentNegotiationStrategy} instances. * - *

As of 5.0 you can set the exact strategies to use via - * {@link #setStrategies(List)}. - * - *

As an alternative you can also rely on the set of defaults described below - * which can be turned on or off or customized through the methods of this - * builder: + *

This factory offers properties that in turn result in configuring the + * underlying strategies. The table below shows the property names, their + * default settings, as well as the strategies that they help to configure: * * * * + * * - * + * * * - * - * - * + * + * + * + * * * * - * + * + * * * * * - * - * + * + * + * * * * - * - * + * + * + * * * * + * * - * + * * *
Property SetterDefault ValueUnderlying StrategyDefault SettingEnabled Or Not
{@link #setFavorPathExtension}{@link PathExtensionContentNegotiationStrategy Path Extension strategy}On{@link #setFavorPathExtension favorPathExtension}true{@link PathExtensionContentNegotiationStrategy}Enabled
{@link #setFavorParameter favorParameter}{@link ParameterContentNegotiationStrategy Parameter strategy}false{@link ParameterContentNegotiationStrategy}Off
{@link #setIgnoreAcceptHeader ignoreAcceptHeader}{@link HeaderContentNegotiationStrategy Header strategy}Onfalse{@link HeaderContentNegotiationStrategy}Enabled
{@link #setDefaultContentType defaultContentType}{@link FixedContentNegotiationStrategy Fixed content strategy}Not setnull{@link FixedContentNegotiationStrategy}Off
{@link #setDefaultContentTypeStrategy defaultContentTypeStrategy}null{@link ContentNegotiationStrategy}Not setOff
* - * Note: if you must use URL-based content type resolution, + *

As of 5.0 you can set the exact strategies to use via + * {@link #setStrategies(List)}. + * + *

Note: if you must use URL-based content type resolution, * the use of a query parameter is simpler and preferable to the use of a path * extension since the latter can cause issues with URI variables, path * parameters, and URI decoding. Consider setting {@link #setFavorPathExtension} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java index 58b100acaaf..6b0c2522ea5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.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. @@ -38,50 +38,53 @@ * Creates a {@code ContentNegotiationManager} and configures it with * one or more {@link ContentNegotiationStrategy} instances. * - *

As of 5.0 you can set the exact strategies to use via - * {@link #strategies(List)}. - * - *

As an alternative you can also rely on the set of defaults described below - * which can be turned on or off or customized through the methods of this - * builder: + *

This factory offers properties that in turn result in configuring the + * underlying strategies. The table below shows the property names, their + * default settings, as well as the strategies that they help to configure: * * * - * + * + * * - * + * * * * - * - * + * + * + * * * * - * + * + * * * * * - * - * + * + * + * * * * - * - * + * + * + * * * * + * * - * + * * *
Configurer PropertyProperty SetterDefault ValueUnderlying StrategyDefault SettingEnabled Or Not
{@link #favorPathExtension}{@link PathExtensionContentNegotiationStrategy Path Extension strategy}Ontrue{@link PathExtensionContentNegotiationStrategy}Enabled
{@link #favorParameter}{@link ParameterContentNegotiationStrategy Parameter strategy}false{@link ParameterContentNegotiationStrategy}Off
{@link #ignoreAcceptHeader}{@link HeaderContentNegotiationStrategy Header strategy}Onfalse{@link HeaderContentNegotiationStrategy}Enabled
{@link #defaultContentType}{@link FixedContentNegotiationStrategy Fixed content strategy}Not setnull{@link FixedContentNegotiationStrategy}Off
{@link #defaultContentTypeStrategy}null{@link ContentNegotiationStrategy}Not setOff
* - *

The order in which strategies are configured is fixed. You can only turn - * them on or off. + *

As of 5.0 you can set the exact strategies to use via + * {@link #strategies(List)}. * - * Note: if you must use URL-based content type resolution, + *

Note: if you must use URL-based content type resolution, * the use of a query parameter is simpler and preferable to the use of a path * extension since the latter can cause issues with URI variables, path * parameters, and URI decoding. Consider setting {@link #favorPathExtension} From 8e65834c44fdb860960c568b714cef3e04aaff9d Mon Sep 17 00:00:00 2001 From: YuDongYing <987425112@qq.com> Date: Tue, 5 Nov 2019 16:44:23 +0800 Subject: [PATCH 032/322] Fix schemaZip Gradle task on MS Windows Prior to this commit, the schemaZip Gradle task failed to find Spring schema files on MS Windows due to path separators hard coded to forward slashes that are not compatible with the Windows operating system. Consequently, a full build failed on Windows since the distZip task was not able to locate the zipped schema archive that the schemaZip task failed to create. This commit fixes this by updating the schemaZip task to search for schema files using backslashes as well as forward slashes. Closes gh-23933 --- gradle/docs.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/docs.gradle b/gradle/docs.gradle index 6b1b32576e5..ab6f2822f9f 100644 --- a/gradle/docs.gradle +++ b/gradle/docs.gradle @@ -149,14 +149,14 @@ task schemaZip(type: Zip) { def Properties schemas = new Properties(); subproject.sourceSets.main.resources.find { - it.path.endsWith("META-INF/spring.schemas") + (it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas")) }?.withInputStream { schemas.load(it) } for (def key : schemas.keySet()) { def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1') assert shortName != key File xsdFile = subproject.sourceSets.main.resources.find { - it.path.endsWith(schemas.get(key)) + (it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\'))) } assert xsdFile != null into (shortName) { From 639dce628285ecabe782d0a1f1169b3f92dbe4ef Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 11 Nov 2019 15:55:26 +0100 Subject: [PATCH 033/322] Support for new MySQL 8 error code 3572 Closes gh-23972 --- .../org/springframework/jdbc/support/sql-error-codes.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml index 1d0441c5a46..f0ed6570be3 100644 --- a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml @@ -205,7 +205,7 @@ 1 - 1205 + 1205,3572 1213 From 9c9ee22c2b946808c589a3d5cf529181d7c629ed Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Nov 2019 16:50:47 +0100 Subject: [PATCH 034/322] Note on injecting results from local @Bean methods (self references) Closes gh-23934 --- src/docs/asciidoc/core/core-beans.adoc | 28 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index f264ae2e874..cf56360a336 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -5092,6 +5092,7 @@ If there is no other resolution indicator (such as a qualifier or a primary mark for a non-unique dependency situation, Spring matches the injection point name (that is, the field name or parameter name) against the target bean names and choose the same-named candidate, if any. +==== That said, if you intend to express annotation-driven injection by name, do not primarily use `@Autowired`, even if it is capable of selecting by bean name among @@ -5115,17 +5116,28 @@ back to the bean that is currently injected). Note that self injection is a fall Regular dependencies on other components always have precedence. In that sense, self references do not participate in regular candidate selection and are therefore in particular never primary. On the contrary, they always end up as lowest precedence. -In practice, you should use self references as a last resort only (for example, for calling other methods -on the same instance through the bean's transactional proxy). Consider factoring out -the effected methods to a separate delegate bean in such a scenario. Alternatively, you -can use `@Resource`, which may obtain a proxy back to the current bean by its unique name. +In practice, you should use self references as a last resort only (for example, for +calling other methods on the same instance through the bean's transactional proxy). +Consider factoring out the effected methods to a separate delegate bean in such a scenario. +Alternatively, you can use `@Resource`, which may obtain a proxy back to the current bean +by its unique name. + +[NOTE] +==== +Trying to inject the results from `@Bean` methods on the same configuration class is +effectively a self-reference scenario as well. Either lazily resolve such references +in the method signature where it is actually needed (as opposed to an autowired field +in the configuration class) or declare the affected `@Bean` methods as `static`, +decoupling them from the containing configuration class instance and its lifecycle. +Otherwise, such beans are only considered in the fallback phase, with matching beans +on other configuration classes selected as primary candidates instead (if available). +==== `@Autowired` applies to fields, constructors, and multi-argument methods, allowing for -narrowing through qualifier annotations at the parameter level. By contrast, `@Resource` +narrowing through qualifier annotations at the parameter level. In contrast, `@Resource` is supported only for fields and bean property setter methods with a single argument. -As a consequence, you should stick with qualifiers if your injection target is a constructor or a -multi-argument method. -==== +As a consequence, you should stick with qualifiers if your injection target is a +constructor or a multi-argument method. You can create your own custom qualifier annotations. To do so, define an annotation and provide the `@Qualifier` annotation within your definition, as the following example shows: From b0d8a667a13fbed8b8295d1e3c85786c5cf736ca Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Nov 2019 16:51:47 +0100 Subject: [PATCH 035/322] Upgrade to Hibernate ORM 5.3.14 --- spring-orm/spring-orm.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-orm/spring-orm.gradle b/spring-orm/spring-orm.gradle index e173b8cbd6d..99de34aa6c7 100644 --- a/spring-orm/spring-orm.gradle +++ b/spring-orm/spring-orm.gradle @@ -9,7 +9,7 @@ dependencies { optional(project(":spring-context")) optional(project(":spring-web")) optional("org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.4") - optional("org.hibernate:hibernate-core:5.3.13.Final") + optional("org.hibernate:hibernate-core:5.3.14.Final") optional("javax.servlet:javax.servlet-api:3.1.0") testCompile("org.aspectj:aspectjweaver:${aspectjVersion}") testCompile("org.hsqldb:hsqldb:${hsqldbVersion}") From e4c57a9b613dddee69adce53ce672aa0e6783023 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Nov 2019 16:53:23 +0100 Subject: [PATCH 036/322] Minor internal refinements (backported from master) --- .../AbstractAutowireCapableBeanFactory.java | 19 +++++--------- ...ReloadableResourceBundleMessageSource.java | 26 ++++++++++++++----- .../AnnotationDrivenEventListenerTests.java | 9 ++++--- .../handler/EventSourceTransportHandler.java | 10 +++---- .../handler/HtmlFileTransportHandler.java | 4 +-- .../handler/WebSocketTransportHandler.java | 4 +-- .../handler/XhrPollingTransportHandler.java | 4 +-- .../handler/XhrStreamingTransportHandler.java | 9 +++---- 8 files changed, 46 insertions(+), 39 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index c11bce18655..20383c0e775 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -645,7 +645,6 @@ else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { @Nullable protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { Class targetType = determineTargetType(beanName, mbd, typesToMatch); - // Apply SmartInstantiationAwareBeanPostProcessors to predict the // eventual type after a before-instantiation shortcut. if (targetType != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { @@ -1364,34 +1363,28 @@ protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable B // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the // state of the bean before properties are set. This can be used, for example, // to support styles of field injection. - boolean continueWithPropertyPopulation = true; - if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { - continueWithPropertyPopulation = false; - break; + return; } } } } - if (!continueWithPropertyPopulation) { - return; - } - PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null); - if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) { + int resolvedAutowireMode = mbd.getResolvedAutowireMode(); + if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) { MutablePropertyValues newPvs = new MutablePropertyValues(pvs); // Add property values based on autowire by name if applicable. - if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME) { + if (resolvedAutowireMode == AUTOWIRE_BY_NAME) { autowireByName(beanName, mbd, bw, newPvs); } // Add property values based on autowire by type if applicable. - if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) { + if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) { autowireByType(beanName, mbd, bw, newPvs); } pvs = newPvs; @@ -1495,7 +1488,7 @@ protected void autowireByType( if (Object.class != pd.getPropertyType()) { MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); // Do not allow eager init for type matching in case of a prioritized post-processor. - boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance()); + boolean eager = !(bw.getWrappedInstance() instanceof PriorityOrdered); DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); if (autowiredArgument != null) { diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index d65869b14f6..5ef83f004bd 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.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. @@ -237,6 +237,7 @@ protected PropertiesHolder getMergedProperties(Locale locale) { if (mergedHolder != null) { return mergedHolder; } + Properties mergedProps = newProperties(); long latestTimestamp = -1; String[] basenames = StringUtils.toStringArray(getBasenameSet()); @@ -253,6 +254,7 @@ protected PropertiesHolder getMergedProperties(Locale locale) { } } } + mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp); PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder); if (existing != null) { @@ -279,18 +281,28 @@ protected List calculateAllFilenames(String basename, Locale locale) { return filenames; } } + + // Filenames for given Locale List filenames = new ArrayList<>(7); filenames.addAll(calculateFilenamesForLocale(basename, locale)); - if (isFallbackToSystemLocale() && !locale.equals(Locale.getDefault())) { - List fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault()); - for (String fallbackFilename : fallbackFilenames) { - if (!filenames.contains(fallbackFilename)) { - // Entry for fallback locale that isn't already in filenames list. - filenames.add(fallbackFilename); + + // Filenames for default Locale, if any + if (isFallbackToSystemLocale()) { + Locale defaultLocale = Locale.getDefault(); + if (!locale.equals(defaultLocale)) { + List fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale); + for (String fallbackFilename : fallbackFilenames) { + if (!filenames.contains(fallbackFilename)) { + // Entry for fallback locale that isn't already in filenames list. + filenames.add(fallbackFilename); + } } } } + + // Filename for default bundle file filenames.add(basename); + if (localeMap == null) { localeMap = new ConcurrentHashMap<>(); Map> existing = this.cachedFilenames.putIfAbsent(basename, localeMap); diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index edb82e00a33..631e135a478 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.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. @@ -794,6 +794,7 @@ public void handleIt(TestEvent event) { @EventListener @Async + @Override public void handleAsync(AnotherTestEvent event) { assertTrue(!Thread.currentThread().getName().equals(event.content)); this.eventCollector.addEvent(this, event); @@ -820,6 +821,7 @@ public void handleIt(TestEvent event) { @EventListener @Async + @Override public void handleAsync(AnotherTestEvent event) { assertTrue(!Thread.currentThread().getName().equals(event.content)); this.eventCollector.addEvent(this, event); @@ -902,7 +904,6 @@ public void handleString(GenericEventPojo value) { } - @EventListener @Retention(RetentionPolicy.RUNTIME) public @interface ConditionalEvent { @@ -934,18 +935,20 @@ public void handle(TestEvent event) { super.handle(event); } - @Override @EventListener(condition = "#payload.startsWith('OK')") + @Override public void handleString(String payload) { super.handleString(payload); } @ConditionalEvent("#root.event.timestamp > #p0") + @Override public void handleTimestamp(Long timestamp) { collectEvent(timestamp); } @ConditionalEvent("@conditionEvaluator.valid(#p0)") + @Override public void handleRatio(Double ratio) { collectEvent(ratio); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java index 4cc8da5d2e6..90b56c378c5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -30,7 +30,7 @@ import org.springframework.web.socket.sockjs.transport.session.StreamingSockJsSession; /** - * A TransportHandler for sending messages via Server-Sent events: + * A TransportHandler for sending messages via Server-Sent Events: * https://dev.w3.org/html5/eventsource/. * * @author Rossen Stoyanchev @@ -50,7 +50,7 @@ protected MediaType getContentType() { @Override public boolean checkSessionType(SockJsSession session) { - return session instanceof EventSourceStreamingSockJsSession; + return (session instanceof EventSourceStreamingSockJsSession); } @Override @@ -66,7 +66,7 @@ protected SockJsFrameFormat getFrameFormat(ServerHttpRequest request) { } - private class EventSourceStreamingSockJsSession extends StreamingSockJsSession { + private static class EventSourceStreamingSockJsSession extends StreamingSockJsSession { public EventSourceStreamingSockJsSession(String sessionId, SockJsServiceConfig config, WebSocketHandler wsHandler, Map attributes) { @@ -76,7 +76,7 @@ public EventSourceStreamingSockJsSession(String sessionId, SockJsServiceConfig c @Override protected byte[] getPrelude(ServerHttpRequest request) { - return new byte[] { '\r', '\n' }; + return new byte[] {'\r', '\n'}; } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java index 6473af39ca7..d39f51bbf9d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.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. @@ -93,7 +93,7 @@ protected MediaType getContentType() { @Override public boolean checkSessionType(SockJsSession session) { - return session instanceof HtmlFileStreamingSockJsSession; + return (session instanceof HtmlFileStreamingSockJsSession); } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java index e4d67b0217a..60423319dc6 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.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. @@ -107,7 +107,7 @@ public boolean isRunning() { @Override public boolean checkSessionType(SockJsSession session) { - return session instanceof WebSocketServerSockJsSession; + return (session instanceof WebSocketServerSockJsSession); } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrPollingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrPollingTransportHandler.java index 5e98adc3607..95a065801f6 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrPollingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrPollingTransportHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -54,7 +54,7 @@ protected SockJsFrameFormat getFrameFormat(ServerHttpRequest request) { @Override public boolean checkSessionType(SockJsSession session) { - return session instanceof PollingSockJsSession; + return (session instanceof PollingSockJsSession); } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java index 07f32132b91..f947643205c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -17,6 +17,7 @@ package org.springframework.web.socket.sockjs.transport.handler; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Map; import org.springframework.http.MediaType; @@ -41,9 +42,7 @@ public class XhrStreamingTransportHandler extends AbstractHttpSendingTransportHa private static final byte[] PRELUDE = new byte[2049]; static { - for (int i = 0; i < 2048; i++) { - PRELUDE[i] = 'h'; - } + Arrays.fill(PRELUDE, (byte) 'h'); PRELUDE[2048] = '\n'; } @@ -60,7 +59,7 @@ protected MediaType getContentType() { @Override public boolean checkSessionType(SockJsSession session) { - return session instanceof XhrStreamingSockJsSession; + return (session instanceof XhrStreamingSockJsSession); } @Override From 6ed6c08ace0ff3d4c60bf1c142e1a958f5430f9d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Nov 2019 23:49:33 +0100 Subject: [PATCH 037/322] Polishing --- .../springframework/context/support/StaticMessageSource.java | 3 ++- .../context/support/StaticMessageSourceTests.java | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java index 26df79d7b71..5f2596b4cf0 100644 --- a/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.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. @@ -43,6 +43,7 @@ public class StaticMessageSource extends AbstractMessageSource { @Override + @Nullable protected String resolveCodeWithoutArguments(String code, Locale locale) { return this.messages.get(code + '_' + locale.toString()); } diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java index b5bb82ebe7e..68d0dcf317c 100644 --- a/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -62,7 +62,6 @@ public class StaticMessageSourceTests extends AbstractApplicationContextTests { @Test @Override public void count() { - // These are only checked for current Ctx (not parent ctx) assertCount(15); } From c9a6f4282a677e1d5b018b72701899dabf07f75a Mon Sep 17 00:00:00 2001 From: monosoul Date: Wed, 30 Oct 2019 15:14:13 +0100 Subject: [PATCH 038/322] Inverse condition to fix ISO-formatted Instant parsing Prior to this commit, InstantFormatter was able to properly serialize an Instant that is far in the future (or in the past), but it could not properly deserialize it, because in such scenarios an ISO-formatted Instant starts with a +/- sign. This commit fixes this issue, while maintaining the previous contract, and also introduces tests for InstantFormatter. Closes gh-23895 --- .../datetime/standard/InstantFormatter.java | 10 +- .../standard/InstantFormatterTests.java | 119 ++++++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 985006e3ba3..010d004ae5d 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -40,14 +40,14 @@ public class InstantFormatter implements Formatter { @Override public Instant parse(String text, Locale locale) throws ParseException { - if (text.length() > 0 && Character.isDigit(text.charAt(0))) { - // assuming UTC instant a la "2007-12-03T10:15:30.00Z" - return Instant.parse(text); - } - else { + if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); } + else { + // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + return Instant.parse(text); + } } @Override diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java new file mode 100644 index 00000000000..5e9b0814fcb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -0,0 +1,119 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.format.datetime.standard; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Random; +import java.util.stream.Stream; + +import static java.time.Instant.MAX; +import static java.time.Instant.MIN; +import static java.time.ZoneId.systemDefault; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andrei Nevedomskii + */ +@SuppressWarnings("ConstantConditions") +class InstantFormatterTests { + + private final InstantFormatter instantFormatter = new InstantFormatter(); + + @ParameterizedTest + @ArgumentsSource(ISOSerializedInstantProvider.class) + void should_parse_an_ISO_formatted_string_representation_of_an_instant(String input) throws ParseException { + Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); + + Instant actual = instantFormatter.parse(input, null); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ArgumentsSource(RFC1123SerializedInstantProvider.class) + void should_parse_an_RFC1123_formatted_string_representation_of_an_instant(String input) throws ParseException { + Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); + + Instant actual = instantFormatter.parse(input, null); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ArgumentsSource(RandomInstantProvider.class) + void should_serialize_an_instant_using_ISO_format_and_ignoring_locale(Instant input) { + String expected = DateTimeFormatter.ISO_INSTANT.format(input); + + String actual = instantFormatter.print(input, null); + + assertThat(actual).isEqualTo(expected); + } + + private static class ISOSerializedInstantProvider extends RandomInstantProvider { + + @Override + Stream provideArguments() { + return randomInstantStream(MIN, MAX).map(DateTimeFormatter.ISO_INSTANT::format); + } + } + + private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { + + // RFC-1123 supports only 4-digit years + private final Instant min = Instant.parse("0000-01-01T00:00:00.00Z"); + + private final Instant max = Instant.parse("9999-12-31T23:59:59.99Z"); + + @Override + Stream provideArguments() { + return randomInstantStream(min, max) + .map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format); + } + } + + private static class RandomInstantProvider implements ArgumentsProvider { + + private static final long DATA_SET_SIZE = 10; + + static final Random RANDOM = new Random(); + + Stream provideArguments() { + return randomInstantStream(MIN, MAX); + } + + @Override + public final Stream provideArguments(ExtensionContext context) { + return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE); + } + + Stream randomInstantStream(Instant min, Instant max) { + return Stream.concat( + Stream.of(Instant.now()), // make sure that the data set includes current instant + RANDOM.longs(min.getEpochSecond(), max.getEpochSecond()) + .mapToObj(Instant::ofEpochSecond) + ); + } + } +} \ No newline at end of file From 40ac055d115e68ed5ce7c0c0ff2de5ca3977bb02 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 14 Nov 2019 09:54:50 +0100 Subject: [PATCH 039/322] Polish contribution See gh-23895 --- .../datetime/standard/InstantFormatter.java | 3 +- .../standard/InstantFormatterTests.java | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 010d004ae5d..456c0ad0909 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -31,6 +31,7 @@ * (which is commonly used for HTTP date header values), as of Spring 4.3. * * @author Juergen Hoeller + * @author Andrei Nevedomskii * @since 4.0 * @see java.time.Instant#parse * @see java.time.format.DateTimeFormatter#ISO_INSTANT diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java index 5e9b0814fcb..3f8dd9c6ad4 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -16,34 +16,42 @@ package org.springframework.format.datetime.standard; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - import java.text.ParseException; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.Random; import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + import static java.time.Instant.MAX; import static java.time.Instant.MIN; import static java.time.ZoneId.systemDefault; import static org.assertj.core.api.Assertions.assertThat; /** + * Unit tests for {@link InstantFormatter}. + * * @author Andrei Nevedomskii + * @author Sam Brannen + * @since 5.1.12 */ -@SuppressWarnings("ConstantConditions") +@DisplayName("InstantFormatter unit tests") +@DisplayNameGeneration(ReplaceUnderscores.class) class InstantFormatterTests { private final InstantFormatter instantFormatter = new InstantFormatter(); @ParameterizedTest @ArgumentsSource(ISOSerializedInstantProvider.class) - void should_parse_an_ISO_formatted_string_representation_of_an_instant(String input) throws ParseException { + void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); Instant actual = instantFormatter.parse(input, null); @@ -53,7 +61,7 @@ void should_parse_an_ISO_formatted_string_representation_of_an_instant(String in @ParameterizedTest @ArgumentsSource(RFC1123SerializedInstantProvider.class) - void should_parse_an_RFC1123_formatted_string_representation_of_an_instant(String input) throws ParseException { + void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); Instant actual = instantFormatter.parse(input, null); @@ -63,7 +71,7 @@ void should_parse_an_RFC1123_formatted_string_representation_of_an_instant(Strin @ParameterizedTest @ArgumentsSource(RandomInstantProvider.class) - void should_serialize_an_instant_using_ISO_format_and_ignoring_locale(Instant input) { + void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) { String expected = DateTimeFormatter.ISO_INSTANT.format(input); String actual = instantFormatter.print(input, null); @@ -97,7 +105,7 @@ private static class RandomInstantProvider implements ArgumentsProvider { private static final long DATA_SET_SIZE = 10; - static final Random RANDOM = new Random(); + private static final Random random = new Random(); Stream provideArguments() { return randomInstantStream(MIN, MAX); @@ -109,11 +117,9 @@ public final Stream provideArguments(ExtensionContext conte } Stream randomInstantStream(Instant min, Instant max) { - return Stream.concat( - Stream.of(Instant.now()), // make sure that the data set includes current instant - RANDOM.longs(min.getEpochSecond(), max.getEpochSecond()) - .mapToObj(Instant::ofEpochSecond) - ); + return Stream.concat(Stream.of(Instant.now()), // make sure that the data set includes current instant + random.longs(min.getEpochSecond(), max.getEpochSecond()).mapToObj(Instant::ofEpochSecond)); } } -} \ No newline at end of file + +} From 96a1a0dec533231f52213bc3f20314c365eee4c0 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 14 Nov 2019 12:01:03 +0100 Subject: [PATCH 040/322] Convert InstantFormatterTests to JUnit 4 See gh-23895 --- .../standard/InstantFormatterTests.java | 110 +++++++++--------- 1 file changed, 53 insertions(+), 57 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java index 3f8dd9c6ad4..b9c32ed8ecd 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -22,19 +22,12 @@ import java.util.Random; import java.util.stream.Stream; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.Test; import static java.time.Instant.MAX; import static java.time.Instant.MIN; import static java.time.ZoneId.systemDefault; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; /** * Unit tests for {@link InstantFormatter}. @@ -43,46 +36,70 @@ * @author Sam Brannen * @since 5.1.12 */ -@DisplayName("InstantFormatter unit tests") -@DisplayNameGeneration(ReplaceUnderscores.class) -class InstantFormatterTests { +public class InstantFormatterTests { private final InstantFormatter instantFormatter = new InstantFormatter(); - @ParameterizedTest - @ArgumentsSource(ISOSerializedInstantProvider.class) - void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException { - Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); - Instant actual = instantFormatter.parse(input, null); + @Test + public void should_parse_an_ISO_formatted_string_representation_of_an_Instant() { + new ISOSerializedInstantProvider().provideArguments().forEach(input -> { + try { + Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); - assertThat(actual).isEqualTo(expected); + Instant actual = instantFormatter.parse(input, null); + + assertEquals(expected, actual); + } + catch (ParseException ex) { + throw new RuntimeException(ex); + } + }); } - @ParameterizedTest - @ArgumentsSource(RFC1123SerializedInstantProvider.class) - void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException { - Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); + @Test + public void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant() { + new RFC1123SerializedInstantProvider().provideArguments().forEach(input -> { + try { + Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); - Instant actual = instantFormatter.parse(input, null); + Instant actual = instantFormatter.parse(input, null); - assertThat(actual).isEqualTo(expected); + assertEquals(expected, actual); + } + catch (ParseException ex) { + throw new RuntimeException(ex); + } + }); } - @ParameterizedTest - @ArgumentsSource(RandomInstantProvider.class) - void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) { - String expected = DateTimeFormatter.ISO_INSTANT.format(input); + @Test + public void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale() { + new RandomInstantProvider().randomInstantStream(MIN, MAX).forEach(instant -> { + String expected = DateTimeFormatter.ISO_INSTANT.format(instant); + + String actual = instantFormatter.print(instant, null); + + assertEquals(expected, actual); + }); + } + + + private static class RandomInstantProvider { + + private static final long DATA_SET_SIZE = 10; - String actual = instantFormatter.print(input, null); + private static final Random random = new Random(); - assertThat(actual).isEqualTo(expected); + Stream randomInstantStream(Instant min, Instant max) { + return Stream.concat(Stream.of(Instant.now()), // make sure that the data set includes current instant + random.longs(min.getEpochSecond(), max.getEpochSecond()).limit(DATA_SET_SIZE).mapToObj(Instant::ofEpochSecond)); + } } private static class ISOSerializedInstantProvider extends RandomInstantProvider { - @Override - Stream provideArguments() { + Stream provideArguments() { return randomInstantStream(MIN, MAX).map(DateTimeFormatter.ISO_INSTANT::format); } } @@ -90,36 +107,15 @@ Stream provideArguments() { private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { // RFC-1123 supports only 4-digit years - private final Instant min = Instant.parse("0000-01-01T00:00:00.00Z"); + private static final Instant min = Instant.parse("0000-01-01T00:00:00.00Z"); + + private static final Instant max = Instant.parse("9999-12-31T23:59:59.99Z"); - private final Instant max = Instant.parse("9999-12-31T23:59:59.99Z"); - @Override - Stream provideArguments() { + Stream provideArguments() { return randomInstantStream(min, max) .map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format); } } - private static class RandomInstantProvider implements ArgumentsProvider { - - private static final long DATA_SET_SIZE = 10; - - private static final Random random = new Random(); - - Stream provideArguments() { - return randomInstantStream(MIN, MAX); - } - - @Override - public final Stream provideArguments(ExtensionContext context) { - return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE); - } - - Stream randomInstantStream(Instant min, Instant max) { - return Stream.concat(Stream.of(Instant.now()), // make sure that the data set includes current instant - random.longs(min.getEpochSecond(), max.getEpochSecond()).mapToObj(Instant::ofEpochSecond)); - } - } - } From 85471d05871ee8461b5f196ed26ffeb866d76cb2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 14 Nov 2019 18:31:32 +0100 Subject: [PATCH 041/322] Revise getElementTypeDescriptor javadoc (no IllegalStateException) Closes gh-23996 --- .../core/convert/TypeDescriptor.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index a5d15557308..07d48d73016 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -36,7 +36,8 @@ import org.springframework.util.ObjectUtils; /** - * Context about a type to convert from or to. + * Contextual descriptor about a type to convert from or to. + * Capable of representing arrays and generic collection types. * * @author Keith Donald * @author Andy Clement @@ -45,6 +46,8 @@ * @author Sam Brannen * @author Stephane Nicoll * @since 3.0 + * @see ConversionService#canConvert(TypeDescriptor, TypeDescriptor) + * @see ConversionService#convert(Object, TypeDescriptor, TypeDescriptor) */ @SuppressWarnings("serial") public class TypeDescriptor implements Serializable { @@ -322,9 +325,9 @@ public boolean isArray() { * If this type is a {@code Stream}, returns the stream's component type. * If this type is a {@link Collection} and it is parameterized, returns the Collection's element type. * If the Collection is not parameterized, returns {@code null} indicating the element type is not declared. - * @return the array component type or Collection element type, or {@code null} if this type is a - * Collection but its element type is not parameterized - * @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type + * @return the array component type or Collection element type, or {@code null} if this type is not + * an array type or a {@code java.util.Collection} or if its element type is not parameterized + * @see #elementTypeDescriptor(Object) */ @Nullable public TypeDescriptor getElementTypeDescriptor() { @@ -351,8 +354,7 @@ public TypeDescriptor getElementTypeDescriptor() { * TypeDescriptor that is returned. * @param element the collection or array element * @return a element type descriptor, narrowed to the type of the provided element - * @throws IllegalStateException if this type is not a {@code java.util.Collection} - * or array type + * @see #getElementTypeDescriptor() * @see #narrow(Object) */ @Nullable From 268d02958415c61d83d01e96fddc8ffaf0d23375 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 19 Nov 2019 14:53:30 +0100 Subject: [PATCH 042/322] Avoid substring allocation in StringUtils.replace Closes gh-24023 --- .../src/main/java/org/springframework/util/StringUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index c1a21478faf..7f1055d1a28 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -417,14 +417,14 @@ public static String replace(String inString, String oldPattern, @Nullable Strin int pos = 0; // our position in the old string int patLen = oldPattern.length(); while (index >= 0) { - sb.append(inString.substring(pos, index)); + sb.append(inString, pos, index); sb.append(newPattern); pos = index + patLen; index = inString.indexOf(oldPattern, pos); } // append any characters to the right of a match - sb.append(inString.substring(pos)); + sb.append(inString, pos, inString.length()); return sb.toString(); } From 9a522946a538871ae10912ebc6c0687bd09bacb3 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 19 Nov 2019 14:37:49 +0100 Subject: [PATCH 043/322] Backport tests for gh-23985 --- ...copedControllerAdviceIntegrationTests.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java new file mode 100644 index 00000000000..e75a84ec3a5 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.annotation; + +import java.util.List; + +import org.junit.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.method.ControllerAdviceBean; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.junit.Assert.assertEquals; + +/** + * Integration tests for request-scoped {@link ControllerAdvice @ControllerAdvice} beans. + * + * @author Sam Brannen + * @since 5.1.12 + */ +public class RequestScopedControllerAdviceIntegrationTests { + + @Test // gh-23985 + public void loadContextWithRequestScopedControllerAdvice() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(Config.class); + context.refresh(); + + // Until gh-24017 is fixed, we expect the RequestScopedControllerAdvice to show up twice. + List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(context); + assertEquals(2, adviceBeans.size()); + + ControllerAdviceBean adviceBean1 = adviceBeans.get(0); + assertEquals(RequestScopedControllerAdvice.class, adviceBean1.getBeanType()); + assertEquals(42, adviceBean1.getOrder()); + + ControllerAdviceBean adviceBean2 = adviceBeans.get(1); + assertEquals(RequestScopedControllerAdvice.class, adviceBean2.getBeanType()); + assertEquals(42, adviceBean2.getOrder()); + + context.close(); + } + + + @Configuration + @EnableWebMvc + static class Config { + + @Bean + @RequestScope + RequestScopedControllerAdvice requestScopedControllerAdvice() { + return new RequestScopedControllerAdvice(); + } + } + + @ControllerAdvice + @Order(42) + static class RequestScopedControllerAdvice implements Ordered { + + @Override + public int getOrder() { + return 99; + } + } + +} From ffcd83e3a86a8c462d383bde4290fc28100c29f6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 19 Nov 2019 14:20:19 +0100 Subject: [PATCH 044/322] Ignore scoped proxy targets for @ControllerAdvice beans Prior to this commit, methods in a @ControllerAdvice bean were registered and invoked twice if the advice was a scoped bean (e.g., request or session scoped). In other words, both the proxy bean and the target bean were wrapped in ControllerAdviceBean instances. This commit fixes this bug by modifying the findAnnotatedBeans() method in ControllerAdviceBean so that it filters out targets of scoped proxies. Closes gh-24017 --- .../spr/ControllerAdviceIntegrationTests.java | 135 ++++++++++++++++++ .../web/method/ControllerAdviceBean.java | 5 +- ...copedControllerAdviceIntegrationTests.java | 13 +- 3 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java new file mode 100644 index 00000000000..982e5e13642 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java @@ -0,0 +1,135 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.web.servlet.samples.spr; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +/** + * Integration tests for {@link ControllerAdvice @ControllerAdvice}. + * + *

Introduced in conjunction with + * gh-24017. + * + * @author Sam Brannen + * @since 5.1.12 + */ +@RunWith(SpringRunner.class) +@WebAppConfiguration +public class ControllerAdviceIntegrationTests { + + @Autowired + WebApplicationContext wac; + + MockMvc mockMvc; + + @Before + public void setUpMockMvc() { + this.mockMvc = webAppContextSetup(wac).build(); + } + + @Test + public void controllerAdviceIsAppliedOnlyOnce() throws Exception { + assertEquals(0, SingletonControllerAdvice.counter.get()); + assertEquals(0, RequestScopedControllerAdvice.counter.get()); + + this.mockMvc.perform(get("/test"))// + .andExpect(status().isOk())// + .andExpect(forwardedUrl("singleton:1;request-scoped:1")); + + assertEquals(1, SingletonControllerAdvice.counter.get()); + assertEquals(1, RequestScopedControllerAdvice.counter.get()); + } + + @Configuration + @EnableWebMvc + static class Config { + + @Bean + TestController testController() { + return new TestController(); + } + + @Bean + SingletonControllerAdvice singletonControllerAdvice() { + return new SingletonControllerAdvice(); + } + + @Bean + @RequestScope + RequestScopedControllerAdvice requestScopedControllerAdvice() { + return new RequestScopedControllerAdvice(); + } + } + + @ControllerAdvice + static class SingletonControllerAdvice { + + static final AtomicInteger counter = new AtomicInteger(); + + @ModelAttribute + void initModel(Model model) { + model.addAttribute("singleton", counter.incrementAndGet()); + } + } + + @ControllerAdvice + static class RequestScopedControllerAdvice { + + static final AtomicInteger counter = new AtomicInteger(); + + @ModelAttribute + void initModel(Model model) { + model.addAttribute("request-scoped", counter.incrementAndGet()); + } + } + + @Controller + static class TestController { + + @GetMapping("/test") + String get(Model model) { + return "singleton:" + model.asMap().get("singleton") + ";request-scoped:" + + model.asMap().get("request-scoped"); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java index af12b15d5be..ddf67af093b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java +++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.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. @@ -20,6 +20,7 @@ import java.util.List; import java.util.stream.Collectors; +import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; @@ -42,6 +43,7 @@ * @author Rossen Stoyanchev * @author Brian Clozel * @author Juergen Hoeller + * @author Sam Brannen * @since 3.2 */ public class ControllerAdviceBean implements Ordered { @@ -187,6 +189,7 @@ public String toString() { */ public static List findAnnotatedBeans(ApplicationContext context) { return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) + .filter(name -> !ScopedProxyUtils.isScopedTarget(name)) .filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null) .map(name -> new ControllerAdviceBean(name, context)) .collect(Collectors.toList()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java index e75a84ec3a5..062ed71dde4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java @@ -48,17 +48,12 @@ public void loadContextWithRequestScopedControllerAdvice() { context.register(Config.class); context.refresh(); - // Until gh-24017 is fixed, we expect the RequestScopedControllerAdvice to show up twice. List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(context); - assertEquals(2, adviceBeans.size()); + assertEquals(1, adviceBeans.size()); - ControllerAdviceBean adviceBean1 = adviceBeans.get(0); - assertEquals(RequestScopedControllerAdvice.class, adviceBean1.getBeanType()); - assertEquals(42, adviceBean1.getOrder()); - - ControllerAdviceBean adviceBean2 = adviceBeans.get(1); - assertEquals(RequestScopedControllerAdvice.class, adviceBean2.getBeanType()); - assertEquals(42, adviceBean2.getOrder()); + ControllerAdviceBean adviceBean = adviceBeans.get(0); + assertEquals(RequestScopedControllerAdvice.class, adviceBean.getBeanType()); + assertEquals(42, adviceBean.getOrder()); context.close(); } From 83f03976ea5ef7c1d4f90a181bc3c78f6ec93e1f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 20 Nov 2019 11:02:08 +0100 Subject: [PATCH 045/322] Start building against Reactor Californium-SR14 snapshots See gh-24038 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7b125b06deb..aef013d6125 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ ext { kotlinVersion = "1.2.71" log4jVersion = "2.11.2" nettyVersion = "4.1.43.Final" - reactorVersion = "Californium-SR13" + reactorVersion = "Californium-BUILD-SNAPSHOT" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" rxjava2Version = "2.2.14" @@ -148,6 +148,7 @@ configure(allprojects) { project -> repositories { maven { url "https://repo.spring.io/libs-release" } + maven { url "https://repo.spring.io/snapshot" } mavenLocal() } From aee33d8b4be34e5db6dace0433899525105b5c26 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 20 Nov 2019 17:37:40 +0100 Subject: [PATCH 046/322] Mark SqlRowSet accessor methods as nullable (for alignment with JDBC) Closes gh-24042 --- .../rowset/ResultSetWrappingSqlRowSet.java | 27 +++++++++++++++- .../jdbc/support/rowset/SqlRowSet.java | 31 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java index 8062eb0eaf8..220c8d17463 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.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. @@ -29,6 +29,7 @@ import java.util.Map; import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; /** * The default implementation of Spring's {@link SqlRowSet} interface, wrapping a @@ -160,6 +161,7 @@ public int findColumn(String columnLabel) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getBigDecimal(int) */ @Override + @Nullable public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getBigDecimal(columnIndex); @@ -173,6 +175,7 @@ public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessEx * @see java.sql.ResultSet#getBigDecimal(String) */ @Override + @Nullable public BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException { return getBigDecimal(findColumn(columnLabel)); } @@ -223,6 +226,7 @@ public byte getByte(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getDate(int) */ @Override + @Nullable public Date getDate(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getDate(columnIndex); @@ -236,6 +240,7 @@ public Date getDate(int columnIndex) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getDate(String) */ @Override + @Nullable public Date getDate(String columnLabel) throws InvalidResultSetAccessException { return getDate(findColumn(columnLabel)); } @@ -244,6 +249,7 @@ public Date getDate(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getDate(int, Calendar) */ @Override + @Nullable public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { try { return this.resultSet.getDate(columnIndex, cal); @@ -257,6 +263,7 @@ public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccess * @see java.sql.ResultSet#getDate(String, Calendar) */ @Override + @Nullable public Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { return getDate(findColumn(columnLabel), cal); } @@ -349,6 +356,7 @@ public long getLong(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getNString(int) */ @Override + @Nullable public String getNString(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getNString(columnIndex); @@ -362,6 +370,7 @@ public String getNString(int columnIndex) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getNString(String) */ @Override + @Nullable public String getNString(String columnLabel) throws InvalidResultSetAccessException { return getNString(findColumn(columnLabel)); } @@ -370,6 +379,7 @@ public String getNString(String columnLabel) throws InvalidResultSetAccessExcept * @see java.sql.ResultSet#getObject(int) */ @Override + @Nullable public Object getObject(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getObject(columnIndex); @@ -383,6 +393,7 @@ public Object getObject(int columnIndex) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getObject(String) */ @Override + @Nullable public Object getObject(String columnLabel) throws InvalidResultSetAccessException { return getObject(findColumn(columnLabel)); } @@ -391,6 +402,7 @@ public Object getObject(String columnLabel) throws InvalidResultSetAccessExcepti * @see java.sql.ResultSet#getObject(int, Map) */ @Override + @Nullable public Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException { try { return this.resultSet.getObject(columnIndex, map); @@ -404,6 +416,7 @@ public Object getObject(int columnIndex, Map> map) throws Inval * @see java.sql.ResultSet#getObject(String, Map) */ @Override + @Nullable public Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException { return getObject(findColumn(columnLabel), map); } @@ -412,6 +425,7 @@ public Object getObject(String columnLabel, Map> map) throws In * @see java.sql.ResultSet#getObject(int, Class) */ @Override + @Nullable public T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException { try { return this.resultSet.getObject(columnIndex, type); @@ -425,6 +439,7 @@ public T getObject(int columnIndex, Class type) throws InvalidResultSetAc * @see java.sql.ResultSet#getObject(String, Class) */ @Override + @Nullable public T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException { return getObject(findColumn(columnLabel), type); } @@ -454,6 +469,7 @@ public short getShort(String columnLabel) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getString(int) */ @Override + @Nullable public String getString(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getString(columnIndex); @@ -467,6 +483,7 @@ public String getString(int columnIndex) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getString(String) */ @Override + @Nullable public String getString(String columnLabel) throws InvalidResultSetAccessException { return getString(findColumn(columnLabel)); } @@ -475,6 +492,7 @@ public String getString(String columnLabel) throws InvalidResultSetAccessExcepti * @see java.sql.ResultSet#getTime(int) */ @Override + @Nullable public Time getTime(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getTime(columnIndex); @@ -488,6 +506,7 @@ public Time getTime(int columnIndex) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getTime(String) */ @Override + @Nullable public Time getTime(String columnLabel) throws InvalidResultSetAccessException { return getTime(findColumn(columnLabel)); } @@ -496,6 +515,7 @@ public Time getTime(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getTime(int, Calendar) */ @Override + @Nullable public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { try { return this.resultSet.getTime(columnIndex, cal); @@ -509,6 +529,7 @@ public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccess * @see java.sql.ResultSet#getTime(String, Calendar) */ @Override + @Nullable public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { return getTime(findColumn(columnLabel), cal); } @@ -517,6 +538,7 @@ public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAcc * @see java.sql.ResultSet#getTimestamp(int) */ @Override + @Nullable public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getTimestamp(columnIndex); @@ -530,6 +552,7 @@ public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessExce * @see java.sql.ResultSet#getTimestamp(String) */ @Override + @Nullable public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException { return getTimestamp(findColumn(columnLabel)); } @@ -538,6 +561,7 @@ public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessE * @see java.sql.ResultSet#getTimestamp(int, Calendar) */ @Override + @Nullable public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { try { return this.resultSet.getTimestamp(columnIndex, cal); @@ -551,6 +575,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResul * @see java.sql.ResultSet#getTimestamp(String, Calendar) */ @Override + @Nullable public Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { return getTimestamp(findColumn(columnLabel), cal); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java index c7c3bef1f49..f0257f7b0e7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.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. @@ -25,6 +25,7 @@ import java.util.Map; import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; /** * Mirror interface for {@link javax.sql.RowSet}, representing a disconnected variant of @@ -74,6 +75,7 @@ public interface SqlRowSet extends Serializable { * @return an BigDecimal object representing the column value * @see java.sql.ResultSet#getBigDecimal(int) */ + @Nullable BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException; /** @@ -82,6 +84,7 @@ public interface SqlRowSet extends Serializable { * @return an BigDecimal object representing the column value * @see java.sql.ResultSet#getBigDecimal(String) */ + @Nullable BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException; /** @@ -122,6 +125,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(int) */ + @Nullable Date getDate(int columnIndex) throws InvalidResultSetAccessException; /** @@ -130,6 +134,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(String) */ + @Nullable Date getDate(String columnLabel) throws InvalidResultSetAccessException; /** @@ -139,6 +144,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(int, Calendar) */ + @Nullable Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; /** @@ -148,6 +154,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(String, Calendar) */ + @Nullable Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; /** @@ -222,6 +229,7 @@ public interface SqlRowSet extends Serializable { * @since 4.1.3 * @see java.sql.ResultSet#getNString(int) */ + @Nullable String getNString(int columnIndex) throws InvalidResultSetAccessException; /** @@ -232,6 +240,7 @@ public interface SqlRowSet extends Serializable { * @since 4.1.3 * @see java.sql.ResultSet#getNString(String) */ + @Nullable String getNString(String columnLabel) throws InvalidResultSetAccessException; /** @@ -240,6 +249,7 @@ public interface SqlRowSet extends Serializable { * @return a Object representing the column value * @see java.sql.ResultSet#getObject(int) */ + @Nullable Object getObject(int columnIndex) throws InvalidResultSetAccessException; /** @@ -248,6 +258,7 @@ public interface SqlRowSet extends Serializable { * @return a Object representing the column value * @see java.sql.ResultSet#getObject(String) */ + @Nullable Object getObject(String columnLabel) throws InvalidResultSetAccessException; /** @@ -257,6 +268,7 @@ public interface SqlRowSet extends Serializable { * @return a Object representing the column value * @see java.sql.ResultSet#getObject(int, Map) */ + @Nullable Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException; /** @@ -266,6 +278,7 @@ public interface SqlRowSet extends Serializable { * @return a Object representing the column value * @see java.sql.ResultSet#getObject(String, Map) */ + @Nullable Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException; /** @@ -273,9 +286,10 @@ public interface SqlRowSet extends Serializable { * @param columnIndex the column index * @param type the Java type to convert the designated column to * @return a Object representing the column value - * @see java.sql.ResultSet#getObject(int) * @since 4.1.3 + * @see java.sql.ResultSet#getObject(int, Class) */ + @Nullable T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException; /** @@ -283,9 +297,10 @@ public interface SqlRowSet extends Serializable { * @param columnLabel the column label * @param type the Java type to convert the designated column to * @return a Object representing the column value - * @see java.sql.ResultSet#getObject(int) * @since 4.1.3 + * @see java.sql.ResultSet#getObject(String, Class) */ + @Nullable T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException; /** @@ -310,6 +325,7 @@ public interface SqlRowSet extends Serializable { * @return a String representing the column value * @see java.sql.ResultSet#getString(int) */ + @Nullable String getString(int columnIndex) throws InvalidResultSetAccessException; /** @@ -318,6 +334,7 @@ public interface SqlRowSet extends Serializable { * @return a String representing the column value * @see java.sql.ResultSet#getString(String) */ + @Nullable String getString(String columnLabel) throws InvalidResultSetAccessException; /** @@ -326,6 +343,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(int) */ + @Nullable Time getTime(int columnIndex) throws InvalidResultSetAccessException; /** @@ -334,6 +352,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(String) */ + @Nullable Time getTime(String columnLabel) throws InvalidResultSetAccessException; /** @@ -343,6 +362,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(int, Calendar) */ + @Nullable Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; /** @@ -352,6 +372,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(String, Calendar) */ + @Nullable Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; /** @@ -360,6 +381,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(int) */ + @Nullable Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException; /** @@ -368,6 +390,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(String) */ + @Nullable Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException; /** @@ -377,6 +400,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(int, Calendar) */ + @Nullable Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; /** @@ -386,6 +410,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(String, Calendar) */ + @Nullable Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; From 8b1709b8f365f7ee276a0efb6c639f9588a1d575 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 20 Nov 2019 17:45:18 +0100 Subject: [PATCH 047/322] Upgrade to Hibernate Validator 6.0.18 and Undertow 2.0.28 --- build.gradle | 2 +- spring-context-support/spring-context-support.gradle | 2 +- spring-test/spring-test.gradle | 2 +- spring-webflux/spring-webflux.gradle | 2 +- spring-webmvc/spring-webmvc.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index aef013d6125..8b43df8aa67 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ ext { slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" tomcatVersion = "9.0.27" - undertowVersion = "2.0.27.Final" + undertowVersion = "2.0.28.Final" gradleScriptDir = "${rootProject.projectDir}/gradle" withoutJclOverSlf4j = { diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle index f4f961a6686..b1d8559cdcf 100644 --- a/spring-context-support/spring-context-support.gradle +++ b/spring-context-support/spring-context-support.gradle @@ -16,7 +16,7 @@ dependencies { optional("org.freemarker:freemarker:${freemarkerVersion}") testCompile(project(":spring-context")) testCompile("org.hsqldb:hsqldb:${hsqldbVersion}") - testCompile("org.hibernate:hibernate-validator:6.0.17.Final") + testCompile("org.hibernate:hibernate-validator:6.0.18.Final") testCompile("javax.annotation:javax.annotation-api:1.3.2") testRuntime("org.ehcache:jcache:1.0.1") testRuntime("org.ehcache:ehcache:3.4.0") diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index c5345278c39..621ab4dec42 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -61,7 +61,7 @@ dependencies { testCompile("javax.interceptor:javax.interceptor-api:1.2.2") testCompile("javax.mail:javax.mail-api:1.6.2") testCompile("org.hibernate:hibernate-core:5.3.12.Final") - testCompile("org.hibernate:hibernate-validator:6.0.17.Final") + testCompile("org.hibernate:hibernate-validator:6.0.18.Final") // Enable use of the JUnit Platform Runner testCompile("org.junit.platform:junit-platform-runner") testCompile("org.junit.jupiter:junit-jupiter-params") diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 88483ef3517..46575ca753f 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -42,7 +42,7 @@ dependencies { optional("com.google.protobuf:protobuf-java-util:3.6.1") testCompile("javax.xml.bind:jaxb-api:2.3.1") testCompile("com.fasterxml:aalto-xml:1.1.1") - testCompile("org.hibernate:hibernate-validator:6.0.17.Final") + testCompile("org.hibernate:hibernate-validator:6.0.18.Final") testCompile "io.reactivex.rxjava2:rxjava:${rxjava2Version}" testCompile("io.projectreactor:reactor-test") testCompile("io.undertow:undertow-core:${undertowVersion}") diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 1c24771a654..6da9b01d6e1 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -66,7 +66,7 @@ dependencies { exclude group: "xerces", module: "xercesImpl" } testCompile("org.xmlunit:xmlunit-matchers:2.6.2") - testCompile("org.hibernate:hibernate-validator:6.0.17.Final") + testCompile("org.hibernate:hibernate-validator:6.0.18.Final") testCompile("io.projectreactor:reactor-core") testCompile("io.reactivex:rxjava:${rxjavaVersion}") testCompile("io.reactivex:rxjava-reactive-streams:${rxjavaAdapterVersion}") From 262332a7c90819d3450bd477bca40306afc5bea5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 21 Nov 2019 13:48:55 +0100 Subject: [PATCH 048/322] Upgrade to Jetty 9.4.23 and Woodstox 5.3 --- build.gradle | 2 +- spring-core/spring-core.gradle | 2 +- spring-web/spring-web.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 8b43df8aa67..b651148ade8 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ ext { groovyVersion = "2.5.8" hsqldbVersion = "2.4.1" jackson2Version = "2.9.9" - jettyVersion = "9.4.21.v20190926" + jettyVersion = "9.4.23.v20191118" junit5Version = "5.3.2" kotlinVersion = "1.2.71" log4jVersion = "2.11.2" diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index bda90aad827..aaf884b1bf2 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -83,7 +83,7 @@ dependencies { testCompile("com.google.code.findbugs:jsr305:3.0.2") testCompile("org.xmlunit:xmlunit-matchers:2.6.2") testCompile("javax.xml.bind:jaxb-api:2.3.1") - testCompile("com.fasterxml.woodstox:woodstox-core:5.2.0") { + testCompile("com.fasterxml.woodstox:woodstox-core:5.3.0") { exclude group: "stax", module: "stax-api" } } diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 390f697127d..d735f4a333e 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -47,7 +47,7 @@ dependencies { } optional("commons-fileupload:commons-fileupload:1.4") optional("org.synchronoss.cloud:nio-multipart-parser:1.1.0") - optional("com.fasterxml.woodstox:woodstox-core:5.2.0") { // woodstox before aalto + optional("com.fasterxml.woodstox:woodstox-core:5.3.0") { // woodstox before aalto exclude group: "stax", module: "stax-api" } optional("com.fasterxml:aalto-xml:1.1.1") From f4676bb41c396126c1ca077a6ef06bce017f0799 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 21 Nov 2019 18:25:37 +0100 Subject: [PATCH 049/322] Restore short-circuiting in equals implementation Closes gh-24048 --- .../support/AbstractBeanDefinition.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 4fa941961a5..4143b40dd18 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -1128,28 +1128,28 @@ public boolean equals(Object other) { } AbstractBeanDefinition that = (AbstractBeanDefinition) other; boolean rtn = ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()); - rtn = rtn &= ObjectUtils.nullSafeEquals(this.scope, that.scope); - rtn = rtn &= this.abstractFlag == that.abstractFlag; - rtn = rtn &= this.lazyInit == that.lazyInit; - rtn = rtn &= this.autowireMode == that.autowireMode; - rtn = rtn &= this.dependencyCheck == that.dependencyCheck; - rtn = rtn &= Arrays.equals(this.dependsOn, that.dependsOn); - rtn = rtn &= this.autowireCandidate == that.autowireCandidate; - rtn = rtn &= ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers); - rtn = rtn &= this.primary == that.primary; - rtn = rtn &= this.nonPublicAccessAllowed == that.nonPublicAccessAllowed; - rtn = rtn &= this.lenientConstructorResolution == that.lenientConstructorResolution; - rtn = rtn &= ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues); - rtn = rtn &= ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues); - rtn = rtn &= ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides); - rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName); - rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName); - rtn = rtn &= ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName); - rtn = rtn &= this.enforceInitMethod == that.enforceInitMethod; - rtn = rtn &= ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName); - rtn = rtn &= this.enforceDestroyMethod == that.enforceDestroyMethod; - rtn = rtn &= this.synthetic == that.synthetic; - rtn = rtn &= this.role == that.role; + rtn = rtn && ObjectUtils.nullSafeEquals(this.scope, that.scope); + rtn = rtn && this.abstractFlag == that.abstractFlag; + rtn = rtn && this.lazyInit == that.lazyInit; + rtn = rtn && this.autowireMode == that.autowireMode; + rtn = rtn && this.dependencyCheck == that.dependencyCheck; + rtn = rtn && Arrays.equals(this.dependsOn, that.dependsOn); + rtn = rtn && this.autowireCandidate == that.autowireCandidate; + rtn = rtn && ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers); + rtn = rtn && this.primary == that.primary; + rtn = rtn && this.nonPublicAccessAllowed == that.nonPublicAccessAllowed; + rtn = rtn && this.lenientConstructorResolution == that.lenientConstructorResolution; + rtn = rtn && ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues); + rtn = rtn && ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues); + rtn = rtn && ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides); + rtn = rtn && ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName); + rtn = rtn && ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName); + rtn = rtn && ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName); + rtn = rtn && this.enforceInitMethod == that.enforceInitMethod; + rtn = rtn && ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName); + rtn = rtn && this.enforceDestroyMethod == that.enforceDestroyMethod; + rtn = rtn && this.synthetic == that.synthetic; + rtn = rtn && this.role == that.role; return rtn && super.equals(other); } From 529f8ba78635e843d1f815002aac152361f66dd0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 22 Nov 2019 15:55:44 +0000 Subject: [PATCH 050/322] Extra isReady-onWritePossible after last write Closes gh-24050 --- .../AbstractListenerWriteProcessor.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 4f05d5e1e09..3f37a955806 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -62,8 +62,17 @@ public abstract class AbstractListenerWriteProcessor implements Processor void onNext(AbstractListenerWriteProcessor processor, T data) { } @Override public void onComplete(AbstractListenerWriteProcessor processor) { - processor.changeStateToComplete(this); + processor.readyToCompleteAfterLastWrite = true; + processor.changeStateToReceived(this); } }, @@ -352,7 +362,10 @@ public void onComplete(AbstractListenerWriteProcessor processor) { @SuppressWarnings("deprecation") @Override public void onWritePossible(AbstractListenerWriteProcessor processor) { - if (processor.changeState(this, WRITING)) { + if (processor.readyToCompleteAfterLastWrite) { + processor.changeStateToComplete(RECEIVED); + } + else if (processor.changeState(this, WRITING)) { T data = processor.currentData; Assert.state(data != null, "No data"); try { @@ -360,7 +373,8 @@ public void onWritePossible(AbstractListenerWriteProcessor processor) { if (processor.changeState(WRITING, REQUESTED)) { processor.currentData = null; if (processor.subscriberCompleted) { - processor.changeStateToComplete(REQUESTED); + processor.readyToCompleteAfterLastWrite = true; + processor.changeStateToReceived(REQUESTED); } else { processor.writingPaused(); From fbde98f36e616a013b9a828586c9f138853ef813 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 22 Nov 2019 16:38:43 +0000 Subject: [PATCH 051/322] Add missing verify() in Jackson2TokenizerTests Closes gh-24056 --- .../http/codec/json/Jackson2TokenizerTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index 5c08550c07e..aa25442cb00 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -210,8 +210,8 @@ public void testLimit() { .expectNext(expected) .verifyComplete(); - StepVerifier.create(decode(source, false, maxInMemorySize - 1)) - .expectError(DataBufferLimitException.class); + StepVerifier.create(decode(source, false, maxInMemorySize - 2)) + .verifyError(DataBufferLimitException.class); } @Test From 2179b67706a019dea487398180e3e5709c7cf9d0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 24 Nov 2019 21:47:25 +0100 Subject: [PATCH 052/322] Upgrade to Tomcat 9.0.29, Jetty 9.4.24, RxJava 2.2.15 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b651148ade8..1841f33d49d 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ ext { groovyVersion = "2.5.8" hsqldbVersion = "2.4.1" jackson2Version = "2.9.9" - jettyVersion = "9.4.23.v20191118" + jettyVersion = "9.4.24.v20191120" junit5Version = "5.3.2" kotlinVersion = "1.2.71" log4jVersion = "2.11.2" @@ -41,10 +41,10 @@ ext { reactorVersion = "Californium-BUILD-SNAPSHOT" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" - rxjava2Version = "2.2.14" + rxjava2Version = "2.2.15" slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" - tomcatVersion = "9.0.27" + tomcatVersion = "9.0.29" undertowVersion = "2.0.28.Final" gradleScriptDir = "${rootProject.projectDir}/gradle" From c164759c41547039343c7be9fcdbce395180c32f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 25 Nov 2019 11:40:53 +0100 Subject: [PATCH 053/322] Fix NullPointerException in Jackson2SmileDecoder Fix uncommon case in Jackson2SmileDecoder, where a null token, incicating a document separator in streaming mode, is followed by NOT_AVAILABLE. Closes gh-24009 (cherry picked from commit 5f3c7ca559ac5f7d4d37917d568001579b596f2b) --- .../http/codec/json/Jackson2Tokenizer.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 606f0a34aa1..60c8c311481 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -120,13 +120,18 @@ private Flux endOfInput() { private List parseTokenBufferFlux() throws IOException { List result = new ArrayList<>(); - while (true) { + // SPR-16151: Smile data format uses null to separate documents + boolean previousNull = false; + while (!this.parser.isClosed()) { JsonToken token = this.parser.nextToken(); - // SPR-16151: Smile data format uses null to separate documents if (token == JsonToken.NOT_AVAILABLE || - (token == null && (token = this.parser.nextToken()) == null)) { + token == null && previousNull) { break; } + else if (token == null ) { // !previousNull + previousNull = true; + continue; + } updateDepth(token); if (!this.tokenizeArrayElements) { processTokenNormal(token, result); @@ -167,6 +172,9 @@ private void processTokenNormal(JsonToken token, List result) throw private void processTokenArray(JsonToken token, List result) throws IOException { if (!isTopLevelArrayToken(token)) { + if (!this.parser.hasCurrentToken()) { + System.out.println("NO CURRENT TOKEN: " + token); + } this.tokenBuffer.copyCurrentEvent(this.parser); } From f5b082d3b3a36c1356da17455a722401681ea2b0 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 25 Nov 2019 18:27:13 +0100 Subject: [PATCH 054/322] Remove println --- .../org/springframework/http/codec/json/Jackson2Tokenizer.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 60c8c311481..9b4b2842762 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -172,9 +172,6 @@ private void processTokenNormal(JsonToken token, List result) throw private void processTokenArray(JsonToken token, List result) throws IOException { if (!isTopLevelArrayToken(token)) { - if (!this.parser.hasCurrentToken()) { - System.out.println("NO CURRENT TOKEN: " + token); - } this.tokenBuffer.copyCurrentEvent(this.parser); } From 4fcc09a02c6ef4642c5237abec57204136c6abe5 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 27 Nov 2019 11:51:27 +0100 Subject: [PATCH 055/322] Upgrade to Reactor Californium-SR14 Closes gh-24038 --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1841f33d49d..4253eafe6ca 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ ext { kotlinVersion = "1.2.71" log4jVersion = "2.11.2" nettyVersion = "4.1.43.Final" - reactorVersion = "Californium-BUILD-SNAPSHOT" + reactorVersion = "Californium-SR14" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" rxjava2Version = "2.2.15" @@ -148,7 +148,6 @@ configure(allprojects) { project -> repositories { maven { url "https://repo.spring.io/libs-release" } - maven { url "https://repo.spring.io/snapshot" } mavenLocal() } From 59084c6b73260513905895d2d52fdca3fea8a1b9 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 28 Nov 2019 13:42:59 +0100 Subject: [PATCH 056/322] Test status quo for AnnotatedTypeMetadata.getAnnotationAttributes() See gh-24077 --- .../springframework/core/type/AnnotationMetadataTests.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index eb1fdf6db70..48a696ede03 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -96,8 +96,12 @@ private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata) { assertThat(metadata.hasAnnotation(Component.class.getName()), is(false)); assertThat(metadata.hasAnnotation(Scope.class.getName()), is(false)); assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(false)); + assertThat(metadata.hasMetaAnnotation(Component.class.getName()), is(false)); + assertThat(metadata.hasMetaAnnotation(MetaAnnotation.class.getName()), is(false)); assertThat(metadata.getAnnotationTypes().size(), is(0)); assertThat(metadata.getAnnotationAttributes(Component.class.getName()), nullValue()); + assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), false), nullValue()); + assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), true), nullValue()); assertThat(metadata.getAnnotatedMethods(DirectAnnotation.class.getName()).size(), equalTo(0)); assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName()), equalTo(false)); assertThat(metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()), nullValue()); From 7fdf775394f3c10b83f45c6aabda265fb5367186 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 28 Nov 2019 18:19:44 +0100 Subject: [PATCH 057/322] Test status quo for @Inherited annotations in AnnotationMetadata See gh-24077 --- .../core/type/AnnotationMetadataTests.java | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 48a696ede03..98c688ab342 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -37,7 +38,7 @@ import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.stereotype.Component; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; /** @@ -70,7 +71,7 @@ public void asmAnnotationMetadata() throws Exception { @Test public void standardAnnotationMetadataForSubclass() { AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponentSubClass.class, true); - doTestSubClassAnnotationInfo(metadata); + doTestSubClassAnnotationInfo(metadata, false); } @Test @@ -78,10 +79,10 @@ public void asmAnnotationMetadataForSubclass() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponentSubClass.class.getName()); AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); - doTestSubClassAnnotationInfo(metadata); + doTestSubClassAnnotationInfo(metadata, true); } - private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata) { + private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata, boolean asm) { assertThat(metadata.getClassName(), is(AnnotatedComponentSubClass.class.getName())); assertThat(metadata.isInterface(), is(false)); assertThat(metadata.isAnnotation(), is(false)); @@ -93,12 +94,23 @@ private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata) { assertThat(metadata.isAnnotated(Component.class.getName()), is(false)); assertThat(metadata.isAnnotated(Scope.class.getName()), is(false)); assertThat(metadata.isAnnotated(SpecialAttr.class.getName()), is(false)); + + if (asm) { + assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(false)); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(false)); + assertThat(metadata.getAnnotationTypes(), is(emptyCollectionOf(String.class))); + } + else { + assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(true)); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(true)); + assertThat(metadata.getAnnotationTypes(), containsInAnyOrder(NamedComposedAnnotation.class.getName())); + } + assertThat(metadata.hasAnnotation(Component.class.getName()), is(false)); assertThat(metadata.hasAnnotation(Scope.class.getName()), is(false)); assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(false)); assertThat(metadata.hasMetaAnnotation(Component.class.getName()), is(false)); assertThat(metadata.hasMetaAnnotation(MetaAnnotation.class.getName()), is(false)); - assertThat(metadata.getAnnotationTypes().size(), is(0)); assertThat(metadata.getAnnotationAttributes(Component.class.getName()), nullValue()); assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), false), nullValue()); assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), true), nullValue()); @@ -266,13 +278,18 @@ private void doTestAnnotationInfo(AnnotationMetadata metadata) { assertThat(metadata.getInterfaceNames().length, is(1)); assertThat(metadata.getInterfaceNames()[0], is(Serializable.class.getName())); + assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(true)); + assertThat(metadata.isAnnotated(Component.class.getName()), is(true)); assertThat(metadata.hasAnnotation(Component.class.getName()), is(true)); assertThat(metadata.hasAnnotation(Scope.class.getName()), is(true)); assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(true)); - assertThat(metadata.getAnnotationTypes().size(), is(6)); - assertThat(metadata.getAnnotationTypes().contains(Component.class.getName()), is(true)); - assertThat(metadata.getAnnotationTypes().contains(Scope.class.getName()), is(true)); - assertThat(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()), is(true)); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(true)); + assertThat(metadata.getAnnotationTypes(), + containsInAnyOrder(Component.class.getName(), Scope.class.getName(), + SpecialAttr.class.getName(), DirectAnnotation.class.getName(), + MetaMetaAnnotation.class.getName(), + EnumSubclasses.class.getName(), + NamedComposedAnnotation.class.getName())); AnnotationAttributes compAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Component.class.getName()); assertThat(compAttrs.size(), is(1)); @@ -469,6 +486,7 @@ public enum SubclassEnum { @DirectAnnotation(value = "direct", additional = "", additionalArray = {}) @MetaMetaAnnotation @EnumSubclasses({SubclassEnum.FOO, SubclassEnum.BAR}) + @NamedComposedAnnotation private static class AnnotatedComponent implements Serializable { @TestAutowired @@ -549,6 +567,7 @@ public static class NamedAnnotationsClass { @NamedAnnotation3(name = "name 3") @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) + @Inherited public @interface NamedComposedAnnotation { } From 43e047c523fefa1fc876208efd6946ceb2e472a3 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 29 Nov 2019 22:26:52 +0100 Subject: [PATCH 058/322] 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-24106 --- .../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 | 38 ++++++++++++++++-- .../function/client/ExchangeStrategies.java | 12 ++++++ .../reactive/function/client/WebClient.java | 28 ++++++++++++- .../client/ExchangeStrategiesTests.java | 11 +++++ src/docs/asciidoc/web/webflux-webclient.adoc | 40 ++++++++++++++++--- 17 files changed, 322 insertions(+), 31 deletions(-) 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 f314b4455da..4d5aeca17eb 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 @@ public WebTestClient.Builder filters(Consumer> filt } @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 cc2e8ab96fc..254d0337fd0 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,34 @@ interface Builder { /** * 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. @@ -877,7 +901,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 +915,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 db31e97218a..028d85af381 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 3d4c625b5d5..bd573d08bae 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 e86ac954f9f..f67821f98b7 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 { - 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 @@ public List> getWriters() { 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 @@ protected List> getWritersInternal(boolean forMultipart) { /** * 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 @@ private static final class DefaultCustomCodecs implements CustomCodecs { 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 @@ public void writer(HttpMessageWriter writer) { (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 f3034ad9354..5cf59998af2 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,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 9f578b7320a..e764cb96961 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 9875ded1b98..737282eecd5 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 2623d5a7f7b..661d45d6669 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 37e924cd7e9..1d997c3777b 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 48e20a9074d..93d981c713e 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 @@ -267,6 +267,14 @@ public void encoderDecoderOverrides() { assertEncoderInstance(jaxb2Encoder); } + @Test + public void cloneConfigurer() { + CodecConfigurer clone = this.configurer.clone(); + this.configurer.registerDefaults(false); + assertEquals(0, this.configurer.getReaders().size()); + assertEquals(11, clone.getReaders().size()); + } + private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); 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 aa1523d9ace..02b0cc5e558 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) { @@ -84,6 +91,12 @@ private static List unmodifiableCopy(List list) { } + @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 c8796e91f78..6f9a755a7fa 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 @@ -38,6 +38,7 @@ * Default implementation of {@link WebClient.Builder}. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 */ final class DefaultWebClientBuilder implements WebClient.Builder { @@ -66,14 +67,16 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private ClientHttpConnector connector; - private ExchangeStrategies exchangeStrategies; + @Nullable + private ExchangeStrategies.Builder exchangeStrategies; + + private List> strategiesConfigurers; @Nullable private ExchangeFunction exchangeFunction; public DefaultWebClientBuilder() { - this.exchangeStrategies = ExchangeStrategies.withDefaults(); } public DefaultWebClientBuilder(DefaultWebClientBuilder other) { @@ -190,12 +193,26 @@ public WebClient.Builder clientConnector(ClientHttpConnector connector) { } @Override + @Deprecated public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { + Assert.notNull(strategies, "ExchangeStrategies must not be null"); + this.exchangeStrategies = strategies.mutate(); + return this; + } + + @Override + public WebClient.Builder exchangeStrategies(ExchangeStrategies.Builder strategies) { Assert.notNull(strategies, "ExchangeStrategies must not be null"); this.exchangeStrategies = strategies; return this; } + @Override + public WebClient.Builder exchangeStrategies(Consumer configurer) { + this.strategiesConfigurers.add(configurer); + return this; + } + @Override public WebClient.Builder exchangeFunction(ExchangeFunction exchangeFunction) { this.exchangeFunction = exchangeFunction; @@ -231,11 +248,24 @@ private ExchangeFunction initExchangeFunction() { return this.exchangeFunction; } else if (this.connector != null) { - return ExchangeFunctions.create(this.connector, this.exchangeStrategies); + return ExchangeFunctions.create(this.connector, initExchangeStrategies()); } else { - return ExchangeFunctions.create(new ReactorClientHttpConnector(), this.exchangeStrategies); + return ExchangeFunctions.create(new ReactorClientHttpConnector(), initExchangeStrategies()); + } + } + + @SuppressWarnings("deprecation") + private ExchangeStrategies initExchangeStrategies() { + if (CollectionUtils.isEmpty(this.strategiesConfigurers)) { + return this.exchangeStrategies != null ? this.exchangeStrategies.build() : ExchangeStrategies.withDefaults(); } + + ExchangeStrategies.Builder builder = + this.exchangeStrategies != null ? this.exchangeStrategies : 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 804fbd9a42f..dfc2e1e14d5 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 8dc2a17c012..91a45fe0648 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 { @@ -288,12 +289,35 @@ interface Builder { 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 b08662c8fb9..09f7cb24ef7 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 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 20980df9dd8..7f6523fa3b1 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -42,14 +42,14 @@ The following example configures < { - // ... - }) - .build(); + Consumer customizeCodecs = builder -> { + builder.codecs(configurer -> { + //... + }); + }; WebClient client = WebClient.builder() - .exchangeStrategies(strategies) + .exchangeStrategies(customizeCodecs) .build(); ---- ==== @@ -73,7 +73,35 @@ modified copy without affecting the original instance, as the following example ---- ==== +[[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. + +You can configure a default value that might not be enough for your use case, and your application +can 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,intent=0] +[subs="verbatim,quotes"] +---- + WebClient webClient = WebClient.builder() + .exchangeStrategies(configurer -> + configurer.codecs(codecs -> + codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) + ) + ) + .build(); +---- +==== [[webflux-client-builder-reactor]] === Reactor Netty From fd68fb115d49162f7fa0ed006afc9ebc62a8a2c3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 1 Dec 2019 00:40:57 +0100 Subject: [PATCH 059/322] Backport of recent ExtendedBeanInfo refinements from master Closes gh-24095 --- .../beans/ExtendedBeanInfo.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java index 3ca7fc95c7e..f5c165d5cfd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.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. @@ -41,8 +41,8 @@ /** * Decorator for a standard {@link BeanInfo} object, e.g. as created by - * {@link Introspector#getBeanInfo(Class)}, designed to discover and register static - * and/or non-void returning setter methods. For example: + * {@link Introspector#getBeanInfo(Class)}, designed to discover and register + * static and/or non-void returning setter methods. For example: * *

  * public class Bean {
@@ -145,11 +145,10 @@ private List findCandidateWriteMethods(MethodDescriptor[] methodDescript
 
 	public static boolean isCandidateWriteMethod(Method method) {
 		String methodName = method.getName();
-		Class[] parameterTypes = method.getParameterTypes();
-		int nParams = parameterTypes.length;
+		int nParams = method.getParameterCount();
 		return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) &&
 				(!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) &&
-				(nParams == 1 || (nParams == 2 && int.class == parameterTypes[0])));
+				(nParams == 1 || (nParams == 2 && int.class == method.getParameterTypes()[0])));
 	}
 
 	private void handleCandidateWriteMethod(Method method) throws IntrospectionException {
@@ -209,7 +208,7 @@ private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, C
 	}
 
 	private String propertyNameFor(Method method) {
-		return Introspector.decapitalize(method.getName().substring(3, method.getName().length()));
+		return Introspector.decapitalize(method.getName().substring(3));
 	}
 
 
@@ -488,7 +487,7 @@ public void setPropertyEditorClass(@Nullable Class propertyEditorClass) {
 		}
 
 		/*
-		 * See java.beans.IndexedPropertyDescriptor#equals(java.lang.Object)
+		 * See java.beans.IndexedPropertyDescriptor#equals
 		 */
 		@Override
 		public boolean equals(Object other) {
@@ -535,11 +534,13 @@ static class PropertyDescriptorComparator implements Comparator
Date: Sun, 1 Dec 2019 00:41:53 +0100
Subject: [PATCH 060/322] Polishing

---
 .../factory/groovy/GroovyBeanDefinitionReader.java    | 11 ++++-------
 .../client/DefaultExchangeStrategiesBuilder.java      |  5 ++---
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java
index ef6d5c6731b..bdfb2d05bab 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.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.
@@ -310,7 +310,7 @@ public AbstractBeanDefinition bean(Class type, Object...args) {
 		GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
 		try {
 			Closure callable = null;
-			Collection constructorArgs = null;
+			Collection constructorArgs = null;
 			if (!ObjectUtils.isEmpty(args)) {
 				int index = args.length;
 				Object lastArg = args[index - 1];
@@ -318,16 +318,13 @@ public AbstractBeanDefinition bean(Class type, Object...args) {
 					callable = (Closure) lastArg;
 					index--;
 				}
-				if (index > -1) {
-					constructorArgs = resolveConstructorArguments(args, 0, index);
-				}
+				constructorArgs = resolveConstructorArguments(args, 0, index);
 			}
 			this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, type, constructorArgs);
 			if (callable != null) {
 				callable.call(this.currentBeanDefinition);
 			}
 			return this.currentBeanDefinition.getBeanDefinition();
-
 		}
 		finally {
 			this.currentBeanDefinition = current;
@@ -810,7 +807,7 @@ public boolean add(Object value) {
 				return retVal;
 			}
 
-			public boolean addAll(Collection values) {
+			public boolean addAll(Collection values) {
 				boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "addAll", values);
 				for (Object value : values) {
 					updateDeferredProperties(value);
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 02b0cc5e558..e39d4a59576 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
@@ -29,6 +29,7 @@
  * Default implementation of {@link ExchangeStrategies.Builder}.
  *
  * @author Arjen Poutsma
+ * @author Brian Clozel
  * @since 5.0
  */
 final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder {
@@ -79,7 +80,6 @@ private static class DefaultExchangeStrategies implements ExchangeStrategies {
 
 		private final List> writers;
 
-
 		public DefaultExchangeStrategies(ClientCodecConfigurer codecConfigurer) {
 			this.codecConfigurer = codecConfigurer;
 			this.readers = unmodifiableCopy(this.codecConfigurer.getReaders());
@@ -90,9 +90,8 @@ private static  List unmodifiableCopy(List list) {
 			return Collections.unmodifiableList(new ArrayList<>(list));
 		}
 
-
-		@Override
 		@Deprecated
+		@Override
 		public Builder mutate() {
 			return new DefaultExchangeStrategiesBuilder(this);
 		}

From 4f86282b142045b2e8f9002eee413d90c9e50567 Mon Sep 17 00:00:00 2001
From: Juergen Hoeller 
Date: Sun, 1 Dec 2019 01:55:26 +0100
Subject: [PATCH 061/322] Polishing

---
 .../groovy/GroovyBeanDefinitionReader.java    | 65 ++++++++++++-------
 .../groovy/GroovyBeanDefinitionWrapper.java   |  4 +-
 .../support/DefaultSingletonBeanRegistry.java |  1 +
 .../interceptor/JCacheAspectSupport.java      |  1 +
 .../quartz/LocalDataSourceJobStore.java       |  6 +-
 .../quartz/ResourceLoaderClassLoadHelper.java |  3 +-
 .../GenericGroovyApplicationContext.java      |  5 ++
 .../jms/connection/CachedMessageProducer.java |  2 +
 .../support/GroovyWebApplicationContext.java  |  5 ++
 .../view/groovy/GroovyMarkupConfigurer.java   |  1 +
 10 files changed, 63 insertions(+), 30 deletions(-)

diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java
index bdfb2d05bab..cf13a408173 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java
@@ -182,10 +182,12 @@ public GroovyBeanDefinitionReader(XmlBeanDefinitionReader xmlBeanDefinitionReade
 	}
 
 
+	@Override
 	public void setMetaClass(MetaClass metaClass) {
 		this.metaClass = metaClass;
 	}
 
+	@Override
 	public MetaClass getMetaClass() {
 		return this.metaClass;
 	}
@@ -216,6 +218,7 @@ public Binding getBinding() {
 	 * @return the number of bean definitions found
 	 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 	 */
+	@Override
 	public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
 		return loadBeanDefinitions(new EncodedResource(resource));
 	}
@@ -240,10 +243,11 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin
 			logger.trace("Loading Groovy bean definitions from " + encodedResource);
 		}
 
-		Closure beans = new Closure(this) {
+		@SuppressWarnings("serial")
+		Closure beans = new Closure(this) {
 			@Override
-			public Object call(Object[] args) {
-				invokeBeanDefiningClosure((Closure) args[0]);
+			public Object call(Object... args) {
+				invokeBeanDefiningClosure((Closure) args[0]);
 				return null;
 			}
 		};
@@ -285,7 +289,7 @@ public void setVariable(String name, Object value) {
 	 * @param closure the block or closure
 	 * @return this {@code GroovyBeanDefinitionReader} instance
 	 */
-	public GroovyBeanDefinitionReader beans(Closure closure) {
+	public GroovyBeanDefinitionReader beans(Closure closure) {
 		return invokeBeanDefiningClosure(closure);
 	}
 
@@ -309,13 +313,13 @@ public GenericBeanDefinition bean(Class type) {
 	public AbstractBeanDefinition bean(Class type, Object...args) {
 		GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
 		try {
-			Closure callable = null;
+			Closure callable = null;
 			Collection constructorArgs = null;
 			if (!ObjectUtils.isEmpty(args)) {
 				int index = args.length;
 				Object lastArg = args[index - 1];
-				if (lastArg instanceof Closure) {
-					callable = (Closure) lastArg;
+				if (lastArg instanceof Closure) {
+					callable = (Closure) lastArg;
 					index--;
 				}
 				constructorArgs = resolveConstructorArguments(args, 0, index);
@@ -370,10 +374,11 @@ public void importBeans(String resourcePattern) throws IOException {
 	 * This method overrides method invocation to create beans for each method name that
 	 * takes a class argument.
 	 */
+	@Override
 	public Object invokeMethod(String name, Object arg) {
 		Object[] args = (Object[])arg;
 		if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure) {
-			return beans((Closure) args[0]);
+			return beans((Closure) args[0]);
 		}
 		else if ("ref".equals(name)) {
 			String refName;
@@ -426,10 +431,10 @@ private boolean addDeferredProperty(String property, Object newValue) {
 	private void finalizeDeferredProperties() {
 		for (DeferredProperty dp : this.deferredProperties.values()) {
 			if (dp.value instanceof List) {
-				dp.value = manageListIfNecessary((List) dp.value);
+				dp.value = manageListIfNecessary((List) dp.value);
 			}
 			else if (dp.value instanceof Map) {
-				dp.value = manageMapIfNecessary((Map) dp.value);
+				dp.value = manageMapIfNecessary((Map) dp.value);
 			}
 			dp.apply();
 		}
@@ -441,7 +446,7 @@ else if (dp.value instanceof Map) {
 	 * @param callable the closure argument
 	 * @return this {@code GroovyBeanDefinitionReader} instance
 	 */
-	protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callable) {
+	protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callable) {
 		callable.setDelegate(this);
 		callable.call();
 		finalizeDeferredProperties();
@@ -480,9 +485,10 @@ else if (args[0] instanceof RuntimeBeanReference) {
 		else if (args[0] instanceof Map) {
 			// named constructor arguments
 			if (args.length > 1 && args[1] instanceof Class) {
-				List constructorArgs = resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
-				this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class)args[1], constructorArgs);
-				Map namedArgs = (Map)args[0];
+				List constructorArgs =
+						resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
+				this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class) args[1], constructorArgs);
+				Map namedArgs = (Map) args[0];
 				for (Object o : namedArgs.keySet()) {
 					String propName = (String) o;
 					setProperty(propName, namedArgs.get(propName));
@@ -491,8 +497,8 @@ else if (args[0] instanceof Map) {
 			// factory method syntax
 			else {
 				this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
-				//First arg is the map containing factoryBean : factoryMethod
-				Map.Entry factoryBeanEntry = (Map.Entry) ((Map) args[0]).entrySet().iterator().next();
+				// First arg is the map containing factoryBean : factoryMethod
+				Map.Entry factoryBeanEntry = ((Map) args[0]).entrySet().iterator().next();
 				// If we have a closure body, that will be the last argument.
 				// In between are the constructor args
 				int constructorArgsTest = (hasClosureArgument ? 2 : 1);
@@ -516,12 +522,13 @@ else if (args[0] instanceof Closure) {
 			this.currentBeanDefinition.getBeanDefinition().setAbstract(true);
 		}
 		else {
-			List constructorArgs = resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
+			List constructorArgs =
+					resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
 			this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, constructorArgs);
 		}
 
 		if (hasClosureArgument) {
-			Closure callable = (Closure) args[args.length - 1];
+			Closure callable = (Closure) args[args.length - 1];
 			callable.setDelegate(this);
 			callable.setResolveStrategy(Closure.DELEGATE_FIRST);
 			callable.call(this.currentBeanDefinition);
@@ -541,10 +548,10 @@ protected List resolveConstructorArguments(Object[] args, int start, int
 				constructorArgs[i] = constructorArgs[i].toString();
 			}
 			else if (constructorArgs[i] instanceof List) {
-				constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]);
+				constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]);
 			}
 			else if (constructorArgs[i] instanceof Map){
-				constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]);
+				constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]);
 			}
 		}
 		return Arrays.asList(constructorArgs);
@@ -598,6 +605,7 @@ private Object manageListIfNecessary(List list) {
 	 * This method overrides property setting in the scope of the {@code GroovyBeanDefinitionReader}
 	 * to set properties on the current bean definition.
 	 */
+	@Override
 	public void setProperty(String name, Object value) {
 		if (this.currentBeanDefinition != null) {
 			applyPropertyToBeanDefinition(name, value);
@@ -614,7 +622,7 @@ protected void applyPropertyToBeanDefinition(String name, Object value) {
 		else if (value instanceof Closure) {
 			GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
 			try {
-				Closure callable = (Closure) value;
+				Closure callable = (Closure) value;
 				Class parameterType = callable.getParameterTypes()[0];
 				if (Object.class == parameterType) {
 					this.currentBeanDefinition = new GroovyBeanDefinitionWrapper("");
@@ -644,6 +652,7 @@ else if (value instanceof Closure) {
 	 * properties from the {@code GroovyBeanDefinitionReader} itself
 	 * 
 	 */
+	@Override
 	public Object getProperty(String name) {
 		Binding binding = getBinding();
 		if (binding != null && binding.hasVariable(name)) {
@@ -687,8 +696,8 @@ else if (this.currentBeanDefinition != null) {
 	}
 
 	private GroovyDynamicElementReader createDynamicElementReader(String namespace) {
-		XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(new DescriptiveResource(
-			"Groovy"));
+		XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(
+				new DescriptiveResource("Groovy"));
 		BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
 		boolean decorating = (this.currentBeanDefinition != null);
 		if (!decorating) {
@@ -746,10 +755,12 @@ public GroovyRuntimeBeanReference(String beanName, GroovyBeanDefinitionWrapper b
 			this.metaClass = InvokerHelper.getMetaClass(this);
 		}
 
+		@Override
 		public MetaClass getMetaClass() {
 			return this.metaClass;
 		}
 
+		@Override
 		public Object getProperty(String property) {
 			if (property.equals("beanName")) {
 				return getBeanName();
@@ -766,14 +777,17 @@ else if (this.beanDefinition != null) {
 			}
 		}
 
+		@Override
 		public Object invokeMethod(String name, Object args) {
 			return this.metaClass.invokeMethod(this, name, args);
 		}
 
+		@Override
 		public void setMetaClass(MetaClass metaClass) {
 			this.metaClass = metaClass;
 		}
 
+		@Override
 		public void setProperty(String property, Object newValue) {
 			if (!addDeferredProperty(property, newValue)) {
 				this.beanDefinition.getBeanDefinition().getPropertyValues().add(property, newValue);
@@ -782,7 +796,7 @@ public void setProperty(String property, Object newValue) {
 
 
 		/**
-		 * Wraps a bean definition property an ensures that any RuntimeBeanReference
+		 * Wraps a bean definition property and ensures that any RuntimeBeanReference
 		 * additions to it are deferred for resolution later.
 		 */
 		private class GroovyPropertyValue extends GroovyObjectSupport {
@@ -796,17 +810,20 @@ public GroovyPropertyValue(String propertyName, Object propertyValue) {
 				this.propertyValue = propertyValue;
 			}
 
+			@SuppressWarnings("unused")
 			public void leftShift(Object value) {
 				InvokerHelper.invokeMethod(this.propertyValue, "leftShift", value);
 				updateDeferredProperties(value);
 			}
 
+			@SuppressWarnings("unused")
 			public boolean add(Object value) {
 				boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "add", value);
 				updateDeferredProperties(value);
 				return retVal;
 			}
 
+			@SuppressWarnings("unused")
 			public boolean addAll(Collection values) {
 				boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "addAll", values);
 				for (Object value : values) {
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java
index 9afc6e9c303..d1aeff7d0bc 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.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.
@@ -196,7 +196,7 @@ else if (Boolean.TRUE.equals(newValue)) {
 			// constructorArgs
 			else if (CONSTRUCTOR_ARGS.equals(property) && newValue instanceof List) {
 				ConstructorArgumentValues cav = new ConstructorArgumentValues();
-				List args = (List) newValue;
+				List args = (List) newValue;
 				for (Object arg : args) {
 					cav.addGenericArgumentValue(arg);
 				}
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
index 185a5807631..73756669126 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
@@ -612,6 +612,7 @@ protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
 	 * should not have their own mutexes involved in singleton creation,
 	 * to avoid the potential for deadlocks in lazy-init situations.
 	 */
+	@Override
 	public final Object getSingletonMutex() {
 		return this.singletonObjects;
 	}
diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java
index dca7f6135f2..893e4a6b792 100644
--- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java
+++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java
@@ -88,6 +88,7 @@ public JCacheOperationSource getCacheOperationSource() {
 		return this.cacheOperationSource;
 	}
 
+	@Override
 	public void afterPropertiesSet() {
 		getCacheOperationSource();
 
diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
index db86dd4e368..d47fa28c0ea 100644
--- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
+++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.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.
@@ -110,7 +110,7 @@ public Connection getConnection() throws SQLException {
 					public void shutdown() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
-					/* Quartz 2.2 initialize method */
+					@Override
 					public void initialize() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
@@ -138,7 +138,7 @@ public Connection getConnection() throws SQLException {
 					public void shutdown() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
-					/* Quartz 2.2 initialize method */
+					@Override
 					public void initialize() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
index 63cbb800bfa..996a598460d 100644
--- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
+++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 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.
@@ -82,6 +82,7 @@ public Class loadClass(String name) throws ClassNotFoundException {
 	}
 
 	@SuppressWarnings("unchecked")
+	@Override
 	public  Class loadClass(String name, Class clazz) throws ClassNotFoundException {
 		return (Class) loadClass(name);
 	}
diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java
index 79950d44576..8e1f74ba45d 100644
--- a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java
@@ -225,18 +225,22 @@ public void load(Class relativeClass, String... resourceNames) {
 
 	// Implementation of the GroovyObject interface
 
+	@Override
 	public void setMetaClass(MetaClass metaClass) {
 		this.metaClass = metaClass;
 	}
 
+	@Override
 	public MetaClass getMetaClass() {
 		return this.metaClass;
 	}
 
+	@Override
 	public Object invokeMethod(String name, Object args) {
 		return this.metaClass.invokeMethod(this, name, args);
 	}
 
+	@Override
 	public void setProperty(String property, Object newValue) {
 		if (newValue instanceof BeanDefinition) {
 			registerBeanDefinition(property, (BeanDefinition) newValue);
@@ -246,6 +250,7 @@ public void setProperty(String property, Object newValue) {
 		}
 	}
 
+	@Override
 	@Nullable
 	public Object getProperty(String property) {
 		if (containsBean(property)) {
diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java
index e7960c2e5e4..daae35af0aa 100644
--- a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java
+++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java
@@ -89,6 +89,7 @@ public boolean getDisableMessageTimestamp() throws JMSException {
 		return this.target.getDisableMessageTimestamp();
 	}
 
+	@Override
 	public void setDeliveryDelay(long deliveryDelay) throws JMSException {
 		if (this.originalDeliveryDelay == null) {
 			this.originalDeliveryDelay = this.target.getDeliveryDelay();
@@ -96,6 +97,7 @@ public void setDeliveryDelay(long deliveryDelay) throws JMSException {
 		this.target.setDeliveryDelay(deliveryDelay);
 	}
 
+	@Override
 	public long getDeliveryDelay() throws JMSException {
 		return this.target.getDeliveryDelay();
 	}
diff --git a/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java b/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java
index ca344393bea..25610fd6a3d 100644
--- a/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java
+++ b/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java
@@ -154,22 +154,27 @@ protected String[] getDefaultConfigLocations() {
 
 	// Implementation of the GroovyObject interface
 
+	@Override
 	public void setMetaClass(MetaClass metaClass) {
 		this.metaClass = metaClass;
 	}
 
+	@Override
 	public MetaClass getMetaClass() {
 		return this.metaClass;
 	}
 
+	@Override
 	public Object invokeMethod(String name, Object args) {
 		return this.metaClass.invokeMethod(this, name, args);
 	}
 
+	@Override
 	public void setProperty(String property, Object newValue) {
 		this.metaClass.setProperty(this, property, newValue);
 	}
 
+	@Override
 	@Nullable
 	public Object getProperty(String property) {
 		if (containsBean(property)) {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java
index 741ca35b14a..8c80a96ad7a 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java
@@ -121,6 +121,7 @@ public void setTemplateEngine(MarkupTemplateEngine templateEngine) {
 		this.templateEngine = templateEngine;
 	}
 
+	@Override
 	public MarkupTemplateEngine getTemplateEngine() {
 		Assert.state(this.templateEngine != null, "No MarkupTemplateEngine set");
 		return this.templateEngine;

From 25554d0b219ce5c15b9610b54c84b41d6dfadf03 Mon Sep 17 00:00:00 2001
From: Brian Clozel 
Date: Mon, 2 Dec 2019 10:29:24 +0100
Subject: [PATCH 062/322] Revert "Allow ExchangeStrategies customizations in
 WebClient"

This reverts commit 43e047c523fefa1fc876208efd6946ceb2e472a3.
---
 .../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     | 28 ++++---------
 .../client/DefaultWebClientBuilder.java       | 38 ++----------------
 .../function/client/ExchangeStrategies.java   | 12 ------
 .../reactive/function/client/WebClient.java   | 28 +------------
 .../client/ExchangeStrategiesTests.java       | 11 -----
 src/docs/asciidoc/web/webflux-webclient.adoc  | 40 +++----------------
 17 files changed, 32 insertions(+), 322 deletions(-)

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 4d5aeca17eb..f314b4455da 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-2019 the original author or authors.
+ * Copyright 2002-2018 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,24 +137,11 @@ public WebTestClient.Builder filters(Consumer> filt
 	}
 
 	@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 254d0337fd0..cc2e8ab96fc 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,7 +77,6 @@
  * 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
@@ -437,34 +436,11 @@ interface Builder {
 
 		/**
 		 * Configure the {@link ExchangeStrategies} to use.
-		 * 

This is useful for changing the default settings, yet still allowing - * further customizations via {@link #exchangeStrategies(Consumer)}. - * By default {@link ExchangeStrategies#withDefaults()} is used. + *

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. @@ -901,7 +877,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); } @@ -915,7 +891,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 028d85af381..db31e97218a 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-2019 the original author or authors. + * Copyright 2002-2018 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,11 +63,6 @@ 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 bd573d08bae..3d4c625b5d5 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,12 +87,6 @@ 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 f67821f98b7..e86ac954f9f 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,14 +34,13 @@ * client and server specific variants. * * @author Rossen Stoyanchev - * @author Brian Clozel * @since 5.0 */ class BaseCodecConfigurer implements CodecConfigurer { - protected final BaseDefaultCodecs defaultCodecs; + private final BaseDefaultCodecs defaultCodecs; - protected final DefaultCustomCodecs customCodecs; + private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs(); /** @@ -51,16 +50,6 @@ 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); } @@ -98,17 +87,6 @@ public List> getWriters() { 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"), @@ -132,7 +110,7 @@ protected List> getWritersInternal(boolean forMultipart) { /** * Default implementation of {@code CustomCodecs}. */ - protected static final class DefaultCustomCodecs implements CustomCodecs { + private static final class DefaultCustomCodecs implements CustomCodecs { private final List> typedReaders = new ArrayList<>(); @@ -143,16 +121,6 @@ protected static final class DefaultCustomCodecs implements CustomCodecs { 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)); @@ -175,6 +143,7 @@ public void writer(HttpMessageWriter writer) { (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 5cf59998af2..f3034ad9354 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,21 +105,6 @@ 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 e764cb96961..9f578b7320a 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-2019 the original author or authors. + * Copyright 2002-2018 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,17 +49,6 @@ 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. @@ -84,14 +73,6 @@ 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) { @@ -135,17 +116,6 @@ 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 737282eecd5..9875ded1b98 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-2019 the original author or authors. + * Copyright 2002-2018 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,30 +26,14 @@ */ 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 661d45d6669..2623d5a7f7b 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-2019 the original author or authors. + * Copyright 2002-2018 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,28 +26,13 @@ */ 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 1d997c3777b..37e924cd7e9 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,16 +46,6 @@ 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 93d981c713e..48e20a9074d 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 @@ -267,14 +267,6 @@ public void encoderDecoderOverrides() { assertEncoderInstance(jaxb2Encoder); } - @Test - public void cloneConfigurer() { - CodecConfigurer clone = this.configurer.clone(); - this.configurer.registerDefaults(false); - assertEquals(0, this.configurer.getReaders().size()); - assertEquals(11, clone.getReaders().size()); - } - private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); 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 e39d4a59576..aa1523d9ace 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-2019 the original author or authors. + * Copyright 2002-2018 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. @@ -29,7 +29,6 @@ * Default implementation of {@link ExchangeStrategies.Builder}. * * @author Arjen Poutsma - * @author Brian Clozel * @since 5.0 */ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder { @@ -43,18 +42,13 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build } - private final ClientCodecConfigurer codecConfigurer; + private final ClientCodecConfigurer codecConfigurer = ClientCodecConfigurer.create(); 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); @@ -68,33 +62,27 @@ public ExchangeStrategies.Builder codecs(Consumer consume @Override public ExchangeStrategies build() { - return new DefaultExchangeStrategies(this.codecConfigurer); + return new DefaultExchangeStrategies( + this.codecConfigurer.getReaders(), this.codecConfigurer.getWriters()); } private static class DefaultExchangeStrategies implements ExchangeStrategies { - private final ClientCodecConfigurer codecConfigurer; - private final List> readers; private final List> writers; - public DefaultExchangeStrategies(ClientCodecConfigurer codecConfigurer) { - this.codecConfigurer = codecConfigurer; - this.readers = unmodifiableCopy(this.codecConfigurer.getReaders()); - this.writers = unmodifiableCopy(this.codecConfigurer.getWriters()); + + public DefaultExchangeStrategies(List> readers, List> writers) { + this.readers = unmodifiableCopy(readers); + this.writers = unmodifiableCopy(writers); } private static List unmodifiableCopy(List list) { return Collections.unmodifiableList(new ArrayList<>(list)); } - @Deprecated - @Override - public Builder mutate() { - return new DefaultExchangeStrategiesBuilder(this); - } @Override public List> messageReaders() { 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 6f9a755a7fa..c8796e91f78 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 @@ -38,7 +38,6 @@ * Default implementation of {@link WebClient.Builder}. * * @author Rossen Stoyanchev - * @author Brian Clozel * @since 5.0 */ final class DefaultWebClientBuilder implements WebClient.Builder { @@ -67,16 +66,14 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private ClientHttpConnector connector; - @Nullable - private ExchangeStrategies.Builder exchangeStrategies; - - private List> strategiesConfigurers; + private ExchangeStrategies exchangeStrategies; @Nullable private ExchangeFunction exchangeFunction; public DefaultWebClientBuilder() { + this.exchangeStrategies = ExchangeStrategies.withDefaults(); } public DefaultWebClientBuilder(DefaultWebClientBuilder other) { @@ -193,26 +190,12 @@ public WebClient.Builder clientConnector(ClientHttpConnector connector) { } @Override - @Deprecated public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { - Assert.notNull(strategies, "ExchangeStrategies must not be null"); - this.exchangeStrategies = strategies.mutate(); - return this; - } - - @Override - public WebClient.Builder exchangeStrategies(ExchangeStrategies.Builder strategies) { Assert.notNull(strategies, "ExchangeStrategies must not be null"); this.exchangeStrategies = strategies; return this; } - @Override - public WebClient.Builder exchangeStrategies(Consumer configurer) { - this.strategiesConfigurers.add(configurer); - return this; - } - @Override public WebClient.Builder exchangeFunction(ExchangeFunction exchangeFunction) { this.exchangeFunction = exchangeFunction; @@ -248,24 +231,11 @@ private ExchangeFunction initExchangeFunction() { return this.exchangeFunction; } else if (this.connector != null) { - return ExchangeFunctions.create(this.connector, initExchangeStrategies()); + return ExchangeFunctions.create(this.connector, this.exchangeStrategies); } else { - return ExchangeFunctions.create(new ReactorClientHttpConnector(), initExchangeStrategies()); - } - } - - @SuppressWarnings("deprecation") - private ExchangeStrategies initExchangeStrategies() { - if (CollectionUtils.isEmpty(this.strategiesConfigurers)) { - return this.exchangeStrategies != null ? this.exchangeStrategies.build() : ExchangeStrategies.withDefaults(); + return ExchangeFunctions.create(new ReactorClientHttpConnector(), this.exchangeStrategies); } - - ExchangeStrategies.Builder builder = - this.exchangeStrategies != null ? this.exchangeStrategies : 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 dfc2e1e14d5..804fbd9a42f 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,18 +47,6 @@ 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 91a45fe0648..8dc2a17c012 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,7 +64,6 @@ * * @author Rossen Stoyanchev * @author Arjen Poutsma - * @author Brian Clozel * @since 5.0 */ public interface WebClient { @@ -289,35 +288,12 @@ interface Builder { Builder clientConnector(ClientHttpConnector connector); /** - * 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()}. + * Configure the {@link ExchangeStrategies} to use. + *

By default this is 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 09f7cb24ef7..b08662c8fb9 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,15 +39,4 @@ 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 7f6523fa3b1..20980df9dd8 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -42,14 +42,14 @@ The following example configures < customizeCodecs = builder -> { - builder.codecs(configurer -> { - //... - }); - }; + ExchangeStrategies strategies = ExchangeStrategies.builder() + .codecs(configurer -> { + // ... + }) + .build(); WebClient client = WebClient.builder() - .exchangeStrategies(customizeCodecs) + .exchangeStrategies(strategies) .build(); ---- ==== @@ -73,35 +73,7 @@ modified copy without affecting the original instance, as the following example ---- ==== -[[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. - -You can configure a default value that might not be enough for your use case, and your application -can 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,intent=0] -[subs="verbatim,quotes"] ----- - WebClient webClient = WebClient.builder() - .exchangeStrategies(configurer -> - configurer.codecs(codecs -> - codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) - ) - ) - .build(); ----- -==== [[webflux-client-builder-reactor]] === Reactor Netty From 59165dd52698151a7093596b3cc89415759e45ce Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 2 Dec 2019 13:10:07 +0100 Subject: [PATCH 063/322] Upgrade to AspectJ 1.9.5 and Checkstyle 8.27 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4253eafe6ca..974b07de3de 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ ext { !it.name.equals("spring-build-src") && !it.name.equals("spring-framework-bom") } - aspectjVersion = "1.9.4" + aspectjVersion = "1.9.5" freemarkerVersion = "2.3.28" groovyVersion = "2.5.8" hsqldbVersion = "2.4.1" @@ -142,7 +142,7 @@ configure(allprojects) { project -> } checkstyle { - toolVersion = "8.26" + toolVersion = "8.27" configDir = rootProject.file("src/checkstyle") } From 83683a13bbfbd6e9315605cc8e61b7ffb48abbf8 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 29 Nov 2019 22:26:52 +0100 Subject: [PATCH 064/322] Allow ExchangeStrategies customizations in WebClient Backport of d4209392 and acfeb77d Closes gh-23961 --- .../server/DefaultWebTestClientBuilder.java | 8 +- .../web/reactive/server/WebTestClient.java | 21 ++++- .../http/codec/ClientCodecConfigurer.java | 8 +- .../http/codec/CodecConfigurer.java | 9 ++ .../http/codec/ServerCodecConfigurer.java | 6 ++ .../codec/support/BaseCodecConfigurer.java | 44 +++++++++- .../http/codec/support/BaseDefaultCodecs.java | 18 ++++ .../support/ClientDefaultCodecsImpl.java | 32 ++++++- .../support/DefaultClientCodecConfigurer.java | 18 +++- .../support/DefaultServerCodecConfigurer.java | 17 +++- .../support/ServerDefaultCodecsImpl.java | 10 +++ .../support/ClientCodecConfigurerTests.java | 48 ++++++++++- .../codec/support/CodecConfigurerTests.java | 84 ++++++++++++++++++- .../support/ServerCodecConfigurerTests.java | 50 ++++++++++- .../DefaultExchangeStrategiesBuilder.java | 26 ++++-- .../client/DefaultWebClientBuilder.java | 66 ++++++++++++--- .../function/client/ExchangeStrategies.java | 9 ++ .../reactive/function/client/WebClient.java | 17 +++- .../client/ExchangeStrategiesTests.java | 12 +++ src/docs/asciidoc/web/webflux-webclient.adoc | 38 +++++++-- src/docs/asciidoc/web/webflux.adoc | 3 +- 21 files changed, 498 insertions(+), 46 deletions(-) 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 f314b4455da..82006d37b25 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 cc2e8ab96fc..e8b109cbc01 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 db31e97218a..e41ec734811 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 3d4c625b5d5..d16a0a9090f 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 59a209ac59a..ba8501b3b29 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 e86ac954f9f..505d19d0e28 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 f3034ad9354..9be10da0951 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 9f578b7320a..e764cb96961 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 9875ded1b98..737282eecd5 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 2623d5a7f7b..661d45d6669 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 37e924cd7e9..1d997c3777b 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 afa5f4cec37..1b98450036e 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 48e20a9074d..eb4a3a0d458 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 b36cdd0ca7d..6373ff6b515 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 aa1523d9ace..e5703203fc5 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 c8796e91f78..44c3164b701 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 804fbd9a42f..acf32d0959a 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 8dc2a17c012..f60a822597c 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 b08662c8fb9..b25bfe9dd4b 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 20980df9dd8..0bc6fdce298 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 5789f295273..172a48b8cd4 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 From a21df0cc6d0384e8bb433e2a8f207178f56c7596 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 2 Dec 2019 22:52:55 +0100 Subject: [PATCH 065/322] Provide default codecs config callback to custom codecs As a follow-up of gh-23961, this change provides a way for custom codecs to align with the default codecs' behavior on common features like buffer size limits and logging request details. Closes gh-24119 Co-authored-by: Rossen Stoyanchev --- .../http/codec/CodecConfigurer.java | 34 +++++++++++++++++++ .../codec/support/BaseCodecConfigurer.java | 17 ++++++++++ .../http/codec/support/BaseDefaultCodecs.java | 8 +++-- .../codec/support/CodecConfigurerTests.java | 13 +++++++ src/docs/asciidoc/web/webflux.adoc | 34 +++++++++++++++++-- 5 files changed, 100 insertions(+), 6 deletions(-) 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 d16a0a9090f..518d3fac7e1 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 @@ -17,9 +17,11 @@ package org.springframework.http.codec; import java.util.List; +import java.util.function.Consumer; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; +import org.springframework.lang.Nullable; /** * Defines a common interface for configuring either client or server HTTP @@ -213,6 +215,38 @@ interface CustomCodecs { * @param writer the writer to add */ void writer(HttpMessageWriter writer); + + /** + * Register a callback for the {@link DefaultCodecConfig configuration} + * applied to default codecs. This allows custom codecs to follow general + * guidelines applied to default ones, such as logging details and limiting + * the amount of buffered data. + * @param codecsConfigConsumer the default codecs configuration callback + * @since 5.1.12 + */ + void withDefaultCodecConfig(Consumer codecsConfigConsumer); + } + + + /** + * Common options applied to default codecs and passed in a callback to custom codecs + * so they get a chance to align their behavior on the default ones. + * @since 5.1.12 + */ + interface DefaultCodecConfig { + + /** + * Get the configured limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. + */ + @Nullable + Integer maxInMemorySize(); + + /** + * Whether to log form data at DEBUG level, and headers at TRACE level. + * Both may contain sensitive information. + */ + boolean isEnableLoggingRequestDetails(); } } 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 505d19d0e28..a63e0be8882 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 @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; @@ -39,6 +40,8 @@ */ abstract class BaseCodecConfigurer implements CodecConfigurer { + protected boolean customCodecsInitialized; + protected final BaseDefaultCodecs defaultCodecs; protected final DefaultCustomCodecs customCodecs; @@ -88,6 +91,7 @@ public CustomCodecs customCodecs() { @Override public List> getReaders() { + initializeCustomCodecs(); List> result = new ArrayList<>(); result.addAll(this.defaultCodecs.getTypedReaders()); @@ -113,6 +117,7 @@ public List> getWriters() { * same except for the multipart writer itself. */ protected List> getWritersInternal(boolean forMultipart) { + initializeCustomCodecs(); List> result = new ArrayList<>(); result.addAll(this.defaultCodecs.getTypedWriters(forMultipart)); @@ -128,6 +133,13 @@ protected List> getWritersInternal(boolean forMultipart) { @Override public abstract CodecConfigurer clone(); + private void initializeCustomCodecs() { + if(!this.customCodecsInitialized) { + this.customCodecs.configConsumers.forEach(consumer -> consumer.accept(this.defaultCodecs)); + this.customCodecsInitialized = true; + } + } + /** * Default implementation of {@code CustomCodecs}. @@ -142,6 +154,7 @@ protected static final class DefaultCustomCodecs implements CustomCodecs { private final List> objectWriters = new ArrayList<>(); + private final List> configConsumers = new ArrayList<>(); DefaultCustomCodecs() { } @@ -179,6 +192,10 @@ public void writer(HttpMessageWriter writer) { (canWriteObject ? this.objectWriters : this.typedWriters).add(writer); } + @Override + public void withDefaultCodecConfig(Consumer codecsConfigConsumer) { + this.configConsumers.add(codecsConfigConsumer); + } // Package private accessors... 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 9be10da0951..75ec9191dcb 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 @@ -59,7 +59,7 @@ * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { +class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig { static final boolean jackson2Present; @@ -158,8 +158,9 @@ public void maxInMemorySize(int byteCount) { this.maxInMemorySize = byteCount; } + @Override @Nullable - protected Integer maxInMemorySize() { + public Integer maxInMemorySize() { return this.maxInMemorySize; } @@ -168,7 +169,8 @@ public void enableLoggingRequestDetails(boolean enable) { this.enableLoggingRequestDetails = enable; } - protected boolean isEnableLoggingRequestDetails() { + @Override + public boolean isEnableLoggingRequestDetails() { return this.enableLoggingRequestDetails; } 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 eb4a3a0d458..02f41655f46 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 @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -336,6 +337,18 @@ public void cloneDefaultCodecs() { assertFalse(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder))); } + @Test + public void withDefaultCodecConfig() { + AtomicBoolean callbackCalled = new AtomicBoolean(false); + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + this.configurer.customCodecs().withDefaultCodecConfig(config -> { + assertTrue(config.isEnableLoggingRequestDetails()); + callbackCalled.compareAndSet(false, true); + }); + this.configurer.getReaders(); + assertTrue(callbackCalled.get()); + } + private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 172a48b8cd4..a43d12b0309 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -883,16 +883,44 @@ The following example shows how to do so for client-side requests: [subs="verbatim,quotes"] ---- Consumer consumer = configurer -> - configurer.defaultCodecs().enableLoggingRequestDetails(true); + configurer.defaultCodecs().enableLoggingRequestDetails(true); WebClient webClient = WebClient.builder() - .exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build()) - .build(); + .exchangeStrategies(strategies -> strategies.codecs(consumer)) + .build(); ---- ==== +[[webflux-codecs-custom]] +==== Custom codecs + +Applications can register custom codecs for supporting additional media types, +or specific behaviors that are not supported by the default codecs. + +Some configuration options expressed by developers are enforced on default codecs. +Custom codecs might want to get a chance to align with those preferences, +like <> +or <>. +The following example shows how to do so for client-side requests: + +==== +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + Consumer consumer = configurer -> { + CustomDecoder customDecoder = new CustomDecoder(); + configurer.customCodecs().decoder(customDecoder); + configurer.customCodecs().withDefaultCodecConfig(config -> + customDecoder.maxInMemorySize(config.maxInMemorySize()) + ); + } + WebClient webClient = WebClient.builder() + .exchangeStrategies(strategies -> strategies.codecs(consumer)) + .build(); +---- +==== [[webflux-dispatcher-handler]] == `DispatcherHandler` From 7203da7133106bd34112b5f7699d07dbf64a9758 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Tue, 3 Dec 2019 08:06:03 +0000 Subject: [PATCH 066/322] Next Development Version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 52cd8c3529f..47ad59cda14 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.1.12.BUILD-SNAPSHOT +version=5.1.13.BUILD-SNAPSHOT From f049a6ec16c758c2a2f6ead45ab212873cb9e506 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 3 Dec 2019 17:12:14 +0100 Subject: [PATCH 067/322] Add integration test for gh-24110 --- .../AnnotatedElementUtilsTests.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 0ca70fac741..ea719a1e2e5 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -488,6 +488,20 @@ public void getMergedAnnotationWithImplicitAliasesInMetaAnnotationOnComposedAnno assertTrue(isAnnotated(element, name)); } + @Test + public void getMergedAnnotationWithImplicitAliasesWithDefaultsInMetaAnnotationOnComposedAnnotation() { + Class element = ImplicitAliasesWithDefaultsClass.class; + String name = AliasesWithDefaults.class.getName(); + AliasesWithDefaults annotation = getMergedAnnotation(element, AliasesWithDefaults.class); + + assertNotNull("Should find @AliasesWithDefaults on " + element.getSimpleName(), annotation); + assertEquals("a1", "ImplicitAliasesWithDefaults", annotation.a1()); + assertEquals("a2", "ImplicitAliasesWithDefaults", annotation.a2()); + + // Verify contracts between utility methods: + assertTrue(isAnnotated(element, name)); + } + @Test public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { Class element = InvalidConventionBasedComposedContextConfigClass.class; @@ -958,7 +972,6 @@ static class MetaCycleAnnotatedClass { String[] xmlConfigFiles() default {}; } - @ContextConfig @Retention(RetentionPolicy.RUNTIME) @interface AliasedComposedContextConfig { @@ -999,6 +1012,27 @@ static class MetaCycleAnnotatedClass { @interface ComposedImplicitAliasesContextConfig { } + @Retention(RetentionPolicy.RUNTIME) + @interface AliasesWithDefaults { + + @AliasFor("a2") + String a1() default "AliasesWithDefaults"; + + @AliasFor("a1") + String a2() default "AliasesWithDefaults"; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasesWithDefaults + @interface ImplicitAliasesWithDefaults { + + @AliasFor(annotation = AliasesWithDefaults.class, attribute = "a1") + String b1() default "ImplicitAliasesWithDefaults"; + + @AliasFor(annotation = AliasesWithDefaults.class, attribute = "a2") + String b2() default "ImplicitAliasesWithDefaults"; + } + @ImplicitAliasesContextConfig @Retention(RetentionPolicy.RUNTIME) @interface TransitiveImplicitAliasesContextConfig { @@ -1296,6 +1330,10 @@ static class ImplicitAliasesContextConfigClass2 { static class ImplicitAliasesContextConfigClass3 { } + @ImplicitAliasesWithDefaults + static class ImplicitAliasesWithDefaultsClass { + } + @TransitiveImplicitAliasesContextConfig(groovy = "test.groovy") static class TransitiveImplicitAliasesContextConfigClass { } From 5341e777960379b6858b26eb28892c7b485676fc Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 7 Dec 2019 19:02:06 +0100 Subject: [PATCH 068/322] Introduce regression tests for prototype/request-scoped @ControllerAdvice See gh-24157 --- .../spr/ControllerAdviceIntegrationTests.java | 97 ++++++++++++++++--- 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java index 982e5e13642..144639e29d7 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.test.web.servlet.samples.spr; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; @@ -25,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Controller; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -33,6 +35,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.annotation.RequestScope; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -64,21 +67,53 @@ public class ControllerAdviceIntegrationTests { @Before public void setUpMockMvc() { this.mockMvc = webAppContextSetup(wac).build(); + resetCounters(); } @Test public void controllerAdviceIsAppliedOnlyOnce() throws Exception { - assertEquals(0, SingletonControllerAdvice.counter.get()); - assertEquals(0, RequestScopedControllerAdvice.counter.get()); + this.mockMvc.perform(get("/test").param("requestParam", "foo"))// + .andExpect(status().isOk())// + .andExpect(forwardedUrl("singleton:1;prototype:1;request-scoped:1;requestParam:foo")); + + assertEquals(1, SingletonControllerAdvice.invocationCount.get()); + assertEquals(1, PrototypeControllerAdvice.invocationCount.get()); + assertEquals(1, RequestScopedControllerAdvice.invocationCount.get()); + } + + @Test + public void prototypeAndRequestScopedControllerAdviceBeansAreNotCached() throws Exception { + this.mockMvc.perform(get("/test").param("requestParam", "foo"))// + .andExpect(status().isOk())// + .andExpect(forwardedUrl("singleton:1;prototype:1;request-scoped:1;requestParam:foo")); + + // singleton @ControllerAdvice beans should not be instantiated again. + assertEquals(0, SingletonControllerAdvice.instanceCount.get()); + // prototype and request-scoped @ControllerAdvice beans should be instantiated once per request. + assertEquals(1, PrototypeControllerAdvice.instanceCount.get()); + assertEquals(1, RequestScopedControllerAdvice.instanceCount.get()); - this.mockMvc.perform(get("/test"))// + this.mockMvc.perform(get("/test").param("requestParam", "bar"))// .andExpect(status().isOk())// - .andExpect(forwardedUrl("singleton:1;request-scoped:1")); + .andExpect(forwardedUrl("singleton:2;prototype:2;request-scoped:2;requestParam:bar")); - assertEquals(1, SingletonControllerAdvice.counter.get()); - assertEquals(1, RequestScopedControllerAdvice.counter.get()); + // singleton @ControllerAdvice beans should not be instantiated again. + assertEquals(0, SingletonControllerAdvice.instanceCount.get()); + // prototype and request-scoped @ControllerAdvice beans should be instantiated once per request. + assertEquals(2, PrototypeControllerAdvice.instanceCount.get()); + assertEquals(2, RequestScopedControllerAdvice.instanceCount.get()); } + private static void resetCounters() { + SingletonControllerAdvice.invocationCount.set(0); + SingletonControllerAdvice.instanceCount.set(0); + PrototypeControllerAdvice.invocationCount.set(0); + PrototypeControllerAdvice.instanceCount.set(0); + RequestScopedControllerAdvice.invocationCount.set(0); + RequestScopedControllerAdvice.instanceCount.set(0); + } + + @Configuration @EnableWebMvc static class Config { @@ -93,6 +128,12 @@ SingletonControllerAdvice singletonControllerAdvice() { return new SingletonControllerAdvice(); } + @Bean + @Scope("prototype") + PrototypeControllerAdvice prototypeControllerAdvice() { + return new PrototypeControllerAdvice(); + } + @Bean @RequestScope RequestScopedControllerAdvice requestScopedControllerAdvice() { @@ -103,22 +144,49 @@ RequestScopedControllerAdvice requestScopedControllerAdvice() { @ControllerAdvice static class SingletonControllerAdvice { - static final AtomicInteger counter = new AtomicInteger(); + static final AtomicInteger instanceCount = new AtomicInteger(); + static final AtomicInteger invocationCount = new AtomicInteger(); + + { + instanceCount.incrementAndGet(); + } @ModelAttribute void initModel(Model model) { - model.addAttribute("singleton", counter.incrementAndGet()); + model.addAttribute("singleton", invocationCount.incrementAndGet()); } } @ControllerAdvice - static class RequestScopedControllerAdvice { + static class PrototypeControllerAdvice { - static final AtomicInteger counter = new AtomicInteger(); + static final AtomicInteger instanceCount = new AtomicInteger(); + static final AtomicInteger invocationCount = new AtomicInteger(); + + { + instanceCount.incrementAndGet(); + } @ModelAttribute void initModel(Model model) { - model.addAttribute("request-scoped", counter.incrementAndGet()); + model.addAttribute("prototype", invocationCount.incrementAndGet()); + } + } + + @ControllerAdvice + static class RequestScopedControllerAdvice { + + static final AtomicInteger instanceCount = new AtomicInteger(); + static final AtomicInteger invocationCount = new AtomicInteger(); + + { + instanceCount.incrementAndGet(); + } + + @ModelAttribute + void initModel(@RequestParam String requestParam, Model model) { + model.addAttribute("requestParam", requestParam); + model.addAttribute("request-scoped", invocationCount.incrementAndGet()); } } @@ -127,8 +195,11 @@ static class TestController { @GetMapping("/test") String get(Model model) { - return "singleton:" + model.asMap().get("singleton") + ";request-scoped:" - + model.asMap().get("request-scoped"); + Map map = model.asMap(); + return "singleton:" + map.get("singleton") + + ";prototype:" + map.get("prototype") + + ";request-scoped:" + map.get("request-scoped") + + ";requestParam:" + map.get("requestParam"); } } From 197dbffe20c85b2feb1ae7eb83babe1cae7c8440 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 9 Dec 2019 13:28:39 +0100 Subject: [PATCH 069/322] Support variable resolution of wildcard types Includes cleanup of "varaible" typos in ResolvableTypeTests. Closes gh-24150 --- .../springframework/core/ResolvableType.java | 14 ++++++--- .../core/ResolvableTypeTests.java | 29 ++++++++----------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 469e934f131..e99cf3b228e 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -872,6 +872,12 @@ private ResolvableType resolveVariable(TypeVariable variable) { return forType(ownerType, this.variableResolver).resolveVariable(variable); } } + if (this.type instanceof WildcardType) { + ResolvableType resolved = resolveType().resolveVariable(variable); + if (resolved != null) { + return resolved; + } + } if (this.variableResolver != null) { return this.variableResolver.resolveVariable(variable); } @@ -997,7 +1003,7 @@ public static ResolvableType forClass(@Nullable Class clazz) { * {@link Class#isAssignableFrom}, which this serves as a wrapper for. * For example: {@code ResolvableType.forRawClass(List.class)}. * @param clazz the class to introspect ({@code null} is semantically - * equivalent to {@code Object.class} for typical use cases here} + * equivalent to {@code Object.class} for typical use cases here) * @return a {@link ResolvableType} for the specified class * @since 4.2 * @see #forClass(Class) @@ -1485,10 +1491,10 @@ public TypeVariablesVariableResolver(TypeVariable[] variables, ResolvableType @Override @Nullable public ResolvableType resolveVariable(TypeVariable variable) { + TypeVariable variableToCompare = SerializableTypeWrapper.unwrap(variable); for (int i = 0; i < this.variables.length; i++) { - TypeVariable v1 = SerializableTypeWrapper.unwrap(this.variables[i]); - TypeVariable v2 = SerializableTypeWrapper.unwrap(variable); - if (ObjectUtils.nullSafeEquals(v1, v2)) { + TypeVariable resolvedVariable = SerializableTypeWrapper.unwrap(this.variables[i]); + if (ObjectUtils.nullSafeEquals(resolvedVariable, variableToCompare)) { return this.generics[i]; } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 58421740df0..330f23a6cff 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -684,38 +684,31 @@ public void doesResolveFromOuterOwner() throws Exception { @Test public void resolveBoundedTypeVariableResult() throws Exception { - ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVaraibleResult")); + ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableResult")); assertThat(type.resolve(), equalTo((Class) CharSequence.class)); } @Test - public void resolveVariableNotFound() throws Exception { - ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn")); - assertThat(type.resolve(), nullValue()); + public void resolveBoundedTypeVariableWildcardResult() throws Exception { + ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableWildcardResult")); + assertThat(type.getGeneric(1).asCollection().resolveGeneric(), equalTo((Class) CharSequence.class)); } @Test - public void resolveTypeVaraibleFromMethodReturn() throws Exception { + public void resolveVariableNotFound() throws Exception { ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn")); assertThat(type.resolve(), nullValue()); } @Test - public void resolveTypeVaraibleFromMethodReturnWithInstanceClass() throws Exception { - ResolvableType type = ResolvableType.forMethodReturnType( - Methods.class.getMethod("typedReturn"), TypedMethods.class); - assertThat(type.resolve(), equalTo((Class) String.class)); - } - - @Test - public void resolveTypeVaraibleFromSimpleInterfaceType() { + public void resolveTypeVariableFromSimpleInterfaceType() { ResolvableType type = ResolvableType.forClass( MySimpleInterfaceType.class).as(MyInterfaceType.class); assertThat(type.resolveGeneric(), equalTo((Class) String.class)); } @Test - public void resolveTypeVaraibleFromSimpleCollectionInterfaceType() { + public void resolveTypeVariableFromSimpleCollectionInterfaceType() { ResolvableType type = ResolvableType.forClass( MyCollectionInterfaceType.class).as(MyInterfaceType.class); assertThat(type.resolveGeneric(), equalTo((Class) Collection.class)); @@ -723,14 +716,14 @@ public void resolveTypeVaraibleFromSimpleCollectionInterfaceType() { } @Test - public void resolveTypeVaraibleFromSimpleSuperclassType() { + public void resolveTypeVariableFromSimpleSuperclassType() { ResolvableType type = ResolvableType.forClass( MySimpleSuperclassType.class).as(MySuperclassType.class); assertThat(type.resolveGeneric(), equalTo((Class) String.class)); } @Test - public void resolveTypeVaraibleFromSimpleCollectionSuperclassType() { + public void resolveTypeVariableFromSimpleCollectionSuperclassType() { ResolvableType type = ResolvableType.forClass( MyCollectionSuperclassType.class).as(MySuperclassType.class); assertThat(type.resolveGeneric(), equalTo((Class) Collection.class)); @@ -1459,7 +1452,9 @@ interface Methods { void charSequenceParameter(List cs); - R boundedTypeVaraibleResult(); + R boundedTypeVariableResult(); + + Map> boundedTypeVariableWildcardResult(); void nested(Map, Map> p); From 5dbd3b0bbfdd66b8f2f478298d7732da32338597 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 9 Dec 2019 13:56:50 +0100 Subject: [PATCH 070/322] Avoid ByteArrayOutputStream for source values without the need to be encoded Closes gh-24154 --- .../web/util/HierarchicalUriComponents.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 96a7f8a1f83..22f13c5e9ce 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -335,8 +335,21 @@ static String encodeUriComponent(String source, Charset charset, Type type) { Assert.notNull(type, "Type must not be null"); byte[] bytes = source.getBytes(charset); + boolean original = true; + for (byte b : bytes) { + if (b < 0) { + b += 256; + } + if (!type.isAllowed(b)) { + original = false; + break; + } + } + if (original) { + return source; + } + ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length); - boolean changed = false; for (byte b : bytes) { if (b < 0) { b += 256; @@ -350,10 +363,9 @@ static String encodeUriComponent(String source, Charset charset, Type type) { char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); bos.write(hex1); bos.write(hex2); - changed = true; } } - return (changed ? new String(bos.toByteArray(), charset) : source); + return new String(bos.toByteArray(), charset); } private Type getHostType() { @@ -416,7 +428,6 @@ else if (!type.isAllowed(ch)) { @Override protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) { - Assert.state(!this.encodeState.equals(EncodeState.FULLY_ENCODED), "URI components already encoded, and could not possibly contain '{' or '}'."); From a368040fd6acc8858100bfe90c15655b2ec223ce Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 9 Dec 2019 15:54:40 +0100 Subject: [PATCH 071/322] Polishing --- build.gradle | 2 +- .../src/main/java/org/springframework/core/ResolvableType.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 974b07de3de..9baaf5604cd 100644 --- a/build.gradle +++ b/build.gradle @@ -184,7 +184,7 @@ configure(allprojects) { project -> "https://tiles.apache.org/tiles-request/apidocs/", "https://tiles.apache.org/framework/apidocs/", "https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/", - "https://www.ehcache.org/apidocs/2.10.4", + "https://www.ehcache.org/apidocs/2.10.4/", "https://www.quartz-scheduler.org/api/2.3.0/", "https://fasterxml.github.io/jackson-core/javadoc/2.9/", "https://fasterxml.github.io/jackson-databind/javadoc/2.9/", diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index e99cf3b228e..7680e57b0c1 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -988,7 +988,7 @@ public String toString() { * using the full generic type information for assignability checks. * For example: {@code ResolvableType.forClass(MyArrayList.class)}. * @param clazz the class to introspect ({@code null} is semantically - * equivalent to {@code Object.class} for typical use cases here} + * equivalent to {@code Object.class} for typical use cases here) * @return a {@link ResolvableType} for the specified class * @see #forClass(Class, Class) * @see #forClassWithGenerics(Class, Class...) From da4e2710b48bd1759563c313194fc772f9cd62c8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 Dec 2019 00:38:21 +0100 Subject: [PATCH 072/322] Upgrade to Joda-Time 2.10.5 and Commons Pool 2.6.2 --- spring-aop/spring-aop.gradle | 2 +- spring-context/spring-context.gradle | 4 ++-- spring-webmvc/spring-webmvc.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle index 6911a3b5f20..cc1fea1f2af 100644 --- a/spring-aop/spring-aop.gradle +++ b/spring-aop/spring-aop.gradle @@ -4,6 +4,6 @@ dependencies { compile(project(":spring-beans")) compile(project(":spring-core")) optional("org.aspectj:aspectjweaver:${aspectjVersion}") - optional("org.apache.commons:commons-pool2:2.6.0") + optional("org.apache.commons:commons-pool2:2.6.2") optional("com.jamonapi:jamon:2.81") } diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index c9ae1662c3a..0ab89e6fc06 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -19,14 +19,14 @@ dependencies { optional("org.aspectj:aspectjweaver:${aspectjVersion}") optional("org.codehaus.groovy:groovy:${groovyVersion}") optional("org.beanshell:bsh:2.0b5") - optional("joda-time:joda-time:2.10.4") + optional("joda-time:joda-time:2.10.5") optional("org.hibernate:hibernate-validator:5.4.3.Final") optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") testCompile("org.codehaus.groovy:groovy-jsr223:${groovyVersion}") testCompile("org.codehaus.groovy:groovy-test:${groovyVersion}") testCompile("org.codehaus.groovy:groovy-xml:${groovyVersion}") - testCompile("org.apache.commons:commons-pool2:2.6.0") + testCompile("org.apache.commons:commons-pool2:2.6.2") testCompile("javax.inject:javax.inject-tck:1") testCompile("org.awaitility:awaitility:3.1.6") testRuntime("javax.xml.bind:jaxb-api:2.3.1") diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 6da9b01d6e1..690baf66551 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -55,7 +55,7 @@ dependencies { } testCompile("commons-fileupload:commons-fileupload:1.4") testCompile("commons-io:commons-io:2.5") - testCompile("joda-time:joda-time:2.10.4") + testCompile("joda-time:joda-time:2.10.5") testCompile("org.mozilla:rhino:1.7.10") testCompile("dom4j:dom4j:1.6.1") { exclude group: "xml-apis", module: "xml-apis" From 015f7d8ce11cdcf6aaed6d8aef30d6b8a9de68b2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2019 17:13:02 +0100 Subject: [PATCH 073/322] Polishing --- .../beans/CachedIntrospectionResults.java | 9 ++-- .../support/AbstractBeanDefinition.java | 48 +++++++++---------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index ca14d6995d2..5b0af83467f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.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. @@ -43,9 +43,9 @@ * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor} * information for a Java class. Not intended for direct use by application code. * - *

Necessary for own caching of descriptors within the application's - * ClassLoader, rather than rely on the JDK's system-wide BeanInfo cache - * (in order to avoid leaks on ClassLoader shutdown). + *

Necessary for Spring's own caching of bean descriptors within the application + * {@link ClassLoader}, rather than relying on the JDK's system-wide {@link BeanInfo} + * cache (in order to avoid leaks on individual application shutdown in a shared JVM). * *

Information is cached statically, so we don't need to create new * objects of this class for every JavaBean we manipulate. Hence, this class @@ -163,7 +163,6 @@ public static void clearClassLoader(@Nullable ClassLoader classLoader) { * @return the corresponding CachedIntrospectionResults * @throws BeansException in case of introspection failure */ - @SuppressWarnings("unchecked") static CachedIntrospectionResults forClass(Class beanClass) throws BeansException { CachedIntrospectionResults results = strongClassCache.get(beanClass); if (results != null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 4143b40dd18..511d374355d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -1127,30 +1127,30 @@ public boolean equals(Object other) { return false; } AbstractBeanDefinition that = (AbstractBeanDefinition) other; - boolean rtn = ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()); - rtn = rtn && ObjectUtils.nullSafeEquals(this.scope, that.scope); - rtn = rtn && this.abstractFlag == that.abstractFlag; - rtn = rtn && this.lazyInit == that.lazyInit; - rtn = rtn && this.autowireMode == that.autowireMode; - rtn = rtn && this.dependencyCheck == that.dependencyCheck; - rtn = rtn && Arrays.equals(this.dependsOn, that.dependsOn); - rtn = rtn && this.autowireCandidate == that.autowireCandidate; - rtn = rtn && ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers); - rtn = rtn && this.primary == that.primary; - rtn = rtn && this.nonPublicAccessAllowed == that.nonPublicAccessAllowed; - rtn = rtn && this.lenientConstructorResolution == that.lenientConstructorResolution; - rtn = rtn && ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues); - rtn = rtn && ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues); - rtn = rtn && ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides); - rtn = rtn && ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName); - rtn = rtn && ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName); - rtn = rtn && ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName); - rtn = rtn && this.enforceInitMethod == that.enforceInitMethod; - rtn = rtn && ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName); - rtn = rtn && this.enforceDestroyMethod == that.enforceDestroyMethod; - rtn = rtn && this.synthetic == that.synthetic; - rtn = rtn && this.role == that.role; - return rtn && super.equals(other); + return (ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()) && + ObjectUtils.nullSafeEquals(this.scope, that.scope) && + this.abstractFlag == that.abstractFlag && + this.lazyInit == that.lazyInit && + this.autowireMode == that.autowireMode && + this.dependencyCheck == that.dependencyCheck && + Arrays.equals(this.dependsOn, that.dependsOn) && + this.autowireCandidate == that.autowireCandidate && + ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers) && + this.primary == that.primary && + this.nonPublicAccessAllowed == that.nonPublicAccessAllowed && + this.lenientConstructorResolution == that.lenientConstructorResolution && + ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues) && + ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues) && + ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides) && + ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName) && + ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName) && + ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName) && + this.enforceInitMethod == that.enforceInitMethod && + ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName) && + this.enforceDestroyMethod == that.enforceDestroyMethod && + this.synthetic == that.synthetic && + this.role == that.role && + super.equals(other)); } @Override From 97cad6ca8e4b95e1c6afa68ff282d048f2395389 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2019 16:48:11 +0100 Subject: [PATCH 074/322] Upgrade to Tomcat 9.0.30 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9baaf5604cd..6c7ff94c10c 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ ext { rxjava2Version = "2.2.15" slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" - tomcatVersion = "9.0.29" + tomcatVersion = "9.0.30" undertowVersion = "2.0.28.Final" gradleScriptDir = "${rootProject.projectDir}/gradle" From 32a8b9b25a422b699f1f8bf4cffd6b961f88e503 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2019 16:48:25 +0100 Subject: [PATCH 075/322] Polishing --- .../jdbc/core/SingleColumnRowMapper.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index 31f70680467..0a2e9cb491c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.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. @@ -220,6 +220,7 @@ else if (this.conversionService != null && this.conversionService.canConvert(val * (with the required type specified only once). * @param requiredType the type that each result object is expected to match * @since 4.1 + * @see #newInstance(Class, ConversionService) */ public static SingleColumnRowMapper newInstance(Class requiredType) { return new SingleColumnRowMapper<>(requiredType); @@ -229,10 +230,15 @@ public static SingleColumnRowMapper newInstance(Class requiredType) { * Static factory method to create a new {@code SingleColumnRowMapper} * (with the required type specified only once). * @param requiredType the type that each result object is expected to match - * @param conversionService the {@link ConversionService} for converting a fetched value + * @param conversionService the {@link ConversionService} for converting a + * fetched value, or {@code null} for none * @since 5.0.4 + * @see #newInstance(Class) + * @see #setConversionService */ - public static SingleColumnRowMapper newInstance(Class requiredType, @Nullable ConversionService conversionService) { + public static SingleColumnRowMapper newInstance( + Class requiredType, @Nullable ConversionService conversionService) { + SingleColumnRowMapper rowMapper = newInstance(requiredType); rowMapper.setConversionService(conversionService); return rowMapper; From 611bb0b92e7c4ee37ac23fc6b6bd407e09f27a7f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2019 17:01:18 +0100 Subject: [PATCH 076/322] Explicit test dependency on javax.annotation.Priority --- spring-core/spring-core.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index aaf884b1bf2..8616f8b5c74 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -80,6 +80,7 @@ dependencies { optional("io.netty:netty-buffer") testCompile("io.projectreactor:reactor-test") testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") + testCompile("javax.annotation:javax.annotation-api:1.3.2") testCompile("com.google.code.findbugs:jsr305:3.0.2") testCompile("org.xmlunit:xmlunit-matchers:2.6.2") testCompile("javax.xml.bind:jaxb-api:2.3.1") From 3fbe76283288c1dbeda2e1e579b3c3f9a9971c78 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2019 17:12:27 +0100 Subject: [PATCH 077/322] Consistent use of annotation-api dependency instead of tomcat-embed-core --- spring-beans/spring-beans.gradle | 2 +- spring-core/spring-core.gradle | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle index b475530b766..09b03d0ba92 100644 --- a/spring-beans/spring-beans.gradle +++ b/spring-beans/spring-beans.gradle @@ -9,7 +9,7 @@ dependencies { optional("org.codehaus.groovy:groovy-xml:${groovyVersion}") optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") - testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") + testCompile("javax.annotation:javax.annotation-api:1.3.2") } // This module does joint compilation for Java and Groovy code with the compileGroovy task. diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index 8616f8b5c74..98290e2f64c 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -79,7 +79,6 @@ dependencies { optional("io.reactivex.rxjava2:rxjava:${rxjava2Version}") optional("io.netty:netty-buffer") testCompile("io.projectreactor:reactor-test") - testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") testCompile("javax.annotation:javax.annotation-api:1.3.2") testCompile("com.google.code.findbugs:jsr305:3.0.2") testCompile("org.xmlunit:xmlunit-matchers:2.6.2") From 2576aa40631b15144ba6c3169bf4a33f612373a4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Dec 2019 05:46:09 +0000 Subject: [PATCH 078/322] ContentDisposition trims charset in filename Backport of c8bce9686f3dca2013aee875b3c86d87aa897662 Closes gh-24112 --- .../org/springframework/http/ContentDisposition.java | 4 ++-- .../springframework/http/ContentDispositionTests.java | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index b0b13aa205d..93e3bd4cb81 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -277,7 +277,7 @@ public static ContentDisposition parse(String contentDisposition) { } else if (attribute.equals("filename*") ) { filename = decodeHeaderFieldParam(value); - charset = Charset.forName(value.substring(0, value.indexOf('\''))); + charset = Charset.forName(value.substring(0, value.indexOf('\'')).trim()); Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Charset should be UTF-8 or ISO-8859-1"); } @@ -371,7 +371,7 @@ private static String decodeHeaderFieldParam(String input) { if (firstQuoteIndex == -1 || secondQuoteIndex == -1) { return input; } - Charset charset = Charset.forName(input.substring(0, firstQuoteIndex)); + Charset charset = Charset.forName(input.substring(0, firstQuoteIndex).trim()); Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Charset should be UTF-8 or ISO-8859-1"); byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset); diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index fd0ec56572b..f195358f63c 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -71,6 +71,14 @@ public void parseAndIgnoreEmptyParts() { .name("foo").filename("foo.txt").size(123L).build(), disposition); } + @Test // gh-24112 + public void parseEncodedFilenameWithPaddedCharset() { + ContentDisposition disposition = ContentDisposition + .parse("attachment; filename*= UTF-8''some-file.zip"); + assertEquals(ContentDisposition.builder("attachment") + .filename("some-file.zip", StandardCharsets.UTF_8).build(), disposition); + } + @Test public void parseEncodedFilename() { ContentDisposition disposition = ContentDisposition From 59e475556200ff00cfa20d15ef9151e317b4b294 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Dec 2019 05:54:22 +0000 Subject: [PATCH 079/322] Correct WebFlux docs on BindingResult with @RequestBody Backport of 70a0c93d6916efa60a338906277ab9b543acffb1 Closes gh-22997 --- src/docs/asciidoc/web/webflux.adoc | 42 ++++++++++++------------------ 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index a43d12b0309..3b0e41a055b 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -1661,9 +1661,8 @@ and others) and is equivalent to `required=false`. See "`Any other argument`" later in this table. | `Errors`, `BindingResult` -| For access to errors from validation and data binding for a command object - (that is, a `@ModelAttribute` argument) or errors from the validation of a `@RequestBody` or - `@RequestPart` argument. An `Errors`, or `BindingResult` argument must be declared +| For access to errors from validation and data binding for a command object, i.e. a + `@ModelAttribute` argument. An `Errors`, or `BindingResult` argument must be declared immediately after the validated method argument. | `SessionStatus` + class-level `@SessionAttributes` @@ -2318,24 +2317,19 @@ you can declare a concrete target `Object`, instead of `Part`, as the following <1> Using `@RequestPart` to get the metadata. ==== -You can use `@RequestPart` combination with `javax.validation.Valid` or Spring's -`@Validated` annotation, which causes Standard Bean Validation to be applied. -By default, validation errors cause a `WebExchangeBindException`, which is turned -into a 400 (`BAD_REQUEST`) response. Alternatively, you can handle validation errors locally -within the controller through an `Errors` or `BindingResult` argument, as the following example shows: +You can use `@RequestPart` in combination with `javax.validation.Valid` or Spring's +`@Validated` annotation, which causes Standard Bean Validation to be applied. Validation +errors lead to a `WebExchangeBindException` that results in a 400 (BAD_REQUEST) response. +The exception contains a `BindingResult` with the error details and can also be handled +in the controller method by declaring the argument with an async wrapper and then using +error related operators: ==== [source,java,indent=0] [subs="verbatim,quotes"] ---- -@PostMapping("/") -public String handle(@Valid @RequestPart("meta-data") MetaData metadata, <1> - BindingResult result) { <2> - // ... -} ----- -<1> Using a `@Valid` annotation. -<2> Using a `BindingResult` argument. + public String handle(@Valid @RequestPart("meta-data") Mono metadata) { + // use one of the onError* operators... ==== To access all multipart data as a `MultiValueMap`, you can use `@RequestBody`, @@ -2407,20 +2401,18 @@ You can use the <> option of the < account) { + // use one of the onError* operators... ---- ==== From f5b43a264aa10eb98c14d1bc4f1c1338e4f6a9ea Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Dec 2019 17:01:01 +0000 Subject: [PATCH 080/322] CodecConfigurer implementation refactoring Backport of 9d65830133c96da18676555818f88c24a0b12753 See gh-24201 --- .../http/codec/ClientCodecConfigurer.java | 2 + .../http/codec/CodecConfigurer.java | 9 +- .../http/codec/ServerCodecConfigurer.java | 3 + .../multipart/MultipartHttpMessageWriter.java | 10 ++ .../http/codec/support/BaseDefaultCodecs.java | 150 +++++++++++++----- .../support/ClientDefaultCodecsImpl.java | 19 +-- .../support/ServerDefaultCodecsImpl.java | 22 +-- .../support/ClientCodecConfigurerTests.java | 84 ++++++---- .../support/ServerCodecConfigurerTests.java | 50 +++--- 9 files changed, 228 insertions(+), 121 deletions(-) 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 e41ec734811..070b0610ee1 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 @@ -95,6 +95,8 @@ interface ClientDefaultCodecs extends DefaultCodecs { *

By default if this is not set, and Jackson is available, the * {@link #jackson2JsonDecoder} override is used instead. Use this property * if you want to further customize the SSE decoder. + *

Note that {@link #maxInMemorySize(int)}, if configured, will be + * applied to the given decoder. * @param decoder the decoder to use */ void serverSentEventDecoder(Decoder decoder); 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 518d3fac7e1..5de5e27db13 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 @@ -109,6 +109,8 @@ interface DefaultCodecs { /** * Override the default Jackson JSON {@code Decoder}. + *

Note that {@link #maxInMemorySize(int)}, if configured, will be + * applied to the given decoder. * @param decoder the decoder instance to use * @see org.springframework.http.codec.json.Jackson2JsonDecoder */ @@ -123,6 +125,8 @@ interface DefaultCodecs { /** * Override the default Protobuf {@code Decoder}. + *

Note that {@link #maxInMemorySize(int)}, if configured, will be + * applied to the given decoder. * @param decoder the decoder instance to use * @since 5.1 * @see org.springframework.http.codec.protobuf.ProtobufDecoder @@ -140,6 +144,8 @@ interface DefaultCodecs { /** * Override the default JAXB2 {@code Decoder}. + *

Note that {@link #maxInMemorySize(int)}, if configured, will be + * applied to the given decoder. * @param decoder the decoder instance to use * @since 5.1.3 * @see org.springframework.http.codec.xml.Jaxb2XmlDecoder @@ -246,7 +252,8 @@ interface DefaultCodecConfig { * Whether to log form data at DEBUG level, and headers at TRACE level. * Both may contain sensitive information. */ - boolean isEnableLoggingRequestDetails(); + @Nullable + Boolean isEnableLoggingRequestDetails(); } } 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 ba8501b3b29..0029b7b2345 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 @@ -91,6 +91,9 @@ interface ServerDefaultCodecs extends DefaultCodecs { * MultipartHttpMessageReader} created with an instance of * {@link org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader * SynchronossPartHttpMessageReader}. + *

Note that {@link #maxInMemorySize(int)} and/or + * {@link #enableLoggingRequestDetails(boolean)}, if configured, will be + * applied to the given reader, if applicable. * @param reader the message reader to use for multipart requests. * @since 5.1.11 */ diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index f5245b61fa7..13a1dfdc684 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -149,6 +149,16 @@ public List> getPartWriters() { return Collections.unmodifiableList(this.partWriters); } + + /** + * Return the configured form writer. + * @since 5.1.13 + */ + @Nullable + public HttpMessageWriter> getFormWriter() { + return this.formWriter; + } + /** * Set the character set to use for part headers such as * "Content-Disposition" (and its filename parameter). 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 75ec9191dcb..292209d19a8 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 @@ -36,14 +36,19 @@ import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.FormHttpMessageReader; +import org.springframework.http.codec.FormHttpMessageWriter; 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.json.AbstractJackson2Decoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.multipart.MultipartHttpMessageReader; +import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; +import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -69,6 +74,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure private static final boolean protobufPresent; + static final boolean synchronossMultipartPresent; + static { ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader(); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && @@ -76,6 +83,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader); protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader); + synchronossMultipartPresent = ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", classLoader); } @@ -100,7 +108,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure @Nullable private Integer maxInMemorySize; - private boolean enableLoggingRequestDetails = false; + @Nullable + private Boolean enableLoggingRequestDetails; private boolean registerDefaults = true; @@ -170,7 +179,8 @@ public void enableLoggingRequestDetails(boolean enable) { } @Override - public boolean isEnableLoggingRequestDetails() { + @Nullable + public Boolean isEnableLoggingRequestDetails() { return this.enableLoggingRequestDetails; } @@ -190,48 +200,107 @@ final List> getTypedReaders() { return Collections.emptyList(); } List> readers = new ArrayList<>(); - readers.add(new DecoderHttpMessageReader<>(init(new ByteArrayDecoder()))); - readers.add(new DecoderHttpMessageReader<>(init(new ByteBufferDecoder()))); - readers.add(new DecoderHttpMessageReader<>(init(new DataBufferDecoder()))); - readers.add(new DecoderHttpMessageReader<>(init(new ResourceDecoder()))); - readers.add(new DecoderHttpMessageReader<>(init(StringDecoder.textPlainOnly()))); + addCodec(readers, new DecoderHttpMessageReader<>(new ByteArrayDecoder())); + addCodec(readers, new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + addCodec(readers, new DecoderHttpMessageReader<>(new DataBufferDecoder())); + addCodec(readers, new DecoderHttpMessageReader<>(new ResourceDecoder())); + addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly())); if (protobufPresent) { - Decoder decoder = this.protobufDecoder != null ? this.protobufDecoder : init(new ProtobufDecoder()); - readers.add(new DecoderHttpMessageReader<>(decoder)); - } - - FormHttpMessageReader formReader = new FormHttpMessageReader(); - if (this.maxInMemorySize != null) { - formReader.setMaxInMemorySize(this.maxInMemorySize); + Decoder decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder(); + addCodec(readers, new DecoderHttpMessageReader<>(decoder)); } - formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails); - readers.add(formReader); + addCodec(readers, new FormHttpMessageReader()); + // client vs server.. extendTypedReaders(readers); return readers; } - private > T init(T decoder) { - if (this.maxInMemorySize != null) { - if (decoder instanceof AbstractDataBufferDecoder) { - ((AbstractDataBufferDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + /** + * Initialize a codec and add it to the List. + * @since 5.1.13 + */ + protected void addCodec(List codecs, T codec) { + initCodec(codec); + codecs.add(codec); + } + + /** + * Apply {@link #maxInMemorySize()} and {@link #enableLoggingRequestDetails}, + * if configured by the application, to the given codec , including any + * codec it contains. + */ + private void initCodec(@Nullable Object codec) { + + if (codec instanceof DecoderHttpMessageReader) { + codec = ((DecoderHttpMessageReader) codec).getDecoder(); + } + else if (codec instanceof ServerSentEventHttpMessageReader) { + codec = ((ServerSentEventHttpMessageReader) codec).getDecoder(); + } + + if (codec == null) { + return; + } + + Integer size = this.maxInMemorySize; + if (size != null) { + if (codec instanceof AbstractDataBufferDecoder) { + ((AbstractDataBufferDecoder) codec).setMaxInMemorySize(size); } - if (decoder instanceof ProtobufDecoder) { - ((ProtobufDecoder) decoder).setMaxMessageSize(this.maxInMemorySize); + if (protobufPresent) { + if (codec instanceof ProtobufDecoder) { + ((ProtobufDecoder) codec).setMaxMessageSize(size); + } } if (jackson2Present) { - if (decoder instanceof AbstractJackson2Decoder) { - ((AbstractJackson2Decoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + if (codec instanceof AbstractJackson2Decoder) { + ((AbstractJackson2Decoder) codec).setMaxInMemorySize(size); } } if (jaxb2Present) { - if (decoder instanceof Jaxb2XmlDecoder) { - ((Jaxb2XmlDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + if (codec instanceof Jaxb2XmlDecoder) { + ((Jaxb2XmlDecoder) codec).setMaxInMemorySize(size); } } + if (codec instanceof FormHttpMessageReader) { + ((FormHttpMessageReader) codec).setMaxInMemorySize(size); + } + if (synchronossMultipartPresent) { + if (codec instanceof SynchronossPartHttpMessageReader) { + ((SynchronossPartHttpMessageReader) codec).setMaxInMemorySize(size); + } + } + } + + Boolean enable = this.enableLoggingRequestDetails; + if (enable != null) { + if (codec instanceof FormHttpMessageReader) { + ((FormHttpMessageReader) codec).setEnableLoggingRequestDetails(enable); + } + if (codec instanceof MultipartHttpMessageReader) { + ((MultipartHttpMessageReader) codec).setEnableLoggingRequestDetails(enable); + } + if (synchronossMultipartPresent) { + if (codec instanceof SynchronossPartHttpMessageReader) { + ((SynchronossPartHttpMessageReader) codec).setEnableLoggingRequestDetails(enable); + } + } + if (codec instanceof FormHttpMessageWriter) { + ((FormHttpMessageWriter) codec).setEnableLoggingRequestDetails(enable); + } + if (codec instanceof MultipartHttpMessageWriter) { + ((MultipartHttpMessageWriter) codec).setEnableLoggingRequestDetails(enable); + } + } + + if (codec instanceof MultipartHttpMessageReader) { + initCodec(((MultipartHttpMessageReader) codec).getPartReader()); + } + else if (codec instanceof MultipartHttpMessageWriter) { + initCodec(((MultipartHttpMessageWriter) codec).getFormWriter()); } - return decoder; } /** @@ -249,16 +318,19 @@ final List> getObjectReaders() { } List> readers = new ArrayList<>(); if (jackson2Present) { - readers.add(new DecoderHttpMessageReader<>(init(getJackson2JsonDecoder()))); + addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); } if (jackson2SmilePresent) { - readers.add(new DecoderHttpMessageReader<>(init(new Jackson2SmileDecoder()))); + addCodec(readers, new DecoderHttpMessageReader<>(new Jackson2SmileDecoder())); } if (jaxb2Present) { - Decoder decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : init(new Jaxb2XmlDecoder()); - readers.add(new DecoderHttpMessageReader<>(decoder)); + Decoder decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder(); + addCodec(readers, new DecoderHttpMessageReader<>(decoder)); } + + // client vs server.. extendObjectReaders(readers); + return readers; } @@ -275,9 +347,9 @@ final List> getCatchAllReaders() { if (!this.registerDefaults) { return Collections.emptyList(); } - List> result = new ArrayList<>(); - result.add(new DecoderHttpMessageReader<>(init(StringDecoder.allMimeTypes()))); - return result; + List> readers = new ArrayList<>(); + addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + return readers; } /** @@ -364,11 +436,17 @@ List> getCatchAllWriters() { // Accessors for use in subclasses... protected Decoder getJackson2JsonDecoder() { - return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder()); + if (this.jackson2JsonDecoder == null) { + this.jackson2JsonDecoder = new Jackson2JsonDecoder(); + } + return this.jackson2JsonDecoder; } protected Encoder getJackson2JsonEncoder() { - return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder()); + if (this.jackson2JsonEncoder == null) { + this.jackson2JsonEncoder = new Jackson2JsonEncoder(); + } + return this.jackson2JsonEncoder; } } 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 e764cb96961..dae7b25704d 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 @@ -95,24 +95,17 @@ public ClientDefaultCodecsImpl clone() { @Override protected void extendObjectReaders(List> objectReaders) { - objectReaders.add(new ServerSentEventHttpMessageReader(getSseDecoder())); - } - @Nullable - private Decoder getSseDecoder() { - return (this.sseDecoder != null ? this.sseDecoder : jackson2Present ? getJackson2JsonDecoder() : null); + Decoder decoder = (this.sseDecoder != null ? + this.sseDecoder : + jackson2Present ? getJackson2JsonDecoder() : null); + + addCodec(objectReaders, new ServerSentEventHttpMessageReader(decoder)); } @Override protected void extendTypedWriters(List> typedWriters) { - - FormHttpMessageWriter formWriter = new FormHttpMessageWriter(); - formWriter.setEnableLoggingRequestDetails(isEnableLoggingRequestDetails()); - - MultipartHttpMessageWriter multipartWriter = new MultipartHttpMessageWriter(getPartWriters(), formWriter); - multipartWriter.setEnableLoggingRequestDetails(isEnableLoggingRequestDetails()); - - typedWriters.add(multipartWriter); + addCodec(typedWriters, new MultipartHttpMessageWriter(getPartWriters(), new FormHttpMessageWriter())); } private List> getPartWriters() { 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 1d997c3777b..eaab4e3237c 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 @@ -25,7 +25,6 @@ import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * Default implementation of {@link ServerCodecConfigurer.ServerDefaultCodecs}. @@ -34,11 +33,6 @@ */ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecConfigurer.ServerDefaultCodecs { - private static final boolean synchronossMultipartPresent = - ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", - DefaultServerCodecConfigurer.class.getClassLoader()); - - @Nullable private HttpMessageReader multipartReader; @@ -70,23 +64,13 @@ public void serverSentEventEncoder(Encoder encoder) { @Override protected void extendTypedReaders(List> typedReaders) { if (this.multipartReader != null) { - typedReaders.add(this.multipartReader); + addCodec(typedReaders, this.multipartReader); return; } if (synchronossMultipartPresent) { - boolean enable = isEnableLoggingRequestDetails(); - SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader(); - Integer size = maxInMemorySize(); - if (size != null) { - partReader.setMaxInMemorySize(size); - } - partReader.setEnableLoggingRequestDetails(enable); - typedReaders.add(partReader); - - MultipartHttpMessageReader reader = new MultipartHttpMessageReader(partReader); - reader.setEnableLoggingRequestDetails(enable); - typedReaders.add(reader); + addCodec(typedReaders, partReader); + addCodec(typedReaders, new MultipartHttpMessageReader(partReader)); } } 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 1b98450036e..87572c61992 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,7 +22,6 @@ 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; @@ -45,6 +44,7 @@ import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.FormHttpMessageReader; +import org.springframework.http.codec.FormHttpMessageWriter; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter; @@ -119,12 +119,45 @@ public void jackson2EncoderOverride() { Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); this.configurer.defaultCodecs().jackson2JsonDecoder(decoder); - assertSame(decoder, this.configurer.getReaders().stream() - .filter(reader -> ServerSentEventHttpMessageReader.class.equals(reader.getClass())) - .map(reader -> (ServerSentEventHttpMessageReader) reader) - .findFirst() - .map(ServerSentEventHttpMessageReader::getDecoder) - .filter(e -> e == decoder).orElse(null)); + List> readers = this.configurer.getReaders(); + assertSame(decoder, findCodec(readers, ServerSentEventHttpMessageReader.class).getDecoder()); + } + + @Test + public void maxInMemorySize() { + int size = 99; + this.configurer.defaultCodecs().maxInMemorySize(size); + List> readers = this.configurer.getReaders(); + assertEquals(12, readers.size()); + assertEquals(size, ((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ResourceDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()); + assertEquals(size, ((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()); + + assertEquals(size, ((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + + ServerSentEventHttpMessageReader reader = (ServerSentEventHttpMessageReader) nextReader(readers); + assertEquals(size, ((Jackson2JsonDecoder) reader.getDecoder()).getMaxInMemorySize()); + + assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + } + + @Test + public void enableLoggingRequestDetails() { + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + + List> writers = this.configurer.getWriters(); + MultipartHttpMessageWriter multipartWriter = findCodec(writers, MultipartHttpMessageWriter.class); + assertTrue(multipartWriter.isEnableLoggingRequestDetails()); + + FormHttpMessageWriter formWriter = (FormHttpMessageWriter) multipartWriter.getFormWriter(); + assertNotNull(formWriter); + assertTrue(formWriter.isEnableLoggingRequestDetails()); } @Test @@ -138,35 +171,19 @@ public void cloneConfigurer() { // 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()); + Decoder sseDecoder = findCodec(clone.getReaders(), ServerSentEventHttpMessageReader.class).getDecoder(); + List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); assertSame(jackson2Decoder, sseDecoder); - assertEquals(2, multipartWriters.size()); + assertEquals(2, writers.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()); + sseDecoder = findCodec(this.configurer.getReaders(), ServerSentEventHttpMessageReader.class).getDecoder(); + writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); assertNotSame(jackson2Decoder, sseDecoder); - assertEquals(10, multipartWriters.size()); + assertEquals(10, writers.size()); } private Decoder getNextDecoder(List> readers) { @@ -175,12 +192,21 @@ private Decoder getNextDecoder(List> readers) { return ((DecoderHttpMessageReader) reader).getDecoder(); } + private HttpMessageReader nextReader(List> readers) { + return readers.get(this.index.getAndIncrement()); + } + private Encoder getNextEncoder(List> writers) { HttpMessageWriter writer = writers.get(this.index.getAndIncrement()); assertEquals(EncoderHttpMessageWriter.class, writer.getClass()); return ((EncoderHttpMessageWriter) writer).getEncoder(); } + @SuppressWarnings("unchecked") + private T findCodec(List codecs, Class type) { + return (T) codecs.stream().filter(type::isInstance).findFirst().get(); + } + @SuppressWarnings("unchecked") private void assertStringDecoder(Decoder decoder, boolean textOnly) { assertEquals(StringDecoder.class, decoder.getClass()); 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 6373ff6b515..f74b4832d12 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 @@ -153,6 +153,20 @@ public void maxInMemorySize() { assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); } + @Test + public void enableRequestLoggingDetails() { + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + + List> readers = this.configurer.getReaders(); + assertTrue(findCodec(readers, FormHttpMessageReader.class).isEnableLoggingRequestDetails()); + + MultipartHttpMessageReader multipartReader = findCodec(readers, MultipartHttpMessageReader.class); + assertTrue(multipartReader.isEnableLoggingRequestDetails()); + + SynchronossPartHttpMessageReader reader = (SynchronossPartHttpMessageReader) multipartReader.getPartReader(); + assertTrue(reader.isEnableLoggingRequestDetails()); + } + @Test public void cloneConfigurer() { ServerCodecConfigurer clone = this.configurer.clone(); @@ -164,37 +178,22 @@ public void cloneConfigurer() { // 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(); + HttpMessageReader actualReader = + findCodec(clone.getReaders(), MultipartHttpMessageReader.class); + ServerSentEventHttpMessageWriter actualWriter = + findCodec(clone.getWriters(), ServerSentEventHttpMessageWriter.class); assertSame(reader, actualReader); - assertSame(encoder, actualEncoder); + assertSame(encoder, actualWriter.getEncoder()); // 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(); - + actualReader = findCodec(this.configurer.getReaders(), MultipartHttpMessageReader.class); + actualWriter = findCodec(this.configurer.getWriters(), ServerSentEventHttpMessageWriter.class); assertNotSame(reader, actualReader); - assertNotSame(encoder, actualEncoder); + assertNotSame(encoder, actualWriter.getEncoder()); } private Decoder getNextDecoder(List> readers) { @@ -213,6 +212,11 @@ private Encoder getNextEncoder(List> writers) { return ((EncoderHttpMessageWriter) writer).getEncoder(); } + @SuppressWarnings("unchecked") + private T findCodec(List codecs, Class type) { + return (T) codecs.stream().filter(type::isInstance).findFirst().get(); + } + @SuppressWarnings("unchecked") private void assertStringDecoder(Decoder decoder, boolean textOnly) { assertEquals(StringDecoder.class, decoder.getClass()); From 802d083df7213e8c69e8016ae288be270975f406 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Dec 2019 18:06:12 +0000 Subject: [PATCH 081/322] Add register methods to CodecConfigurer.CustomCodecs The new register methods replace the now deprecated encoder, decoder, reader, and writer methods, and also offer a choice to opt into default properties such maxInMemorySize, if configured. Backport of 11e321b8e7f4e7f35c0ac18d222e20466c615728 See gh-24201 --- .../http/codec/CodecConfigurer.java | 65 +++++++++- .../codec/support/BaseCodecConfigurer.java | 114 ++++++++++++------ .../http/codec/support/BaseDefaultCodecs.java | 16 +++ .../codec/support/CodecConfigurerTests.java | 41 ++++--- .../support/ServerCodecConfigurerTests.java | 40 ++++++ .../WebFluxConfigurationSupportTests.java | 8 +- .../ControllerMethodResolverTests.java | 4 +- 7 files changed, 221 insertions(+), 67 deletions(-) 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 5de5e27db13..8d387d19858 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 @@ -192,18 +192,66 @@ interface DefaultCodecs { */ interface CustomCodecs { + /** + * Register a custom codec. This is expected to be one of the following: + *

    + *
  • {@link HttpMessageReader} + *
  • {@link HttpMessageWriter} + *
  • {@link Encoder} (wrapped internally with {@link EncoderHttpMessageWriter}) + *
  • {@link Decoder} (wrapped internally with {@link DecoderHttpMessageReader}) + *
+ * @param codec the codec to register + * @since 5.1.13 + */ + void register(Object codec); + + /** + * Variant of {@link #register(Object)} that also applies the below + * properties, if configured, via {@link #defaultCodecs()}: + *
    + *
  • {@link CodecConfigurer.DefaultCodecs#maxInMemorySize(int) maxInMemorySize} + *
  • {@link CodecConfigurer.DefaultCodecs#enableLoggingRequestDetails(boolean) enableLoggingRequestDetails} + *
+ *

The properties are applied every time {@link #getReaders()} or + * {@link #getWriters()} are used to obtain the list of configured + * readers or writers. + * @param codec the codec to register and apply default config to + * @since 5.1.13 + */ + void registerWithDefaultConfig(Object codec); + + /** + * Variant of {@link #register(Object)} that also allows the caller to + * apply the properties from {@link DefaultCodecConfig} to the given + * codec. If you want to apply all the properties, prefer using + * {@link #registerWithDefaultConfig(Object)}. + *

The consumer is called every time {@link #getReaders()} or + * {@link #getWriters()} are used to obtain the list of configured + * readers or writers. + * @param codec the codec to register + * @param configConsumer consumer of the default config + * @since 5.1.13 + */ + void registerWithDefaultConfig(Object codec, Consumer configConsumer); + /** * Add a custom {@code Decoder} internally wrapped with * {@link DecoderHttpMessageReader}). * @param decoder the decoder to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void decoder(Decoder decoder); /** * Add a custom {@code Encoder}, internally wrapped with * {@link EncoderHttpMessageWriter}. * @param encoder the encoder to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void encoder(Encoder encoder); /** @@ -211,7 +259,10 @@ interface CustomCodecs { * {@link DecoderHttpMessageReader} consider using the shortcut * {@link #decoder(Decoder)} instead. * @param reader the reader to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void reader(HttpMessageReader reader); /** @@ -219,7 +270,10 @@ interface CustomCodecs { * {@link EncoderHttpMessageWriter} consider using the shortcut * {@link #encoder(Encoder)} instead. * @param writer the writer to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void writer(HttpMessageWriter writer); /** @@ -228,16 +282,21 @@ interface CustomCodecs { * guidelines applied to default ones, such as logging details and limiting * the amount of buffered data. * @param codecsConfigConsumer the default codecs configuration callback - * @since 5.1.12 + * @deprecated as of 5.1.13, use {@link #registerWithDefaultConfig(Object)} + * or {@link #registerWithDefaultConfig(Object, Consumer)} instead. */ + @Deprecated void withDefaultCodecConfig(Consumer codecsConfigConsumer); } /** - * Common options applied to default codecs and passed in a callback to custom codecs - * so they get a chance to align their behavior on the default ones. + * Exposes the values of properties configured through + * {@link #defaultCodecs()} that are applied to default codecs. + * The main purpose of this interface is to provide access to them so they + * can also be applied to custom codecs if needed. * @since 5.1.12 + * @see CustomCodecs#registerWithDefaultConfig(Object, Consumer) */ interface DefaultCodecConfig { 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 a63e0be8882..c698d5fba32 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 @@ -17,7 +17,9 @@ package org.springframework.http.codec.support; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import org.springframework.core.ResolvableType; @@ -40,8 +42,6 @@ */ abstract class BaseCodecConfigurer implements CodecConfigurer { - protected boolean customCodecsInitialized; - protected final BaseDefaultCodecs defaultCodecs; protected final DefaultCustomCodecs customCodecs; @@ -91,21 +91,20 @@ public CustomCodecs customCodecs() { @Override public List> getReaders() { - initializeCustomCodecs(); - List> result = new ArrayList<>(); + this.defaultCodecs.applyDefaultConfig(this.customCodecs); + List> result = new ArrayList<>(); result.addAll(this.defaultCodecs.getTypedReaders()); - result.addAll(this.customCodecs.getTypedReaders()); - + result.addAll(this.customCodecs.getTypedReaders().keySet()); result.addAll(this.defaultCodecs.getObjectReaders()); - result.addAll(this.customCodecs.getObjectReaders()); - + result.addAll(this.customCodecs.getObjectReaders().keySet()); result.addAll(this.defaultCodecs.getCatchAllReaders()); return result; } @Override public List> getWriters() { + this.defaultCodecs.applyDefaultConfig(this.customCodecs); return getWritersInternal(false); } @@ -117,14 +116,13 @@ public List> getWriters() { * same except for the multipart writer itself. */ protected List> getWritersInternal(boolean forMultipart) { - initializeCustomCodecs(); List> result = new ArrayList<>(); result.addAll(this.defaultCodecs.getTypedWriters(forMultipart)); - result.addAll(this.customCodecs.getTypedWriters()); + result.addAll(this.customCodecs.getTypedWriters().keySet()); result.addAll(this.defaultCodecs.getObjectWriters(forMultipart)); - result.addAll(this.customCodecs.getObjectWriters()); + result.addAll(this.customCodecs.getObjectWriters().keySet()); result.addAll(this.defaultCodecs.getCatchAllWriters()); return result; @@ -133,28 +131,21 @@ protected List> getWritersInternal(boolean forMultipart) { @Override public abstract CodecConfigurer clone(); - private void initializeCustomCodecs() { - if(!this.customCodecsInitialized) { - this.customCodecs.configConsumers.forEach(consumer -> consumer.accept(this.defaultCodecs)); - this.customCodecsInitialized = true; - } - } - /** * Default implementation of {@code CustomCodecs}. */ protected static final class DefaultCustomCodecs implements CustomCodecs { - private final List> typedReaders = new ArrayList<>(); + private final Map, Boolean> typedReaders = new LinkedHashMap<>(4); - private final List> typedWriters = new ArrayList<>(); + private final Map, Boolean> typedWriters = new LinkedHashMap<>(4); - private final List> objectReaders = new ArrayList<>(); + private final Map, Boolean> objectReaders = new LinkedHashMap<>(4); - private final List> objectWriters = new ArrayList<>(); + private final Map, Boolean> objectWriters = new LinkedHashMap<>(4); - private final List> configConsumers = new ArrayList<>(); + private final List> defaultConfigConsumers = new ArrayList<>(4); DefaultCustomCodecs() { } @@ -164,56 +155,103 @@ protected static final class DefaultCustomCodecs implements CustomCodecs { * @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); + other.typedReaders.putAll(this.typedReaders); + other.typedWriters.putAll(this.typedWriters); + other.objectReaders.putAll(this.objectReaders); + other.objectWriters.putAll(this.objectWriters); + } + + @Override + public void register(Object codec) { + addCodec(codec, false); } + @Override + public void registerWithDefaultConfig(Object codec) { + addCodec(codec, true); + } + + @Override + public void registerWithDefaultConfig(Object codec, Consumer configConsumer) { + addCodec(codec, false); + this.defaultConfigConsumers.add(configConsumer); + } + + @SuppressWarnings("deprecation") @Override public void decoder(Decoder decoder) { - reader(new DecoderHttpMessageReader<>(decoder)); + addCodec(decoder, false); } + @SuppressWarnings("deprecation") @Override public void encoder(Encoder encoder) { - writer(new EncoderHttpMessageWriter<>(encoder)); + addCodec(encoder, false); } + @SuppressWarnings("deprecation") @Override public void reader(HttpMessageReader reader) { - boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null); - (canReadToObject ? this.objectReaders : this.typedReaders).add(reader); + addCodec(reader, false); } + @SuppressWarnings("deprecation") @Override public void writer(HttpMessageWriter writer) { - boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null); - (canWriteObject ? this.objectWriters : this.typedWriters).add(writer); + addCodec(writer, false); } + @SuppressWarnings("deprecation") @Override public void withDefaultCodecConfig(Consumer codecsConfigConsumer) { - this.configConsumers.add(codecsConfigConsumer); + this.defaultConfigConsumers.add(codecsConfigConsumer); + } + + private void addCodec(Object codec, boolean applyDefaultConfig) { + + if (codec instanceof Decoder) { + codec = new DecoderHttpMessageReader<>((Decoder) codec); + } + else if (codec instanceof Encoder) { + codec = new EncoderHttpMessageWriter<>((Encoder) codec); + } + + if (codec instanceof HttpMessageReader) { + HttpMessageReader reader = (HttpMessageReader) codec; + boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null); + (canReadToObject ? this.objectReaders : this.typedReaders).put(reader, applyDefaultConfig); + } + else if (codec instanceof HttpMessageWriter) { + HttpMessageWriter writer = (HttpMessageWriter) codec; + boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null); + (canWriteObject ? this.objectWriters : this.typedWriters).put(writer, applyDefaultConfig); + } + else { + throw new IllegalArgumentException("Unexpected codec type: " + codec.getClass().getName()); + } } // Package private accessors... - List> getTypedReaders() { + Map, Boolean> getTypedReaders() { return this.typedReaders; } - List> getTypedWriters() { + Map, Boolean> getTypedWriters() { return this.typedWriters; } - List> getObjectReaders() { + Map, Boolean> getObjectReaders() { return this.objectReaders; } - List> getObjectWriters() { + Map, Boolean> getObjectWriters() { return this.objectWriters; } + + List> getDefaultConfigConsumers() { + return this.defaultConfigConsumers; + } } } 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 292209d19a8..00b0fdab3e3 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 @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.springframework.core.codec.AbstractDataBufferDecoder; import org.springframework.core.codec.ByteArrayDecoder; @@ -432,6 +433,21 @@ List> getCatchAllWriters() { return result; } + void applyDefaultConfig(BaseCodecConfigurer.DefaultCustomCodecs customCodecs) { + applyDefaultConfig(customCodecs.getTypedReaders()); + applyDefaultConfig(customCodecs.getObjectReaders()); + applyDefaultConfig(customCodecs.getTypedWriters()); + applyDefaultConfig(customCodecs.getObjectWriters()); + customCodecs.getDefaultConfigConsumers().forEach(consumer -> consumer.accept(this)); + } + + private void applyDefaultConfig(Map readers) { + readers.entrySet().stream() + .filter(Map.Entry::getValue) + .map(Map.Entry::getKey) + .forEach(this::initCodec); + } + // Accessors for use in subclasses... 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 02f41655f46..d02ef4951f7 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 @@ -121,11 +121,11 @@ public void defaultAndCustomReaders() { when(customReader1.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(false); when(customReader2.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(true); - this.configurer.customCodecs().decoder(customDecoder1); - this.configurer.customCodecs().decoder(customDecoder2); + this.configurer.customCodecs().register(customDecoder1); + this.configurer.customCodecs().register(customDecoder2); - this.configurer.customCodecs().reader(customReader1); - this.configurer.customCodecs().reader(customReader2); + this.configurer.customCodecs().register(customReader1); + this.configurer.customCodecs().register(customReader2); List> readers = this.configurer.getReaders(); @@ -161,11 +161,11 @@ public void defaultAndCustomWriters() { when(customWriter1.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(false); when(customWriter2.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(true); - this.configurer.customCodecs().encoder(customEncoder1); - this.configurer.customCodecs().encoder(customEncoder2); + this.configurer.customCodecs().register(customEncoder1); + this.configurer.customCodecs().register(customEncoder2); - this.configurer.customCodecs().writer(customWriter1); - this.configurer.customCodecs().writer(customWriter2); + this.configurer.customCodecs().register(customWriter1); + this.configurer.customCodecs().register(customWriter2); List> writers = this.configurer.getWriters(); @@ -200,11 +200,11 @@ public void defaultsOffCustomReaders() { when(customReader1.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(false); when(customReader2.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(true); - this.configurer.customCodecs().decoder(customDecoder1); - this.configurer.customCodecs().decoder(customDecoder2); + this.configurer.customCodecs().register(customDecoder1); + this.configurer.customCodecs().register(customDecoder2); - this.configurer.customCodecs().reader(customReader1); - this.configurer.customCodecs().reader(customReader2); + this.configurer.customCodecs().register(customReader1); + this.configurer.customCodecs().register(customReader2); this.configurer.registerDefaults(false); @@ -231,11 +231,11 @@ public void defaultsOffWithCustomWriters() { when(customWriter1.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(false); when(customWriter2.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(true); - this.configurer.customCodecs().encoder(customEncoder1); - this.configurer.customCodecs().encoder(customEncoder2); + this.configurer.customCodecs().register(customEncoder1); + this.configurer.customCodecs().register(customEncoder2); - this.configurer.customCodecs().writer(customWriter1); - this.configurer.customCodecs().writer(customWriter2); + this.configurer.customCodecs().register(customWriter1); + this.configurer.customCodecs().register(customWriter2); this.configurer.registerDefaults(false); @@ -277,10 +277,10 @@ 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()); + clone.customCodecs().register(new Jackson2JsonEncoder()); + clone.customCodecs().register(new Jackson2JsonDecoder()); + clone.customCodecs().register(new ServerSentEventHttpMessageReader()); + clone.customCodecs().register(new ServerSentEventHttpMessageWriter()); assertEquals(0, this.configurer.getReaders().size()); assertEquals(0, this.configurer.getWriters().size()); @@ -337,6 +337,7 @@ public void cloneDefaultCodecs() { assertFalse(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder))); } + @SuppressWarnings("deprecation") @Test public void withDefaultCodecConfig() { AtomicBoolean callbackCalled = new AtomicBoolean(false); 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 f74b4832d12..b5a2c8d6e46 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 @@ -40,6 +40,7 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; +import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.FormHttpMessageReader; @@ -61,6 +62,7 @@ import org.springframework.util.MimeTypeUtils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; @@ -132,6 +134,7 @@ public void jackson2EncoderOverride() { public void maxInMemorySize() { int size = 99; this.configurer.defaultCodecs().maxInMemorySize(size); + List> readers = this.configurer.getReaders(); assertEquals(13, readers.size()); assertEquals(size, ((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()); @@ -153,6 +156,28 @@ public void maxInMemorySize() { assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); } + @Test + public void maxInMemorySizeWithCustomCodecs() { + + int size = 99; + this.configurer.defaultCodecs().maxInMemorySize(size); + this.configurer.registerDefaults(false); + + CodecConfigurer.CustomCodecs customCodecs = this.configurer.customCodecs(); + customCodecs.register(new ByteArrayDecoder()); + customCodecs.registerWithDefaultConfig(new ByteArrayDecoder()); + customCodecs.register(new Jackson2JsonDecoder()); + customCodecs.registerWithDefaultConfig(new Jackson2JsonDecoder()); + + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + + List> readers = this.configurer.getReaders(); + assertEquals(-1, ((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(-1, ((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + assertEquals(size, ((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()); + } + @Test public void enableRequestLoggingDetails() { this.configurer.defaultCodecs().enableLoggingRequestDetails(true); @@ -167,6 +192,21 @@ public void enableRequestLoggingDetails() { assertTrue(reader.isEnableLoggingRequestDetails()); } + @Test + public void enableRequestLoggingDetailsWithCustomCodecs() { + + this.configurer.registerDefaults(false); + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + + CodecConfigurer.CustomCodecs customCodecs = this.configurer.customCodecs(); + customCodecs.register(new FormHttpMessageReader()); + customCodecs.registerWithDefaultConfig(new FormHttpMessageReader()); + + List> readers = this.configurer.getReaders(); + assertFalse(((FormHttpMessageReader) readers.get(0)).isEnableLoggingRequestDetails()); + assertTrue(((FormHttpMessageReader) readers.get(1)).isEnableLoggingRequestDetails()); + } + @Test public void cloneConfigurer() { ServerCodecConfigurer clone = this.configurer.clone(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index 99d8ce7ba99..6b50a0ff1fd 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -351,10 +351,10 @@ static class CustomMessageConverterConfig extends WebFluxConfigurationSupport { @Override protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { configurer.registerDefaults(false); - configurer.customCodecs().decoder(StringDecoder.textPlainOnly()); - configurer.customCodecs().decoder(new Jaxb2XmlDecoder()); - configurer.customCodecs().encoder(CharSequenceEncoder.textPlainOnly()); - configurer.customCodecs().encoder(new Jaxb2XmlEncoder()); + configurer.customCodecs().register(StringDecoder.textPlainOnly()); + configurer.customCodecs().register(new Jaxb2XmlDecoder()); + configurer.customCodecs().register(CharSequenceEncoder.textPlainOnly()); + configurer.customCodecs().register(new Jaxb2XmlEncoder()); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java index 56ff66c5c7b..61f7f704c68 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java @@ -68,8 +68,8 @@ public void setup() { resolvers.addCustomResolver(new CustomSyncArgumentResolver()); ServerCodecConfigurer codecs = ServerCodecConfigurer.create(); - codecs.customCodecs().decoder(new ByteArrayDecoder()); - codecs.customCodecs().decoder(new ByteBufferDecoder()); + codecs.customCodecs().register(new ByteArrayDecoder()); + codecs.customCodecs().register(new ByteBufferDecoder()); AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(TestControllerAdvice.class); From 26a2d3875f89cce1f2ba08bc58d4646ec7d83667 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Dec 2019 20:56:03 +0000 Subject: [PATCH 082/322] Expose ClientCodecConfigurer in WebClient.Builder Using Consumer instead of Consumer eliminates one level of nesting that is also unnecessary since codecs are the only strategy at present. Backport of dd9b6287b4c3b507f963c655144ed06dd8f5ff21 Closes gh-24201 --- .../server/DefaultWebTestClientBuilder.java | 8 ++++++ .../web/reactive/server/WebTestClient.java | 24 ++++++++++++----- .../client/DefaultWebClientBuilder.java | 11 ++++++++ .../reactive/function/client/WebClient.java | 27 +++++++++++++------ src/docs/asciidoc/web/webflux.adoc | 14 ++++------ 5 files changed, 60 insertions(+), 24 deletions(-) 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 82006d37b25..69279499fe7 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 @@ -23,6 +23,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -136,12 +137,19 @@ public WebTestClient.Builder filters(Consumer> filt return this; } + @Override + public WebTestClient.Builder codecs(Consumer configurer) { + this.webClientBuilder.codecs(configurer); + return this; + } + @Override public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) { this.webClientBuilder.exchangeStrategies(strategies); return this; } + @SuppressWarnings("deprecation") @Override public WebTestClient.Builder exchangeStrategies(Consumer configurer) { this.webClientBuilder.exchangeStrategies(configurer); 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 e8b109cbc01..691d7aa6846 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 @@ -36,6 +36,7 @@ import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; @@ -435,14 +436,22 @@ interface Builder { */ Builder filters(Consumer> filtersConsumer); + /** + * Configure the codecs for the {@code WebClient} in the + * {@link #exchangeStrategies(ExchangeStrategies) underlying} + * {@code ExchangeStrategies}. + * @param configurer the configurer to apply + * @since 5.1.13 + */ + Builder codecs(Consumer configurer); + /** * Configure the {@link ExchangeStrategies} to use. - *

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

For most cases, prefer using {@link #codecs(Consumer)} which allows + * customizing the codecs in the {@code ExchangeStrategies} rather than + * replace them. That ensures multiple parties can contribute to codecs + * configuration. + *

By default this is set to {@link ExchangeStrategies#withDefaults()}. * @param strategies the strategies to use */ Builder exchangeStrategies(ExchangeStrategies strategies); @@ -452,8 +461,9 @@ interface Builder { * {@link #exchangeStrategies(ExchangeStrategies)}. This method is * designed for use in scenarios where multiple parties wish to update * the {@code ExchangeStrategies}. - * @since 5.1.12 + * @deprecated as of 5.1.13 in favor of {@link #codecs(Consumer)} */ + @Deprecated Builder exchangeStrategies(Consumer configurer); /** 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 44c3164b701..c255c439d32 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 @@ -27,6 +27,7 @@ import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -206,12 +207,22 @@ public WebClient.Builder clientConnector(ClientHttpConnector connector) { return this; } + @Override + public WebClient.Builder codecs(Consumer configurer) { + if (this.strategiesConfigurers == null) { + this.strategiesConfigurers = new ArrayList<>(4); + } + this.strategiesConfigurers.add(builder -> builder.codecs(configurer)); + return this; + } + @Override public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { this.strategies = strategies; return this; } + @SuppressWarnings("deprecation") @Override public WebClient.Builder exchangeStrategies(Consumer configurer) { if (this.strategiesConfigurers == null) { 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 f60a822597c..eda41805c75 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 @@ -37,6 +37,7 @@ import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; @@ -288,14 +289,22 @@ interface Builder { */ Builder clientConnector(ClientHttpConnector connector); + /** + * Configure the codecs for the {@code WebClient} in the + * {@link #exchangeStrategies(ExchangeStrategies) underlying} + * {@code ExchangeStrategies}. + * @param configurer the configurer to apply + * @since 5.1.13 + */ + Builder codecs(Consumer configurer); + /** * Configure the {@link ExchangeStrategies} to use. - *

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

For most cases, prefer using {@link #codecs(Consumer)} which allows + * customizing the codecs in the {@code ExchangeStrategies} rather than + * replace them. That ensures multiple parties can contribute to codecs + * configuration. + *

By default this is set to {@link ExchangeStrategies#withDefaults()}. * @param strategies the strategies to use */ Builder exchangeStrategies(ExchangeStrategies strategies); @@ -305,15 +314,17 @@ interface Builder { * {@link #exchangeStrategies(ExchangeStrategies)}. This method is * designed for use in scenarios where multiple parties wish to update * the {@code ExchangeStrategies}. - * @since 5.1.12 + * @deprecated as of 5.1.13 in favor of {@link #codecs(Consumer)} */ + @Deprecated Builder exchangeStrategies(Consumer configurer); /** * Provide an {@link ExchangeFunction} pre-configured with * {@link ClientHttpConnector} and {@link ExchangeStrategies}. *

This is an alternative to, and effectively overrides - * {@link #clientConnector}, and {@link #exchangeStrategies}. + * {@link #clientConnector}, and + * {@link #exchangeStrategies(ExchangeStrategies)}. * @param exchangeFunction the exchange function to use */ Builder exchangeFunction(ExchangeFunction exchangeFunction); diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 3b0e41a055b..3d047c642b7 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -908,20 +908,16 @@ The following example shows how to do so for client-side requests: [source,java,indent=0] [subs="verbatim,quotes"] ---- - Consumer consumer = configurer -> { - CustomDecoder customDecoder = new CustomDecoder(); - configurer.customCodecs().decoder(customDecoder); - configurer.customCodecs().withDefaultCodecConfig(config -> - customDecoder.maxInMemorySize(config.maxInMemorySize()) - ); - } - WebClient webClient = WebClient.builder() - .exchangeStrategies(strategies -> strategies.codecs(consumer)) + .codecs(configurer -> { + CustomDecoder decoder = new CustomDecoder(); + configurer.customCodecs().registerWithDefaultConfig(decoder); + }) .build(); ---- ==== + [[webflux-dispatcher-handler]] == `DispatcherHandler` [.small]#<># From 634aba4ab63b43a700f95301edb0be0282577783 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Dec 2019 21:55:13 +0000 Subject: [PATCH 083/322] Fix cloning issue in CodecConfigurer for multipart writers Backport of b23617637d21f1f49f7f0c1c9c3ccc90c1de5b92 Closes gh-24194 --- .../codec/support/BaseCodecConfigurer.java | 17 +------- .../http/codec/support/BaseDefaultCodecs.java | 42 +++++++++++-------- .../support/ClientDefaultCodecsImpl.java | 10 ++--- .../support/DefaultClientCodecConfigurer.java | 17 +++++++- .../support/ClientCodecConfigurerTests.java | 28 ++++++++++++- 5 files changed, 73 insertions(+), 41 deletions(-) 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 c698d5fba32..b6e351fc8ec 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 @@ -105,25 +105,12 @@ public List> getReaders() { @Override public List> getWriters() { this.defaultCodecs.applyDefaultConfig(this.customCodecs); - return getWritersInternal(false); - } - - /** - * Internal method that returns the configured writers. - * @param forMultipart whether to returns writers for general use ("false"), - * or for multipart requests only ("true"). Generally the two sets are the - * same except for the multipart writer itself. - */ - protected List> getWritersInternal(boolean forMultipart) { List> result = new ArrayList<>(); - - result.addAll(this.defaultCodecs.getTypedWriters(forMultipart)); + result.addAll(this.defaultCodecs.getTypedWriters()); result.addAll(this.customCodecs.getTypedWriters().keySet()); - - result.addAll(this.defaultCodecs.getObjectWriters(forMultipart)); + result.addAll(this.defaultCodecs.getObjectWriters()); result.addAll(this.customCodecs.getObjectWriters().keySet()); - result.addAll(this.defaultCodecs.getCatchAllWriters()); return result; } 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 00b0fdab3e3..e01fb3c29c2 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 @@ -354,13 +354,23 @@ final List> getCatchAllReaders() { } /** - * Return writers that support specific types. - * @param forMultipart whether to returns writers for general use ("false"), - * or for multipart requests only ("true"). Generally the two sets are the - * same except for the multipart writer itself. + * Return all writers that support specific types. + */ + @SuppressWarnings({"rawtypes" }) + final List> getTypedWriters() { + if (!this.registerDefaults) { + return Collections.emptyList(); + } + List> writers = getBaseTypedWriters(); + extendTypedWriters(writers); + return writers; + } + + /** + * Return "base" typed writers only, i.e. common to client and server. */ @SuppressWarnings("unchecked") - final List> getTypedWriters(boolean forMultipart) { + final List> getBaseTypedWriters() { if (!this.registerDefaults) { return Collections.emptyList(); } @@ -370,10 +380,6 @@ final List> getTypedWriters(boolean forMultipart) { writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder())); writers.add(new ResourceHttpMessageWriter()); writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); - // No client or server specific multipart writers currently.. - if (!forMultipart) { - extendTypedWriters(writers); - } if (protobufPresent) { Encoder encoder = this.protobufEncoder != null ? this.protobufEncoder : new ProtobufEncoder(); writers.add(new ProtobufHttpMessageWriter((Encoder) encoder)); @@ -389,14 +395,20 @@ protected void extendTypedWriters(List> typedWriters) { /** * Return Object writers (JSON, XML, SSE). - * @param forMultipart whether to returns writers for general use ("false"), - * or for multipart requests only ("true"). Generally the two sets are the - * same except for the multipart writer itself. */ - final List> getObjectWriters(boolean forMultipart) { + final List> getObjectWriters() { if (!this.registerDefaults) { return Collections.emptyList(); } + List> writers = getBaseObjectWriters(); + extendObjectWriters(writers); + return writers; + } + + /** + * Return "base" object writers only, i.e. common to client and server. + */ + final List> getBaseObjectWriters() { List> writers = new ArrayList<>(); if (jackson2Present) { writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder())); @@ -408,10 +420,6 @@ final List> getObjectWriters(boolean forMultipart) { Encoder encoder = this.jaxb2Encoder != null ? this.jaxb2Encoder : new Jaxb2XmlEncoder(); writers.add(new EncoderHttpMessageWriter<>(encoder)); } - // No client or server specific multipart writers currently.. - if (!forMultipart) { - extendObjectWriters(writers); - } return writers; } 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 dae7b25704d..cc1c7f1a439 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 @@ -54,9 +54,9 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo ClientDefaultCodecsImpl(ClientDefaultCodecsImpl other) { super(other); - this.multipartCodecs = new DefaultMultipartCodecs(other.multipartCodecs); + this.multipartCodecs = (other.multipartCodecs != null ? + new DefaultMultipartCodecs(other.multipartCodecs) : null); this.sseDecoder = other.sseDecoder; - this.partWritersSupplier = other.partWritersSupplier; } @@ -132,10 +132,8 @@ private static class DefaultMultipartCodecs implements ClientCodecConfigurer.Mul DefaultMultipartCodecs() { } - DefaultMultipartCodecs(@Nullable DefaultMultipartCodecs other) { - if (other != null) { - this.writers.addAll(other.writers); - } + DefaultMultipartCodecs(DefaultMultipartCodecs other) { + this.writers.addAll(other.writers); } 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 737282eecd5..382d11bec8c 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 @@ -16,7 +16,11 @@ package org.springframework.http.codec.support; +import java.util.ArrayList; +import java.util.List; + import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.http.codec.HttpMessageWriter; /** * Default implementation of {@link ClientCodecConfigurer}. @@ -29,11 +33,12 @@ public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements public DefaultClientCodecConfigurer() { super(new ClientDefaultCodecsImpl()); - ((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true)); + ((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(this::getPartWriters); } private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) { super(other); + ((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(this::getPartWriters); } @@ -52,4 +57,14 @@ protected BaseDefaultCodecs cloneDefaultCodecs() { return new ClientDefaultCodecsImpl((ClientDefaultCodecsImpl) defaultCodecs()); } + private List> getPartWriters() { + List> result = new ArrayList<>(); + result.addAll(this.customCodecs.getTypedWriters().keySet()); + result.addAll(this.defaultCodecs.getBaseTypedWriters()); + result.addAll(this.customCodecs.getObjectWriters().keySet()); + result.addAll(this.defaultCodecs.getBaseObjectWriters()); + result.addAll(this.defaultCodecs.getCatchAllWriters()); + return result; + } + } 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 87572c61992..aed7f71db4e 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 @@ -106,8 +106,8 @@ public void defaultWriters() { assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertStringEncoder(getNextEncoder(writers), true); - assertEquals(MultipartHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass()); assertEquals(ProtobufHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); + assertEquals(MultipartHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); @@ -161,7 +161,7 @@ public void enableLoggingRequestDetails() { } @Test - public void cloneConfigurer() { + public void clonedConfigurer() { ClientCodecConfigurer clone = this.configurer.clone(); Jackson2JsonDecoder jackson2Decoder = new Jackson2JsonDecoder(); @@ -186,6 +186,30 @@ public void cloneConfigurer() { assertEquals(10, writers.size()); } + @Test // gh-24194 + public void cloneShouldNotDropMultipartCodecs() { + + ClientCodecConfigurer clone = this.configurer.clone(); + List> writers = + findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); + + assertEquals(10, writers.size()); + } + + @Test + public void cloneShouldNotBeImpactedByChangesToOriginal() { + + ClientCodecConfigurer clone = this.configurer.clone(); + + this.configurer.registerDefaults(false); + this.configurer.customCodecs().register(new Jackson2JsonEncoder()); + + List> writers = + findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); + + assertEquals(10, writers.size()); + } + private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); From afe22b84c27cc0ac20fd87dfbee9536a9216dec3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Dec 2019 16:49:39 +0100 Subject: [PATCH 084/322] ConcurrentReferenceHashMap cache for getInterfaceMethodIfPossible results Closes gh-24206 --- .../org/springframework/util/ClassUtils.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 3303f169f61..f9f7e65d67c 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -110,6 +110,11 @@ public abstract class ClassUtils { */ private static final Set> javaLanguageInterfaces; + /** + * Cache for equivalent methods on an interface implemented by the declaring class. + */ + private static final Map interfaceMethodCache = new ConcurrentReferenceHashMap<>(256); + static { primitiveWrapperTypeMap.put(Boolean.class, boolean.class); @@ -1291,13 +1296,16 @@ public static Method getMostSpecificMethod(Method method, @Nullable Class tar * @see #getMostSpecificMethod */ public static Method getInterfaceMethodIfPossible(Method method) { - if (Modifier.isPublic(method.getModifiers()) && !method.getDeclaringClass().isInterface()) { - Class current = method.getDeclaringClass(); + if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) { + return method; + } + return interfaceMethodCache.computeIfAbsent(method, key -> { + Class current = key.getDeclaringClass(); while (current != null && current != Object.class) { Class[] ifcs = current.getInterfaces(); for (Class ifc : ifcs) { try { - return ifc.getMethod(method.getName(), method.getParameterTypes()); + return ifc.getMethod(key.getName(), key.getParameterTypes()); } catch (NoSuchMethodException ex) { // ignore @@ -1305,8 +1313,8 @@ public static Method getInterfaceMethodIfPossible(Method method) { } current = current.getSuperclass(); } - } - return method; + return key; + }); } /** From 7615e0b0360cb53a1f0b8347b0f55be13da26fa6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Dec 2019 16:50:04 +0100 Subject: [PATCH 085/322] Un-deprecate PathResource (for NIO Path resolution in createRelative) Includes aligned createRelative signature and dedicated java.io.File test. Closes gh-24211 --- .../springframework/core/io/FileSystemResource.java | 12 +++++++++++- .../org/springframework/core/io/PathResource.java | 9 +++++---- .../org/springframework/core/io/ResourceTests.java | 11 ++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java index 776add606db..2d174702e87 100644 --- a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.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,7 @@ * * @author Juergen Hoeller * @since 28.12.2003 + * @see #FileSystemResource(String) * @see #FileSystemResource(File) * @see #FileSystemResource(Path) * @see java.io.File @@ -108,6 +109,15 @@ public FileSystemResource(File file) { *

In contrast to {@link PathResource}, this variant strictly follows the * general {@link FileSystemResource} conventions, in particular in terms of * path cleaning and {@link #createRelative(String)} handling. + *

Note: When building relative resources via {@link #createRelative}, + * the relative path will apply at the same directory level: + * e.g. Paths.get("C:/dir1"), relative path "dir2" -> "C:/dir2"! + * If you prefer to have relative paths built underneath the given root directory, + * use the {@link #FileSystemResource(String) constructor with a file path} + * to append a trailing slash to the root path: "C:/dir1/", which indicates + * this directory as root for all relative paths. Alternatively, consider + * using {@link PathResource#PathResource(Path)} for {@code java.nio.path.Path} + * resolution in {@code createRelative}, always nesting relative paths. * @param filePath a Path handle to a file * @since 5.1 * @see #FileSystemResource(File) diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java index 9e7c150df58..540222e46b7 100644 --- a/spring-core/src/main/java/org/springframework/core/io/PathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.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. @@ -44,15 +44,16 @@ * in {@link FileSystemResource#FileSystemResource(Path) FileSystemResource}, * applying Spring's standard String-based path transformations but * performing all operations via the {@link java.nio.file.Files} API. + * This {@code PathResource} is effectively a pure {@code java.nio.path.Path} + * based alternative with different {@code createRelative} behavior. * * @author Philippe Marschall * @author Juergen Hoeller * @since 4.0 * @see java.nio.file.Path * @see java.nio.file.Files - * @deprecated as of 5.1.1, in favor of {@link FileSystemResource#FileSystemResource(Path)} + * @see FileSystemResource */ -@Deprecated public class PathResource extends AbstractResource implements WritableResource { private final Path path; @@ -252,7 +253,7 @@ public long lastModified() throws IOException { * @see java.nio.file.Path#resolve(String) */ @Override - public Resource createRelative(String relativePath) throws IOException { + public Resource createRelative(String relativePath) { return new PathResource(this.path.resolve(relativePath)); } diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java index be1c77d7838..f6cf5ac468e 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.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. @@ -17,6 +17,7 @@ package org.springframework.core.io; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -129,6 +130,14 @@ public void testFileSystemResource() throws IOException { assertEquals(new FileSystemResource(file), resource); } + @Test + public void fileSystemResourceWithFile() throws IOException { + File file = new File(getClass().getResource("Resource.class").getFile()); + Resource resource = new FileSystemResource(file); + doTestResource(resource); + assertEquals(new FileSystemResource(file), resource); + } + @Test public void testFileSystemResourceWithFilePath() throws Exception { Path filePath = Paths.get(getClass().getResource("Resource.class").toURI()); From f61983d908626a84eef2c0e7b357ffcf7418e2fc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Dec 2019 17:05:48 +0100 Subject: [PATCH 086/322] Polishing --- .../java/org/springframework/cache/CacheReproTests.java | 6 +++--- .../support/converter/MappingJackson2MessageConverter.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index b9aa6f80081..059e1959512 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -60,7 +60,7 @@ public class CacheReproTests { @Test - public void spr11124MultipleAnnotations() throws Exception { + public void spr11124MultipleAnnotations() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11124Config.class); Spr11124Service bean = context.getBean(Spr11124Service.class); bean.single(2); @@ -71,7 +71,7 @@ public void spr11124MultipleAnnotations() throws Exception { } @Test - public void spr11249PrimitiveVarargs() throws Exception { + public void spr11249PrimitiveVarargs() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11249Config.class); Spr11249Service bean = context.getBean(Spr11249Service.class); Object result = bean.doSomething("op", 2, 3); diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java index e12c56d6803..37edf692c66 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java @@ -340,7 +340,7 @@ protected Message mapToMessage(Object object, Session session, ObjectWriter obje * sets the resulting value (either a mapped id or the raw Java class name) * into the configured type id message property. * @param object the payload object to set a type id for - * @param message the JMS Message to set the type id on + * @param message the JMS Message on which to set the type id property * @throws JMSException if thrown by JMS methods * @see #getJavaTypeForMessage(javax.jms.Message) * @see #setTypeIdPropertyName(String) @@ -442,7 +442,7 @@ protected Object convertFromMessage(Message message, JavaType targetJavaType) *

The default implementation parses the configured type id property name * and consults the configured type id mapping. This can be overridden with * a different strategy, e.g. doing some heuristics based on message origin. - * @param message the JMS Message to set the type id on + * @param message the JMS Message from which to get the type id property * @throws JMSException if thrown by JMS methods * @see #setTypeIdOnMessage(Object, javax.jms.Message) * @see #setTypeIdPropertyName(String) From c8ef49cc8ecf7b4f7124e3e3987def5efb08cf9b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Dec 2019 17:06:08 +0100 Subject: [PATCH 087/322] Upgrade to RxJava 2.2.16 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6c7ff94c10c..64b34b1cffb 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ ext { reactorVersion = "Californium-SR14" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" - rxjava2Version = "2.2.15" + rxjava2Version = "2.2.16" slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" tomcatVersion = "9.0.30" From 6ce19ff86138a9dd284588fb62c73af5bc97ec66 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jan 2020 18:18:56 +0000 Subject: [PATCH 088/322] Escape quotes in filename Also sync up with master on refactorings in ContentDisposition and ContentDispositionTests. Closes gh-24224 --- .../http/ContentDisposition.java | 98 ++++--- .../http/ContentDispositionTests.java | 244 ++++++++++-------- 2 files changed, 206 insertions(+), 136 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 93e3bd4cb81..1a6fab102b3 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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,6 +42,10 @@ */ public final class ContentDisposition { + private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT = + "Invalid header field parameter format (as defined in RFC 5987)"; + + @Nullable private final String type; @@ -201,11 +205,11 @@ public String toString() { if (this.filename != null) { if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) { sb.append("; filename=\""); - sb.append(this.filename).append('\"'); + sb.append(escapeQuotationsInFilename(this.filename)).append('\"'); } else { sb.append("; filename*="); - sb.append(encodeHeaderFieldParam(this.filename, this.charset)); + sb.append(encodeFilename(this.filename, this.charset)); } } if (this.size != null) { @@ -271,15 +275,23 @@ public static ContentDisposition parse(String contentDisposition) { String attribute = part.substring(0, eqIndex); String value = (part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ? part.substring(eqIndex + 2, part.length() - 1) : - part.substring(eqIndex + 1, part.length())); + part.substring(eqIndex + 1)); if (attribute.equals("name") ) { name = value; } else if (attribute.equals("filename*") ) { - filename = decodeHeaderFieldParam(value); - charset = Charset.forName(value.substring(0, value.indexOf('\'')).trim()); - Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), - "Charset should be UTF-8 or ISO-8859-1"); + int idx1 = value.indexOf('\''); + int idx2 = value.indexOf('\'', idx1 + 1); + if (idx1 != -1 && idx2 != -1) { + charset = Charset.forName(value.substring(0, idx1).trim()); + Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), + "Charset should be UTF-8 or ISO-8859-1"); + filename = decodeFilename(value.substring(idx2 + 1), charset); + } + else { + // US ASCII + filename = decodeFilename(value, StandardCharsets.US_ASCII); + } } else if (attribute.equals("filename") && (filename == null)) { filename = value; @@ -359,22 +371,15 @@ else if (!escaped && ch == '"') { /** * Decode the given header field param as describe in RFC 5987. *

Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. - * @param input the header field param + * @param filename the header field param + * @param charset the charset to use * @return the encoded header field param * @see RFC 5987 */ - private static String decodeHeaderFieldParam(String input) { - Assert.notNull(input, "Input String should not be null"); - int firstQuoteIndex = input.indexOf('\''); - int secondQuoteIndex = input.indexOf('\'', firstQuoteIndex + 1); - // US_ASCII - if (firstQuoteIndex == -1 || secondQuoteIndex == -1) { - return input; - } - Charset charset = Charset.forName(input.substring(0, firstQuoteIndex).trim()); - Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), - "Charset should be UTF-8 or ISO-8859-1"); - byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset); + private static String decodeFilename(String filename, Charset charset) { + Assert.notNull(filename, "'input' String` should not be null"); + Assert.notNull(charset, "'charset' should not be null"); + byte[] value = filename.getBytes(charset); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int index = 0; while (index < value.length) { @@ -383,13 +388,18 @@ private static String decodeHeaderFieldParam(String input) { bos.write((char) b); index++; } - else if (b == '%') { - char[] array = { (char)value[index + 1], (char)value[index + 2]}; - bos.write(Integer.parseInt(String.valueOf(array), 16)); + else if (b == '%' && index < value.length - 2) { + char[] array = new char[]{(char) value[index + 1], (char) value[index + 2]}; + try { + bos.write(Integer.parseInt(String.valueOf(array), 16)); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT, ex); + } index+=3; } else { - throw new IllegalArgumentException("Invalid header field parameter format (as defined in RFC 5987)"); + throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT); } } return new String(bos.toByteArray(), charset); @@ -401,6 +411,23 @@ private static boolean isRFC5987AttrChar(byte c) { c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; } + private static String escapeQuotationsInFilename(String filename) { + if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { + return filename; + } + boolean escaped = false; + StringBuilder sb = new StringBuilder(); + for (char c : filename.toCharArray()) { + sb.append((c == '"' && !escaped) ? "\\\"" : c); + escaped = (!escaped && c == '\\'); + } + // Remove backslash at the end.. + if (escaped) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + /** * Encode the given header field param as describe in RFC 5987. * @param input the header field param @@ -409,14 +436,11 @@ private static boolean isRFC5987AttrChar(byte c) { * @return the encoded header field param * @see RFC 5987 */ - private static String encodeHeaderFieldParam(String input, Charset charset) { - Assert.notNull(input, "Input String should not be null"); - Assert.notNull(charset, "Charset should not be null"); - if (StandardCharsets.US_ASCII.equals(charset)) { - return input; - } - Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), - "Charset should be UTF-8 or ISO-8859-1"); + private static String encodeFilename(String input, Charset charset) { + Assert.notNull(input, "`input` is required"); + Assert.notNull(charset, "`charset` is required"); + Assert.isTrue(!StandardCharsets.US_ASCII.equals(charset), "ASCII does not require encoding"); + Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Only UTF-8 and ISO-8859-1 supported."); byte[] source = input.getBytes(charset); int len = source.length; StringBuilder sb = new StringBuilder(len << 1); @@ -449,7 +473,11 @@ public interface Builder { Builder name(String name); /** - * Set the value of the {@literal filename} parameter. + * Set the value of the {@literal filename} parameter. The given + * filename will be formatted as quoted-string, as defined in RFC 2616, + * section 2.2, and any quote characters within the filename value will + * be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes + * {@code "foo\\\"bar.txt"}. */ Builder filename(String filename); @@ -530,12 +558,14 @@ public Builder name(String name) { @Override public Builder filename(String filename) { + Assert.hasText(filename, "No filename"); this.filename = filename; return this; } @Override public Builder filename(String filename, Charset charset) { + Assert.hasText(filename, "No filename"); this.filename = filename; this.charset = charset; return this; diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index f195358f63c..e0072e09e46 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,178 +17,218 @@ package org.springframework.http; import java.lang.reflect.Method; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.function.BiConsumer; +import java.util.function.Consumer; -import static org.junit.Assert.assertEquals; import org.junit.Test; import org.springframework.util.ReflectionUtils; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.springframework.http.ContentDisposition.builder; + /** * Unit tests for {@link ContentDisposition} - * * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ public class ContentDispositionTests { - @Test - public void parse() { - ContentDisposition disposition = ContentDisposition - .parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123"); - assertEquals(ContentDisposition.builder("form-data") - .name("foo").filename("foo.txt").size(123L).build(), disposition); - } + private static DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; + @Test - public void parseType() { - ContentDisposition disposition = ContentDisposition.parse("form-data"); - assertEquals(ContentDisposition.builder("form-data").build(), disposition); + public void parse() { + assertEquals(builder("form-data").name("foo").filename("foo.txt").size(123L).build(), + parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123")); } @Test - public void parseUnquotedFilename() { - ContentDisposition disposition = ContentDisposition - .parse("form-data; filename=unquoted"); - assertEquals(ContentDisposition.builder("form-data").filename("unquoted").build(), disposition); + public void parseFilenameUnquoted() { + assertEquals(builder("form-data").filename("unquoted").build(), + parse("form-data; filename=unquoted")); } @Test // SPR-16091 public void parseFilenameWithSemicolon() { - ContentDisposition disposition = ContentDisposition - .parse("attachment; filename=\"filename with ; semicolon.txt\""); - assertEquals(ContentDisposition.builder("attachment") - .filename("filename with ; semicolon.txt").build(), disposition); + assertEquals(builder("attachment").filename("filename with ; semicolon.txt").build(), + parse("attachment; filename=\"filename with ; semicolon.txt\"")); } @Test - public void parseAndIgnoreEmptyParts() { - ContentDisposition disposition = ContentDisposition - .parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123"); - assertEquals(ContentDisposition.builder("form-data") - .name("foo").filename("foo.txt").size(123L).build(), disposition); + public void parseEncodedFilename() { + assertEquals(builder("form-data").name("name").filename("中文.txt", StandardCharsets.UTF_8).build(), + parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt")); } @Test // gh-24112 public void parseEncodedFilenameWithPaddedCharset() { - ContentDisposition disposition = ContentDisposition - .parse("attachment; filename*= UTF-8''some-file.zip"); - assertEquals(ContentDisposition.builder("attachment") - .filename("some-file.zip", StandardCharsets.UTF_8).build(), disposition); + assertEquals(builder("attachment").filename("some-file.zip", StandardCharsets.UTF_8).build(), + parse("attachment; filename*= UTF-8''some-file.zip")); } @Test - public void parseEncodedFilename() { - ContentDisposition disposition = ContentDisposition - .parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"); - assertEquals(ContentDisposition.builder("form-data").name("name") - .filename("中文.txt", StandardCharsets.UTF_8).build(), disposition); + public void parseEncodedFilenameWithoutCharset() { + assertEquals(builder("form-data").name("name").filename("test.txt").build(), + parse("form-data; name=\"name\"; filename*=test.txt")); + } + + @Test(expected = IllegalArgumentException.class) + public void parseEncodedFilenameWithInvalidCharset() { + parse("form-data; name=\"name\"; filename*=UTF-16''test.txt"); + } + + @Test + public void parseEncodedFilenameWithInvalidName() { + + Consumer tester = input -> { + try { + parse(input); + fail(); + } + catch (IllegalArgumentException ex) { + // expected + } + }; + + tester.accept("form-data; name=\"name\"; filename*=UTF-8''%A"); + tester.accept("form-data; name=\"name\"; filename*=UTF-8''%A.txt"); } @Test // gh-23077 public void parseWithEscapedQuote() { - ContentDisposition disposition = ContentDisposition.parse( - "form-data; name=\"file\"; filename=\"\\\"The Twilight Zone\\\".txt\"; size=123"); - assertEquals(ContentDisposition.builder("form-data").name("file") - .filename("\\\"The Twilight Zone\\\".txt").size(123L).build(), disposition); + + BiConsumer tester = (description, filename) -> + assertEquals(description, + builder("form-data").name("file").filename(filename).size(123L).build(), + parse("form-data; name=\"file\"; filename=\"" + filename + "\"; size=123")); + + tester.accept("Escaped quotes should be ignored", + "\\\"The Twilight Zone\\\".txt"); + + tester.accept("Escaped quotes preceded by escaped backslashes should be ignored", + "\\\\\\\"The Twilight Zone\\\\\\\".txt"); + + tester.accept("Escaped backslashes should not suppress quote", + "The Twilight Zone \\\\"); + + tester.accept("Escaped backslashes should not suppress quote", + "The Twilight Zone \\\\\\\\"); + } + + @Test + public void parseWithExtraSemicolons() { + assertEquals(builder("form-data").name("foo").filename("foo.txt").size(123L).build(), + parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123")); + } + + @Test + public void parseDates() { + assertEquals( + builder("attachment") + .creationDate(ZonedDateTime.parse("Mon, 12 Feb 2007 10:15:30 -0500", formatter)) + .modificationDate(ZonedDateTime.parse("Tue, 13 Feb 2007 10:15:30 -0500", formatter)) + .readDate(ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter)).build(), + parse("attachment; creation-date=\"Mon, 12 Feb 2007 10:15:30 -0500\"; " + + "modification-date=\"Tue, 13 Feb 2007 10:15:30 -0500\"; " + + "read-date=\"Wed, 14 Feb 2007 10:15:30 -0500\"")); + } + + @Test + public void parseIgnoresInvalidDates() { + assertEquals( + builder("attachment") + .readDate(ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter)) + .build(), + parse("attachment; creation-date=\"-1\"; " + + "modification-date=\"-1\"; " + + "read-date=\"Wed, 14 Feb 2007 10:15:30 -0500\"")); } @Test(expected = IllegalArgumentException.class) public void parseEmpty() { - ContentDisposition.parse(""); + parse(""); } @Test(expected = IllegalArgumentException.class) public void parseNoType() { - ContentDisposition.parse(";"); + parse(";"); } @Test(expected = IllegalArgumentException.class) public void parseInvalidParameter() { - ContentDisposition.parse("foo;bar"); + parse("foo;bar"); } - @Test - public void parseDates() { - ContentDisposition disposition = ContentDisposition - .parse("attachment; creation-date=\"Mon, 12 Feb 2007 10:15:30 -0500\"; " + - "modification-date=\"Tue, 13 Feb 2007 10:15:30 -0500\"; " + - "read-date=\"Wed, 14 Feb 2007 10:15:30 -0500\""); - DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; - assertEquals(ContentDisposition.builder("attachment") - .creationDate(ZonedDateTime.parse("Mon, 12 Feb 2007 10:15:30 -0500", formatter)) - .modificationDate(ZonedDateTime.parse("Tue, 13 Feb 2007 10:15:30 -0500", formatter)) - .readDate(ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter)).build(), disposition); + private static ContentDisposition parse(String input) { + return ContentDisposition.parse(input); } + @Test - public void parseInvalidDates() { - ContentDisposition disposition = ContentDisposition - .parse("attachment; creation-date=\"-1\"; modification-date=\"-1\"; " + - "read-date=\"Wed, 14 Feb 2007 10:15:30 -0500\""); - DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; - assertEquals(ContentDisposition.builder("attachment") - .readDate(ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter)).build(), disposition); + public void format() { + assertEquals("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123", + builder("form-data").name("foo").filename("foo.txt").size(123L).build().toString()); } @Test - public void headerValue() { - ContentDisposition disposition = ContentDisposition.builder("form-data") - .name("foo").filename("foo.txt").size(123L).build(); - assertEquals("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123", disposition.toString()); + public void formatWithEncodedFilename() { + assertEquals("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt", + builder("form-data").name("name").filename("中文.txt", StandardCharsets.UTF_8).build().toString()); } @Test - public void headerValueWithEncodedFilename() { - ContentDisposition disposition = ContentDisposition.builder("form-data") - .name("name").filename("中文.txt", StandardCharsets.UTF_8).build(); - assertEquals("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt", - disposition.toString()); + public void formatWithEncodedFilenameUsingUsAscii() { + assertEquals("form-data; name=\"name\"; filename=\"test.txt\"", + builder("form-data") + .name("name") + .filename("test.txt", StandardCharsets.US_ASCII) + .build() + .toString()); } - @Test // SPR-14547 - public void encodeHeaderFieldParam() { - Method encode = ReflectionUtils.findMethod(ContentDisposition.class, - "encodeHeaderFieldParam", String.class, Charset.class); - ReflectionUtils.makeAccessible(encode); + @Test // gh-24220 + public void formatWithFilenameWithQuotes() { - String result = (String)ReflectionUtils.invokeMethod(encode, null, "test.txt", - StandardCharsets.US_ASCII); - assertEquals("test.txt", result); + BiConsumer tester = (input, output) -> { - result = (String)ReflectionUtils.invokeMethod(encode, null, "中文.txt", StandardCharsets.UTF_8); - assertEquals("UTF-8''%E4%B8%AD%E6%96%87.txt", result); - } + assertEquals("form-data; filename=\"" + output + "\"", + builder("form-data").filename(input).build().toString()); - @Test(expected = IllegalArgumentException.class) - public void encodeHeaderFieldParamInvalidCharset() { - Method encode = ReflectionUtils.findMethod(ContentDisposition.class, - "encodeHeaderFieldParam", String.class, Charset.class); - ReflectionUtils.makeAccessible(encode); - ReflectionUtils.invokeMethod(encode, null, "test", StandardCharsets.UTF_16); - } + assertEquals("form-data; filename=\"" + output + "\"", + builder("form-data").filename(input, StandardCharsets.US_ASCII).build().toString()); + }; + + String filename = "\"foo.txt"; + tester.accept(filename, "\\" + filename); + + filename = "\\\"foo.txt"; + tester.accept(filename, filename); + + filename = "\\\\\"foo.txt"; + tester.accept(filename, "\\" + filename); + + filename = "\\\\\\\"foo.txt"; + tester.accept(filename, filename); - @Test // SPR-14408 - public void decodeHeaderFieldParam() { - Method decode = ReflectionUtils.findMethod(ContentDisposition.class, - "decodeHeaderFieldParam", String.class); - ReflectionUtils.makeAccessible(decode); + filename = "\\\\\\\\\"foo.txt"; + tester.accept(filename, "\\" + filename); - String result = (String)ReflectionUtils.invokeMethod(decode, null, "test.txt"); - assertEquals("test.txt", result); + tester.accept("\"\"foo.txt", "\\\"\\\"foo.txt"); + tester.accept("\"\"\"foo.txt", "\\\"\\\"\\\"foo.txt"); - result = (String)ReflectionUtils.invokeMethod(decode, null, "UTF-8''%E4%B8%AD%E6%96%87.txt"); - assertEquals("中文.txt", result); + tester.accept("foo.txt\\", "foo.txt"); + tester.accept("foo.txt\\\\", "foo.txt\\\\"); + tester.accept("foo.txt\\\\\\", "foo.txt\\\\"); } @Test(expected = IllegalArgumentException.class) - public void decodeHeaderFieldParamInvalidCharset() { - Method decode = ReflectionUtils.findMethod(ContentDisposition.class, - "decodeHeaderFieldParam", String.class); - ReflectionUtils.makeAccessible(decode); - ReflectionUtils.invokeMethod(decode, null, "UTF-16''test"); + public void formatWithEncodedFilenameUsingInvalidCharset() { + builder("form-data").name("name").filename("test.txt", StandardCharsets.UTF_16).build().toString(); } } From d8abbc501eea59c2dd54529093bb91d4147e7bbf Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 18 Dec 2019 16:48:49 +0000 Subject: [PATCH 089/322] CorsInterceptor skips async dispatch Closes gh-24223 --- .../web/servlet/handler/AbstractHandlerMapping.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index 302e22a2f42..5bb4a53f7d0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -36,6 +36,8 @@ import org.springframework.util.PathMatcher; import org.springframework.web.HttpRequestHandler; import org.springframework.web.context.request.WebRequestInterceptor; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -569,6 +571,12 @@ public CorsInterceptor(@Nullable CorsConfiguration config) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // Consistent with CorsFilter, ignore ASYNC dispatches + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + if (asyncManager.hasConcurrentResult()) { + return true; + } + return corsProcessor.processRequest(this.config, request, response); } From 18d983c686df7b515503536ab4da41cb88556857 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jan 2020 22:05:29 +0000 Subject: [PATCH 090/322] Multi-value headers in ResponseStatusException Closes gh-24284 --- .../web/server/MethodNotAllowedException.java | 23 ++++++++++++---- .../server/NotAcceptableStatusException.java | 22 +++++++++++++--- .../web/server/ResponseStatusException.java | 26 ++++++++++++++++--- .../ResponseStatusExceptionHandler.java | 7 ++--- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java index 14dcd2a62b8..40cf7d46607 100644 --- a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java +++ b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -22,12 +22,12 @@ import java.util.Map; import java.util.Set; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; /** * Exception for errors that fit response status 405 (method not allowed). @@ -62,11 +62,24 @@ public MethodNotAllowedException(String method, @Nullable Collection * Return a Map with an "Allow" header. * @since 5.1.11 */ + @SuppressWarnings("deprecation") @Override public Map getHeaders() { - return !CollectionUtils.isEmpty(this.httpMethods) ? - Collections.singletonMap("Allow", StringUtils.collectionToDelimitedString(this.httpMethods, ", ")) : - Collections.emptyMap(); + return getResponseHeaders().toSingleValueMap(); + } + + /** + * Return HttpHeaders with an "Allow" header. + * @since 5.1.13 + */ + @Override + public HttpHeaders getResponseHeaders() { + if (CollectionUtils.isEmpty(this.httpMethods)) { + return HttpHeaders.EMPTY; + } + HttpHeaders headers = new HttpHeaders(); + headers.setAllow(this.httpMethods); + return headers; } /** diff --git a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java index 3e3a25285b2..3d37d91fa5c 100644 --- a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; @@ -57,11 +58,24 @@ public NotAcceptableStatusException(List supportedMediaTypes) { * Return a Map with an "Accept" header, or an empty map. * @since 5.1.11 */ + @SuppressWarnings("deprecation") @Override public Map getHeaders() { - return !CollectionUtils.isEmpty(this.supportedMediaTypes) ? - Collections.singletonMap("Accept", MediaType.toString(this.supportedMediaTypes)) : - Collections.emptyMap(); + return getResponseHeaders().toSingleValueMap(); + } + + /** + * Return HttpHeaders with an "Accept" header, or an empty instance. + * @since 5.1.13 + */ + @Override + public HttpHeaders getResponseHeaders() { + if (CollectionUtils.isEmpty(this.supportedMediaTypes)) { + return HttpHeaders.EMPTY; + } + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(this.supportedMediaTypes); + return headers; } /** diff --git a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java index a0245686a08..67c8d78391e 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -21,6 +21,7 @@ import org.springframework.core.NestedExceptionUtils; import org.springframework.core.NestedRuntimeException; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -82,14 +83,33 @@ public HttpStatus getStatus() { } /** - * Return response headers associated with the exception, possibly required - * for the given status code (e.g. "Allow", "Accept"). + * Return headers associated with the exception that should be added to the + * error response, e.g. "Allow", "Accept", etc. + *

The default implementation in this class returns an empty map. * @since 5.1.11 + * @deprecated as of 5.1.13 in favor of {@link #getResponseHeaders()} */ + @Deprecated public Map getHeaders() { return Collections.emptyMap(); } + /** + * Return headers associated with the exception that should be added to the + * error response, e.g. "Allow", "Accept", etc. + *

The default implementation in this class returns empty headers. + * @since 5.1.13 + */ + public HttpHeaders getResponseHeaders() { + Map headers = getHeaders(); + if (headers.isEmpty()) { + return HttpHeaders.EMPTY; + } + HttpHeaders result = new HttpHeaders(); + getHeaders().forEach(result::add); + return result; + } + /** * The reason explaining the exception (potentially {@code null} or empty). */ diff --git a/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java b/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java index 263e9cf1afe..cda26cba213 100644 --- a/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java +++ b/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -92,8 +92,9 @@ private boolean updateResponse(ServerHttpResponse response, Throwable ex) { if (status != null) { if (response.setStatusCode(status)) { if (ex instanceof ResponseStatusException) { - ((ResponseStatusException) ex).getHeaders() - .forEach((name, value) -> response.getHeaders().add(name, value)); + ((ResponseStatusException) ex).getResponseHeaders() + .forEach((name, values) -> + values.forEach(value -> response.getHeaders().add(name, value))); } result = true; } From d0e3e2acfc88516bc0d40f265b4c786146fb5d86 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 8 Jan 2020 18:37:07 +0100 Subject: [PATCH 091/322] Thread-safe compiled expression evaluation in SpelExpression Closes gh-24265 --- .../spel/standard/SpelExpression.java | 120 ++++++++++-------- 1 file changed, 70 insertions(+), 50 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java index e4a2ac455b8..139ba5b8c7c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -16,6 +16,8 @@ package org.springframework.expression.spel.standard; +import java.util.concurrent.atomic.AtomicInteger; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; @@ -65,15 +67,15 @@ public class SpelExpression implements Expression { // Holds the compiled form of the expression (if it has been compiled) @Nullable - private CompiledExpression compiledAst; + private volatile CompiledExpression compiledAst; // Count of many times as the expression been interpreted - can trigger compilation // when certain limit reached - private volatile int interpretedCount = 0; + private final AtomicInteger interpretedCount = new AtomicInteger(0); // The number of times compilation was attempted and failed - enables us to eventually // give up trying to compile it when it just doesn't seem to be possible. - private volatile int failedAttempts = 0; + private final AtomicInteger failedAttempts = new AtomicInteger(0); /** @@ -116,16 +118,17 @@ public String getExpressionString() { @Override @Nullable public Object getValue() throws EvaluationException { - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { EvaluationContext context = getEvaluationContext(); - return this.compiledAst.getValue(context.getRootObject().getValue(), context); + return compiledAst.getValue(context.getRootObject().getValue(), context); } catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -144,10 +147,11 @@ public Object getValue() throws EvaluationException { @Override @Nullable public T getValue(@Nullable Class expectedResultType) throws EvaluationException { - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { EvaluationContext context = getEvaluationContext(); - Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context); + Object result = compiledAst.getValue(context.getRootObject().getValue(), context); if (expectedResultType == null) { return (T) result; } @@ -159,8 +163,8 @@ public T getValue(@Nullable Class expectedResultType) throws EvaluationEx catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -179,15 +183,16 @@ public T getValue(@Nullable Class expectedResultType) throws EvaluationEx @Override @Nullable public Object getValue(Object rootObject) throws EvaluationException { - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { - return this.compiledAst.getValue(rootObject, getEvaluationContext()); + return compiledAst.getValue(rootObject, getEvaluationContext()); } catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -207,9 +212,10 @@ public Object getValue(Object rootObject) throws EvaluationException { @Override @Nullable public T getValue(Object rootObject, @Nullable Class expectedResultType) throws EvaluationException { - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { - Object result = this.compiledAst.getValue(rootObject, getEvaluationContext()); + Object result = compiledAst.getValue(rootObject, getEvaluationContext()); if (expectedResultType == null) { return (T)result; } @@ -221,8 +227,8 @@ public T getValue(Object rootObject, @Nullable Class expectedResultType) catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -244,15 +250,16 @@ public T getValue(Object rootObject, @Nullable Class expectedResultType) public Object getValue(EvaluationContext context) throws EvaluationException { Assert.notNull(context, "EvaluationContext is required"); - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { - return this.compiledAst.getValue(context.getRootObject().getValue(), context); + return compiledAst.getValue(context.getRootObject().getValue(), context); } catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -273,9 +280,10 @@ public Object getValue(EvaluationContext context) throws EvaluationException { public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { Assert.notNull(context, "EvaluationContext is required"); - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { - Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context); + Object result = compiledAst.getValue(context.getRootObject().getValue(), context); if (expectedResultType != null) { return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType); } @@ -286,8 +294,8 @@ public T getValue(EvaluationContext context, @Nullable Class expectedResu catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -307,15 +315,16 @@ public T getValue(EvaluationContext context, @Nullable Class expectedResu public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { Assert.notNull(context, "EvaluationContext is required"); - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { - return this.compiledAst.getValue(rootObject, context); + return compiledAst.getValue(rootObject, context); } catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -338,9 +347,10 @@ public T getValue(EvaluationContext context, Object rootObject, @Nullable Cl Assert.notNull(context, "EvaluationContext is required"); - if (this.compiledAst != null) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { try { - Object result = this.compiledAst.getValue(rootObject, context); + Object result = compiledAst.getValue(rootObject, context); if (expectedResultType != null) { return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType); } @@ -351,8 +361,8 @@ public T getValue(EvaluationContext context, Object rootObject, @Nullable Cl catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { - this.interpretedCount = 0; this.compiledAst = null; + this.interpretedCount.set(0); } else { // Running in SpelCompilerMode.immediate mode - propagate exception to caller @@ -473,48 +483,58 @@ public void setValue(EvaluationContext context, Object rootObject, @Nullable Obj * @param expressionState the expression state used to determine compilation mode */ private void checkCompile(ExpressionState expressionState) { - this.interpretedCount++; + this.interpretedCount.incrementAndGet(); SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode(); if (compilerMode != SpelCompilerMode.OFF) { if (compilerMode == SpelCompilerMode.IMMEDIATE) { - if (this.interpretedCount > 1) { + if (this.interpretedCount.get() > 1) { compileExpression(); } } else { // compilerMode = SpelCompilerMode.MIXED - if (this.interpretedCount > INTERPRETED_COUNT_THRESHOLD) { + if (this.interpretedCount.get() > INTERPRETED_COUNT_THRESHOLD) { compileExpression(); } } } } - /** - * Perform expression compilation. This will only succeed once exit descriptors for all nodes have - * been determined. If the compilation fails and has failed more than 100 times the expression is - * no longer considered suitable for compilation. + * Perform expression compilation. This will only succeed once exit descriptors for + * all nodes have been determined. If the compilation fails and has failed more than + * 100 times the expression is no longer considered suitable for compilation. + * @return whether this expression has been successfully compiled */ public boolean compileExpression() { - if (this.failedAttempts > FAILED_ATTEMPTS_THRESHOLD) { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + // Previously compiled + return true; + } + if (this.failedAttempts.get() > FAILED_ATTEMPTS_THRESHOLD) { // Don't try again return false; } - if (this.compiledAst == null) { - synchronized (this.expression) { - // Possibly compiled by another thread before this thread got into the sync block - if (this.compiledAst != null) { - return true; - } - SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader()); - this.compiledAst = compiler.compile(this.ast); - if (this.compiledAst == null) { - this.failedAttempts++; - } + + synchronized (this) { + if (this.compiledAst != null) { + // Compiled by another thread before this thread got into the sync block + return true; + } + SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader()); + compiledAst = compiler.compile(this.ast); + if (compiledAst != null) { + // Successfully compiled + this.compiledAst = compiledAst; + return true; + } + else { + // Failed to compile + this.failedAttempts.incrementAndGet(); + return false; } } - return (this.compiledAst != null); } /** @@ -524,8 +544,8 @@ public boolean compileExpression() { */ public void revertToInterpreted() { this.compiledAst = null; - this.interpretedCount = 0; - this.failedAttempts = 0; + this.interpretedCount.set(0); + this.failedAttempts.set(0); } /** From b9bf56e41dff2324c4c0b736c25feead409a349e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 9 Jan 2020 16:16:36 +0100 Subject: [PATCH 092/322] Polishing --- .../org/springframework/cache/interceptor/SimpleKey.java | 4 +++- .../export/assembler/InterfaceBasedMBeanInfoAssembler.java | 5 +++-- .../cache/interceptor/SimpleKeyGeneratorTests.java | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java index 15928028084..a7668db559f 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java @@ -32,7 +32,9 @@ @SuppressWarnings("serial") public class SimpleKey implements Serializable { - /** An empty key. */ + /** + * An empty key. + */ public static final SimpleKey EMPTY = new SimpleKey(); diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java index b7d3186ee81..72bbaa32423 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -207,7 +207,7 @@ protected boolean includeOperation(Method method, String beanKey) { * configured interfaces and is public, otherwise {@code false}. */ private boolean isPublicInInterface(Method method, String beanKey) { - return ((method.getModifiers() & Modifier.PUBLIC) > 0) && isDeclaredInInterface(method, beanKey); + return Modifier.isPublic(method.getModifiers()) && isDeclaredInInterface(method, beanKey); } /** @@ -231,6 +231,7 @@ private boolean isDeclaredInInterface(Method method, String beanKey) { for (Class ifc : ifaces) { for (Method ifcMethod : ifc.getMethods()) { if (ifcMethod.getName().equals(method.getName()) && + ifcMethod.getParameterCount() == method.getParameterCount() && Arrays.equals(ifcMethod.getParameterTypes(), method.getParameterTypes())) { return true; } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java index 523a203230e..ba6caea68ed 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java @@ -44,7 +44,7 @@ public void noValues() { } @Test - public void singleValue(){ + public void singleValue() { Object k1 = generateKey(new Object[] { "a" }); Object k2 = generateKey(new Object[] { "a" }); Object k3 = generateKey(new Object[] { "different" }); @@ -56,7 +56,7 @@ public void singleValue(){ } @Test - public void multipleValues() { + public void multipleValues() { Object k1 = generateKey(new Object[] { "a", 1, "b" }); Object k2 = generateKey(new Object[] { "a", 1, "b" }); Object k3 = generateKey(new Object[] { "b", 1, "a" }); From 7f49abb6d14a68337909b4a32b295d2676bd1009 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 9 Jan 2020 16:17:33 +0100 Subject: [PATCH 093/322] Upgrade to Netty 4.1.44, Jetty 9.4.25, Undertow 2.0.29, OkHttp 3.14.5 --- build.gradle | 6 +++--- spring-web/spring-web.gradle | 4 ++-- spring-webflux/spring-webflux.gradle | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 64b34b1cffb..53b29951667 100644 --- a/build.gradle +++ b/build.gradle @@ -33,11 +33,11 @@ ext { groovyVersion = "2.5.8" hsqldbVersion = "2.4.1" jackson2Version = "2.9.9" - jettyVersion = "9.4.24.v20191120" + jettyVersion = "9.4.25.v20191220" junit5Version = "5.3.2" kotlinVersion = "1.2.71" log4jVersion = "2.11.2" - nettyVersion = "4.1.43.Final" + nettyVersion = "4.1.44.Final" reactorVersion = "Californium-SR14" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" @@ -45,7 +45,7 @@ ext { slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" tomcatVersion = "9.0.30" - undertowVersion = "2.0.28.Final" + undertowVersion = "2.0.29.Final" gradleScriptDir = "${rootProject.projectDir}/gradle" withoutJclOverSlf4j = { diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index d735f4a333e..9913a6c5ac7 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -38,7 +38,7 @@ dependencies { exclude group: "javax.servlet", module: "javax.servlet-api" } optional("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3") - optional("com.squareup.okhttp3:okhttp:3.14.4") + optional("com.squareup.okhttp3:okhttp:3.14.5") optional("org.apache.httpcomponents:httpclient:4.5.10") { exclude group: "commons-logging", module: "commons-logging" } @@ -75,7 +75,7 @@ dependencies { testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") testCompile("org.eclipse.jetty:jetty-server") testCompile("org.eclipse.jetty:jetty-servlet") - testCompile("com.squareup.okhttp3:mockwebserver:3.14.4") + testCompile("com.squareup.okhttp3:mockwebserver:3.14.5") testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") testCompile("org.skyscreamer:jsonassert:1.5.0") testCompile("org.xmlunit:xmlunit-matchers:2.6.2") diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 46575ca753f..2afb6e4639f 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -51,7 +51,7 @@ dependencies { testCompile("org.eclipse.jetty:jetty-server") testCompile("org.eclipse.jetty:jetty-servlet") testCompile("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3") - testCompile("com.squareup.okhttp3:mockwebserver:3.14.4") + testCompile("com.squareup.okhttp3:mockwebserver:3.14.5") testCompile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-script-util:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}") From 6df8c2678b2f79f99a1fc8de50a47103684882de Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 9 Jan 2020 17:13:16 +0100 Subject: [PATCH 094/322] Upgrade to SLF4J 1.7.30 and Checkstyle 8.28 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 53b29951667..297a989c4b7 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ ext { rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" rxjava2Version = "2.2.16" - slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps + slf4jVersion = "1.7.30" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" tomcatVersion = "9.0.30" undertowVersion = "2.0.29.Final" @@ -142,7 +142,7 @@ configure(allprojects) { project -> } checkstyle { - toolVersion = "8.27" + toolVersion = "8.28" configDir = rootProject.file("src/checkstyle") } From 98390e1e5e172a5ef6954f056bb4a51c7da4e318 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 10 Jan 2020 15:59:59 +0100 Subject: [PATCH 095/322] Copy strategiesConfigurers when cloning WebClient.Builder This commit fixes the missing `strategiesConfigurers` copy when the `WebClient.Builder` is cloned. Fixes gh-24330 See gh-24329 --- .../web/reactive/function/client/DefaultWebClientBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c255c439d32..dfd1087598b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -114,6 +114,7 @@ public DefaultWebClientBuilder(DefaultWebClientBuilder other) { this.filters = other.filters != null ? new ArrayList<>(other.filters) : null; this.connector = other.connector; this.strategies = other.strategies; + this.strategiesConfigurers = other.strategiesConfigurers != null ? new ArrayList<>(other.strategiesConfigurers) : null; this.exchangeFunction = other.exchangeFunction; } From 170adccf806ebd0da94631e28f9a7d7fc7ed3be5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 13 Jan 2020 10:58:51 +0100 Subject: [PATCH 096/322] Upgrade to RxJava 2.2.17, OkHttp 3.14.6, Hibernate ORM 5.3.15 --- build.gradle | 2 +- spring-orm/spring-orm.gradle | 2 +- spring-test/spring-test.gradle | 2 +- spring-web/spring-web.gradle | 4 ++-- spring-webflux/spring-webflux.gradle | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 297a989c4b7..c473286d719 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ ext { reactorVersion = "Californium-SR14" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" - rxjava2Version = "2.2.16" + rxjava2Version = "2.2.17" slf4jVersion = "1.7.30" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" tomcatVersion = "9.0.30" diff --git a/spring-orm/spring-orm.gradle b/spring-orm/spring-orm.gradle index 99de34aa6c7..6cf034be1a0 100644 --- a/spring-orm/spring-orm.gradle +++ b/spring-orm/spring-orm.gradle @@ -9,7 +9,7 @@ dependencies { optional(project(":spring-context")) optional(project(":spring-web")) optional("org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.4") - optional("org.hibernate:hibernate-core:5.3.14.Final") + optional("org.hibernate:hibernate-core:5.3.15.Final") optional("javax.servlet:javax.servlet-api:3.1.0") testCompile("org.aspectj:aspectjweaver:${aspectjVersion}") testCompile("org.hsqldb:hsqldb:${hsqldbVersion}") diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index 621ab4dec42..fa6ed7ee7ef 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -60,7 +60,7 @@ dependencies { testCompile("javax.ejb:javax.ejb-api:3.2") testCompile("javax.interceptor:javax.interceptor-api:1.2.2") testCompile("javax.mail:javax.mail-api:1.6.2") - testCompile("org.hibernate:hibernate-core:5.3.12.Final") + testCompile("org.hibernate:hibernate-core:5.3.15.Final") testCompile("org.hibernate:hibernate-validator:6.0.18.Final") // Enable use of the JUnit Platform Runner testCompile("org.junit.platform:junit-platform-runner") diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 9913a6c5ac7..bf3d6ed5690 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -38,7 +38,7 @@ dependencies { exclude group: "javax.servlet", module: "javax.servlet-api" } optional("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3") - optional("com.squareup.okhttp3:okhttp:3.14.5") + optional("com.squareup.okhttp3:okhttp:3.14.6") optional("org.apache.httpcomponents:httpclient:4.5.10") { exclude group: "commons-logging", module: "commons-logging" } @@ -75,7 +75,7 @@ dependencies { testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") testCompile("org.eclipse.jetty:jetty-server") testCompile("org.eclipse.jetty:jetty-servlet") - testCompile("com.squareup.okhttp3:mockwebserver:3.14.5") + testCompile("com.squareup.okhttp3:mockwebserver:3.14.6") testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") testCompile("org.skyscreamer:jsonassert:1.5.0") testCompile("org.xmlunit:xmlunit-matchers:2.6.2") diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 2afb6e4639f..ba3e6b07e70 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -51,7 +51,7 @@ dependencies { testCompile("org.eclipse.jetty:jetty-server") testCompile("org.eclipse.jetty:jetty-servlet") testCompile("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3") - testCompile("com.squareup.okhttp3:mockwebserver:3.14.5") + testCompile("com.squareup.okhttp3:mockwebserver:3.14.6") testCompile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-script-util:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}") From f2c364b49163ba8e20c3168e5bab460a3b3bb1b4 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 13 Jan 2020 13:16:50 +0100 Subject: [PATCH 097/322] Update ASM and CBLIB versions in license.txt --- src/docs/dist/license.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/docs/dist/license.txt b/src/docs/dist/license.txt index 1a517dfdf79..8076a80c39b 100644 --- a/src/docs/dist/license.txt +++ b/src/docs/dist/license.txt @@ -212,7 +212,7 @@ code for these subcomponents is subject to the terms and conditions of the following licenses. ->>> ASM 4.0 (org.ow2.asm:asm:4.0, org.ow2.asm:asm-commons:4.0): +>>> ASM 7.1 (org.ow2.asm:asm:7.1, org.ow2.asm:asm-commons:7.1): Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. @@ -247,12 +247,12 @@ THE POSSIBILITY OF SUCH DAMAGE. Copyright (c) 1999-2009, OW2 Consortium ->>> CGLIB 3.0 (cglib:cglib:3.0): +>>> CGLIB 3.2.11 (cglib:cglib:3.2.11): Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download, -CGLIB 3.0 is licensed under the Apache License, version 2.0, the text of which -is included above. +https://github.com/cglib/cglib/releases/download/RELEASE_3_2_11/cglib-3.2.11.jar, +CGLIB 3.2.11 is licensed under the Apache License, version 2.0, the text of +which is included above. =============================================================================== From c91b47fded67403f47fafd56203dcf5aedb533cf Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 13 Jan 2020 14:41:52 +0100 Subject: [PATCH 098/322] Document Objenesis license in license.txt Closes gh-24340 --- src/docs/dist/license.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/docs/dist/license.txt b/src/docs/dist/license.txt index 8076a80c39b..eca912cd2d8 100644 --- a/src/docs/dist/license.txt +++ b/src/docs/dist/license.txt @@ -255,6 +255,13 @@ CGLIB 3.2.11 is licensed under the Apache License, version 2.0, the text of which is included above. +>>> Objenesis 3.0.1 (org.objenesis:objenesis:3.0.1): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.0.1 is licensed under the +Apache License, version 2.0, the text of which is included above. + + =============================================================================== To the extent any open source components are licensed under the EPL and/or From 04b3f5a247e1d89df0f12b6a176eb2ed522754be Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 13 Jan 2020 17:09:38 +0000 Subject: [PATCH 099/322] Upgrade to Reactor Californium SR15 Closes gh-24345 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c473286d719..87ad6abb1c8 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ ext { kotlinVersion = "1.2.71" log4jVersion = "2.11.2" nettyVersion = "4.1.44.Final" - reactorVersion = "Californium-SR14" + reactorVersion = "Californium-SR15" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" rxjava2Version = "2.2.17" From 9969cb6d83916a8e8a68885f3b32cd89c42b483c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 13 Jan 2020 20:46:51 +0000 Subject: [PATCH 100/322] Improve limit handling in StringDecoder The case of one data buffer containing multiple lines can could cause a buffer leak due to a suspected issue in concatMapIterable. This commit adds workarounds for that until the underlying issue is addressed. Closes gh-24346 --- .../core/codec/StringDecoder.java | 100 ++++++++++++------ .../core/codec/StringDecoderTests.java | 31 ++++-- 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java index c3100490dde..0674cc5aa2c 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -95,17 +95,44 @@ public Flux decode(Publisher input, ResolvableType elementTy List delimiterBytes = getDelimiterBytes(mimeType); - // TODO: Drop Consumer and use bufferUntil with Supplier (reactor-core#1925) - // TODO: Drop doOnDiscard(LimitedDataBufferList.class, ...) (reactor-core#1924) - LimitedDataBufferConsumer limiter = new LimitedDataBufferConsumer(getMaxInMemorySize()); + Flux inputFlux = Flux.defer(() -> { + if (getMaxInMemorySize() != -1) { - Flux inputFlux = Flux.from(input) - .flatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes)) - .doOnNext(limiter) - .bufferUntil(buffer -> buffer == END_FRAME) - .map(StringDecoder::joinUntilEndFrame) - .doOnDiscard(LimitedDataBufferList.class, LimitedDataBufferList::releaseAndClear) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + // Passing limiter into endFrameAfterDelimiter helps to ensure that in case of one DataBuffer + // containing multiple lines, the limit is checked and raised immediately without accumulating + // subsequent lines. This is necessary because concatMapIterable doesn't respect doOnDiscard. + // When reactor-core#1925 is resolved, we could replace bufferUntil with: + + // .windowUntil(buffer -> buffer instanceof EndFrameBuffer) + // .concatMap(fluxes -> fluxes.collect(() -> new LimitedDataBufferList(getMaxInMemorySize()), LimitedDataBufferList::add)) + + LimitedDataBufferList limiter = new LimitedDataBufferList(getMaxInMemorySize()); + + return Flux.from(input) + .concatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes, limiter)) + .bufferUntil(buffer -> buffer == END_FRAME) + .map(StringDecoder::joinUntilEndFrame) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + } + else { + + // When the decoder is unlimited (-1), concatMapIterable will cache buffers that may not + // be released if cancel is signalled before they are turned into String lines + // (see test maxInMemoryLimitReleasesUnprocessedLinesWhenUnlimited). + // When reactor-core#1925 is resolved, the workaround can be removed and the entire + // else clause possibly dropped. + + ConcatMapIterableDiscardWorkaroundCache cache = new ConcatMapIterableDiscardWorkaroundCache(); + + return Flux.from(input) + .concatMapIterable(buffer -> cache.addAll(splitOnDelimiter(buffer, delimiterBytes, null))) + .doOnNext(cache) + .doOnCancel(cache) + .bufferUntil(buffer -> buffer == END_FRAME) + .map(StringDecoder::joinUntilEndFrame) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + } + }); return super.decode(inputFlux, elementType, mimeType, hints); } @@ -125,7 +152,9 @@ private List getDelimiterBytes(@Nullable MimeType mimeType) { * Split the given data buffer on delimiter boundaries. * The returned Flux contains an {@link #END_FRAME} buffer after each delimiter. */ - private List splitOnDelimiter(DataBuffer buffer, List delimiterBytes) { + private List splitOnDelimiter( + DataBuffer buffer, List delimiterBytes, @Nullable LimitedDataBufferList limiter) { + List frames = new ArrayList<>(); try { do { @@ -147,15 +176,28 @@ private List splitOnDelimiter(DataBuffer buffer, List delimi buffer.readPosition(readPosition + length + matchingDelimiter.length); frames.add(DataBufferUtils.retain(frame)); frames.add(END_FRAME); + if (limiter != null) { + limiter.add(frame); // enforce the limit + limiter.clear(); + } } else { frame = buffer.slice(readPosition, buffer.readableByteCount()); buffer.readPosition(readPosition + buffer.readableByteCount()); frames.add(DataBufferUtils.retain(frame)); + if (limiter != null) { + limiter.add(frame); + } } } while (buffer.readableByteCount() > 0); } + catch (DataBufferLimitException ex) { + if (limiter != null) { + limiter.releaseAndClear(); + } + throw ex; + } catch (Throwable ex) { for (DataBuffer frame : frames) { DataBufferUtils.release(frame); @@ -293,34 +335,32 @@ public static StringDecoder allMimeTypes(List delimiters, boolean stripD } - /** - * Temporary measure for reactor-core#1925. - * Consumer that adds to a {@link LimitedDataBufferList} to enforce limits. - */ - private static class LimitedDataBufferConsumer implements Consumer { + private class ConcatMapIterableDiscardWorkaroundCache implements Consumer, Runnable { - private final LimitedDataBufferList bufferList; + private final List buffers = new ArrayList<>(); - public LimitedDataBufferConsumer(int maxInMemorySize) { - this.bufferList = new LimitedDataBufferList(maxInMemorySize); + public List addAll(List buffersToAdd) { + this.buffers.addAll(buffersToAdd); + return buffersToAdd; } + @Override + public void accept(DataBuffer dataBuffer) { + this.buffers.remove(dataBuffer); + } @Override - public void accept(DataBuffer buffer) { - if (buffer == END_FRAME) { - this.bufferList.clear(); - } - else { + public void run() { + this.buffers.forEach(buffer -> { try { - this.bufferList.add(buffer); - } - catch (DataBufferLimitException ex) { DataBufferUtils.release(buffer); - throw ex; } - } + catch (Throwable ex) { + // Keep going.. + } + }); } } + } diff --git a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java index b584080af23..ef43be54b16 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -128,17 +128,30 @@ public void decodeNewLine() { } @Test - public void decodeNewLineWithLimit() { + public void maxInMemoryLimit() { Flux input = Flux.just( - stringBuffer("abc\n"), - stringBuffer("defg\n"), - stringBuffer("hijkl\n") - ); - this.decoder.setMaxInMemorySize(4); + stringBuffer("abc\n"), stringBuffer("defg\n"), stringBuffer("hijkl\n")); + this.decoder.setMaxInMemorySize(4); testDecode(input, String.class, step -> - step.expectNext("abc", "defg") - .verifyError(DataBufferLimitException.class)); + step.expectNext("abc", "defg").verifyError(DataBufferLimitException.class)); + } + + @Test // gh-24312 + public void maxInMemoryLimitReleaseUnprocessedLinesFromCurrentBuffer() { + Flux input = Flux.just( + stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n")); + + this.decoder.setMaxInMemorySize(5); + testDecode(input, String.class, step -> step.verifyError(DataBufferLimitException.class)); + } + + @Test // gh-24339 + public void maxInMemoryLimitReleaseUnprocessedLinesWhenUnlimited() { + Flux input = Flux.just(stringBuffer("Line 1\nLine 2\nLine 3\n")); + + this.decoder.setMaxInMemorySize(-1); + testDecodeCancel(input, ResolvableType.forClass(String.class), null, Collections.emptyMap()); } @Test From b62e066b7d698d0517bc50ddef27e86c4579b7ed Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Tue, 14 Jan 2020 07:37:59 +0000 Subject: [PATCH 101/322] Next Development Version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 47ad59cda14..8f32a16e094 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.1.13.BUILD-SNAPSHOT +version=5.1.14.BUILD-SNAPSHOT From 095acefd7efec268fc16a9f26201f005169a2c40 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 15 Jan 2020 14:18:30 +0100 Subject: [PATCH 102/322] Include Objenesis NOTICE file contents in binary distributions Closes gh-24326 --- src/docs/dist/license.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/docs/dist/license.txt b/src/docs/dist/license.txt index eca912cd2d8..2b52bb0ffe7 100644 --- a/src/docs/dist/license.txt +++ b/src/docs/dist/license.txt @@ -261,6 +261,13 @@ Per the LICENSE file in the Objenesis ZIP distribution downloaded from http://objenesis.org/download.html, Objenesis 3.0.1 is licensed under the Apache License, version 2.0, the text of which is included above. +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2018 Joe Walnes, Henri Tremblay, Leonardo Mesquita + =============================================================================== From 6699833121975ac94bf458cab4aa3b2e2098772e Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 16 Jan 2020 16:34:21 +0100 Subject: [PATCH 103/322] Introduce regression test for gh-24375 --- ...anningCandidateComponentProviderTests.java | 12 ++++++- .../annotation/componentscan/gh24375/A.java | 35 +++++++++++++++++++ .../annotation/componentscan/gh24375/B.java | 29 +++++++++++++++ .../componentscan/gh24375/MyComponent.java | 24 +++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/A.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/B.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/MyComponent.java diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index 0f761cca2f1..197dba38cfa 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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,6 +42,7 @@ import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.componentscan.gh24375.MyComponent; import org.springframework.context.index.CandidateComponentsTestClassLoader; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; @@ -501,6 +502,15 @@ public void testIntegrationWithAnnotationConfigApplicationContext_metaProfile() } } + @Test + public void gh24375() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + Set components = provider.findCandidateComponents(MyComponent.class.getPackage().getName()); + assertEquals(1, components.size()); + assertEquals(MyComponent.class.getName(), components.iterator().next().getBeanClassName()); + } + + private boolean containsBeanClass(Set candidates, Class beanClass) { for (BeanDefinition candidate : candidates) { if (beanClass.getName().equals(candidate.getBeanClassName())) { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/A.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/A.java new file mode 100644 index 00000000000..195b5bc4d18 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/A.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.componentscan.gh24375; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface A { + + @AliasFor("value") + B other() default @B; + + @AliasFor("other") + B value() default @B; +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/B.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/B.java new file mode 100644 index 00000000000..f1fdfb24de0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/B.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.componentscan.gh24375; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface B { + + String name() default ""; +} \ No newline at end of file diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/MyComponent.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/MyComponent.java new file mode 100644 index 00000000000..f51b9d37c48 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/gh24375/MyComponent.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.componentscan.gh24375; + +import org.springframework.stereotype.Component; + +@Component +@A(other = @B) +public class MyComponent { +} From e1e8c165db8d1866d11ed4e51b5abfb7240024cd Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 17 Jan 2020 15:01:58 +0100 Subject: [PATCH 104/322] Avoid setting special Content-* response headers for Tomcat As of gh-21783, Spring WebFlux uses a `TomcatHeadersAdapter` implementation to directly address the native headers used by the server. In the case of Tomcat, "Content-Length" and "Content-Type" headers are processed separately and should not be added to the native headers map. This commit improves the `HandlerAdapter` implementation for Tomcat and removes those headers, if previously set in the map. The adapter already has a section that handles the Tomcat-specific calls for such headers. Fixes gh-24387 --- .../http/server/reactive/TomcatHttpHandlerAdapter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java index 055869ed0bb..c01a11b5a7e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java @@ -216,6 +216,7 @@ protected void applyHeaders() { if (response.getContentType() == null && contentType != null) { response.setContentType(contentType.toString()); } + getHeaders().remove(HttpHeaders.CONTENT_TYPE); Charset charset = (contentType != null ? contentType.getCharset() : null); if (response.getCharacterEncoding() == null && charset != null) { response.setCharacterEncoding(charset.name()); @@ -224,6 +225,7 @@ protected void applyHeaders() { if (contentLength != -1) { response.setContentLengthLong(contentLength); } + getHeaders().remove(HttpHeaders.CONTENT_LENGTH); } @Override From 6bae75934921b23daf133f6a87cb18421b477d4a Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 17 Jan 2020 13:43:29 +0100 Subject: [PATCH 105/322] Use Jackson SequenceWriter for streaming Before this commit, the AbstractJackson2Encoder instantiated a ObjectWriter per value. This is not an issue for single values or non-streaming scenarios (which effectively are the same, because in the latter values are collected into a list until offered to Jackson). However, this does create a problem for SMILE, because it allows for shared references that do not match up when writing each value with a new ObjectWriter, resulting in errors parsing the result. This commit uses Jackson's SequenceWriter for streaming scenarios, allowing Jackson to reuse the same context for writing multiple values, fixing the issue described above. Closes gh-24198 --- .../codec/json/AbstractJackson2Encoder.java | 171 ++++++++++++------ .../codec/json/Jackson2SmileEncoderTests.java | 90 +++++---- 2 files changed, 160 insertions(+), 101 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index 68841a93f14..d7274fbeb42 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,7 +17,6 @@ package org.springframework.http.codec.json; import java.io.IOException; -import java.io.OutputStream; import java.lang.annotation.Annotation; import java.nio.charset.Charset; import java.util.ArrayList; @@ -29,9 +28,11 @@ import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.ByteArrayBuilder; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SequenceWriter; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -44,7 +45,6 @@ import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageEncoder; @@ -115,65 +115,53 @@ public Flux encode(Publisher inputStream, DataBufferFactory buffe Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); Assert.notNull(elementType, "'elementType' must not be null"); - JsonEncoding encoding = getJsonEncoding(mimeType); - if (inputStream instanceof Mono) { - return Mono.from(inputStream).map(value -> - encodeValue(value, mimeType, bufferFactory, elementType, hints, encoding)).flux(); + return Mono.from(inputStream) + .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) + .flux(); } else { - return this.streamingMediaTypes.stream() - .filter(mediaType -> mediaType.isCompatibleWith(mimeType)) - .findFirst() - .map(mediaType -> { - byte[] separator = STREAM_SEPARATORS.getOrDefault(mediaType, NEWLINE_SEPARATOR); - return Flux.from(inputStream).map(value -> { - DataBuffer buffer = encodeValue( - value, mimeType, bufferFactory, elementType, hints, encoding); - if (separator != null) { - buffer.write(separator); - } - return buffer; - }); - }) - .orElseGet(() -> { - ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); - return Flux.from(inputStream).collectList().map(list -> - encodeValue(list, mimeType, bufferFactory, listType, hints, encoding)).flux(); - }); - } - } - - private DataBuffer encodeValue(Object value, @Nullable MimeType mimeType, DataBufferFactory bufferFactory, - ResolvableType elementType, @Nullable Map hints, JsonEncoding encoding) { + byte[] separator = streamSeparator(mimeType); + if (separator != null) { // streaming + try { + ObjectWriter writer = createObjectWriter(elementType, mimeType, hints); + ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler()); + JsonEncoding encoding = getJsonEncoding(mimeType); + JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding); + SequenceWriter sequenceWriter = writer.writeValues(generator); + + return Flux.from(inputStream) + .map(value -> encodeStreamingValue(value, bufferFactory, hints, sequenceWriter, byteBuilder, + separator)); + } + catch (IOException ex) { + return Flux.error(ex); + } + } + else { // non-streaming + ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); + return Flux.from(inputStream) + .collectList() + .map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints)) + .flux(); + } - if (!Hints.isLoggingSuppressed(hints)) { - LogFormatUtils.traceDebug(logger, traceOn -> { - String formatted = LogFormatUtils.formatValue(value, !traceOn); - return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]"; - }); } + } - JavaType javaType = getJavaType(elementType.getType(), null); - Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); - ObjectWriter writer = (jsonView != null ? - getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer()); + public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { - if (javaType.isContainerType()) { - writer = writer.forType(javaType); - } - - writer = customizeWriter(writer, mimeType, elementType, hints); + ObjectWriter writer = createObjectWriter(valueType, mimeType, hints); + ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler()); + JsonEncoding encoding = getJsonEncoding(mimeType); - DataBuffer buffer = bufferFactory.allocateBuffer(); - boolean release = true; - OutputStream outputStream = buffer.asOutputStream(); + logValue(hints, value); try { - JsonGenerator generator = getObjectMapper().getFactory().createGenerator(outputStream, encoding); + JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding); writer.writeValue(generator, value); generator.flush(); - release = false; } catch (InvalidDefinitionException ex) { throw new CodecException("Type definition error: " + ex.getType(), ex); @@ -182,24 +170,97 @@ private DataBuffer encodeValue(Object value, @Nullable MimeType mimeType, DataBu throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); } catch (IOException ex) { - throw new IllegalStateException("Unexpected I/O error while writing to data buffer", + throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex); } - finally { - if (release) { - DataBufferUtils.release(buffer); - } + + byte[] bytes = byteBuilder.toByteArray(); + DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + + return buffer; + } + + private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map hints, + SequenceWriter sequenceWriter, ByteArrayBuilder byteArrayBuilder, byte[] separator) { + + logValue(hints, value); + + try { + sequenceWriter.write(value); + sequenceWriter.flush(); } + catch (InvalidDefinitionException ex) { + throw new CodecException("Type definition error: " + ex.getType(), ex); + } + catch (JsonProcessingException ex) { + throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); + } + catch (IOException ex) { + throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", + ex); + } + + byte[] bytes = byteArrayBuilder.toByteArray(); + byteArrayBuilder.reset(); + + int offset; + int length; + if (bytes.length > 0 && bytes[0] == ' ') { + // SequenceWriter writes an unnecessary space in between values + offset = 1; + length = bytes.length - 1; + } + else { + offset = 0; + length = bytes.length; + } + DataBuffer buffer = bufferFactory.allocateBuffer(length + separator.length); + buffer.write(bytes, offset, length); + buffer.write(separator); return buffer; } + private void logValue(@Nullable Map hints, Object value) { + if (!Hints.isLoggingSuppressed(hints)) { + LogFormatUtils.traceDebug(logger, traceOn -> { + String formatted = LogFormatUtils.formatValue(value, !traceOn); + return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]"; + }); + } + } + + private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType, + @Nullable Map hints) { + JavaType javaType = getJavaType(valueType.getType(), null); + Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); + ObjectWriter writer = (jsonView != null ? + getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer()); + + if (javaType.isContainerType()) { + writer = writer.forType(javaType); + } + + return customizeWriter(writer, mimeType, valueType, hints); + } + protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType, ResolvableType elementType, @Nullable Map hints) { return writer; } + @Nullable + private byte[] streamSeparator(@Nullable MimeType mimeType) { + for (MediaType streamingMediaType : this.streamingMediaTypes) { + if (streamingMediaType.isCompatibleWith(mimeType)) { + return STREAM_SEPARATORS.getOrDefault(streamingMediaType, NEWLINE_SEPARATOR); + } + } + return null; + } + /** * Determine the JSON encoding to use for the given mime type. * @param mimeType the mime type as requested by the caller diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java index e6b3cba6fc0..46e19e7bf65 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -20,23 +20,26 @@ import java.io.UncheckedIOException; import java.util.Arrays; import java.util.List; -import java.util.function.Consumer; +import com.fasterxml.jackson.databind.MappingIterator; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.core.ResolvableType; import org.springframework.core.codec.AbstractEncoderTestCase; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.codec.Pojo; import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.util.MimeType; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.springframework.core.io.buffer.DataBufferUtils.release; import static org.springframework.http.MediaType.APPLICATION_XML; @@ -59,21 +62,6 @@ public Jackson2SmileEncoderTests() { } - public Consumer pojoConsumer(Pojo expected) { - return dataBuffer -> { - try { - Pojo actual = this.mapper.reader().forType(Pojo.class) - .readValue(DataBufferTestUtils.dumpBytes(dataBuffer)); - assertEquals(expected, actual); - release(dataBuffer); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }; - } - - @Override @Test public void canEncode() { @@ -106,7 +94,19 @@ public void encode() { Flux input = Flux.fromIterable(list); testEncode(input, Pojo.class, step -> step - .consumeNextWith(expect(list, List.class))); + .consumeNextWith(dataBuffer -> { + try { + Object actual = this.mapper.reader().forType(List.class) + .readValue(dataBuffer.asInputStream()); + assertEquals(list, actual); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + finally { + release(dataBuffer); + } + })); } @Test @@ -127,32 +127,30 @@ public void encodeAsStream() throws Exception { Flux input = Flux.just(pojo1, pojo2, pojo3); ResolvableType type = ResolvableType.forClass(Pojo.class); - testEncodeAll(input, type, step -> step - .consumeNextWith(expect(pojo1, Pojo.class)) - .consumeNextWith(expect(pojo2, Pojo.class)) - .consumeNextWith(expect(pojo3, Pojo.class)) - .verifyComplete(), - STREAM_SMILE_MIME_TYPE, null); + Flux result = this.encoder + .encode(input, bufferFactory, type, STREAM_SMILE_MIME_TYPE, null); + + Mono> joined = DataBufferUtils.join(result) + .map(buffer -> { + try { + return this.mapper.reader().forType(Pojo.class).readValues(buffer.asInputStream(true)); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + StepVerifier.create(joined) + .assertNext(iter -> { + assertTrue(iter.hasNext()); + assertEquals(pojo1, iter.next()); + assertTrue(iter.hasNext()); + assertEquals(pojo2, iter.next()); + assertTrue(iter.hasNext()); + assertEquals(pojo3, iter.next()); + assertFalse(iter.hasNext()); + }) + .verifyComplete(); } - - private Consumer expect(T expected, Class expectedType) { - return dataBuffer -> { - try { - Object actual = this.mapper.reader().forType(expectedType) - .readValue(dataBuffer.asInputStream()); - assertEquals(expected, actual); - } - catch (IOException e) { - throw new UncheckedIOException(e); - } - finally { - release(dataBuffer); - } - }; - - } - - - } From 1b50ca6bd3f21dac3553e592081165d966e1ffa6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 28 Jan 2020 21:19:15 +0100 Subject: [PATCH 106/322] Solve ReflectPermission issue in sandbox security policy model for CGLIB Closes gh-24420 --- .../org/springframework/cglib/core/ReflectUtils.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 0b09ad76004..1af56bd8366 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -336,7 +336,15 @@ public static Object newInstance(final Constructor cstruct, final Object[] args) public static Constructor getConstructor(Class type, Class[] parameterTypes) { try { Constructor constructor = type.getDeclaredConstructor(parameterTypes); - constructor.setAccessible(true); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + constructor.setAccessible(true); + return null; + }); + } + else { + constructor.setAccessible(true); + } return constructor; } catch (NoSuchMethodException e) { From ff385aaf7b1c36c0585f724c5c406c4b4ff8fc6a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 28 Jan 2020 21:20:46 +0100 Subject: [PATCH 107/322] Add Informix to supported database products for procedure calls Closes gh-24443 --- .../jdbc/core/metadata/CallMetaDataProviderFactory.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java index 5030fe33e4c..ff93bfc9a85 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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,8 +42,9 @@ public final class CallMetaDataProviderFactory { public static final List supportedDatabaseProductsForProcedures = Arrays.asList( "Apache Derby", "DB2", - "MySQL", + "Informix Dynamic Server", "Microsoft SQL Server", + "MySQL", "Oracle", "PostgreSQL", "Sybase" @@ -51,8 +52,8 @@ public final class CallMetaDataProviderFactory { /** List of supported database products for function calls. */ public static final List supportedDatabaseProductsForFunctions = Arrays.asList( - "MySQL", "Microsoft SQL Server", + "MySQL", "Oracle", "PostgreSQL" ); From abc2269d97abc11f82ae1db5caed78fef2069b6e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 28 Jan 2020 21:21:30 +0100 Subject: [PATCH 108/322] Use local LoggerContext in Log4jLog when static field not initialized yet Closes gh-24440 --- .../apache/commons/logging/LogAdapter.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java index e99cb2964de..e6d6f42f0f3 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -157,7 +157,12 @@ private static class Log4jLog implements Log, Serializable { private final ExtendedLogger logger; public Log4jLog(String name) { - this.logger = loggerContext.getLogger(name); + LoggerContext context = loggerContext; + if (context == null) { + // Circular call in early-init scenario -> static field not initialized yet + context = LogManager.getContext(Log4jLog.class.getClassLoader(), false); + } + this.logger = context.getLogger(name); } @Override @@ -280,92 +285,110 @@ public Slf4jLog(T logger) { this.logger = logger; } + @Override public boolean isFatalEnabled() { return isErrorEnabled(); } + @Override public boolean isErrorEnabled() { return this.logger.isErrorEnabled(); } + @Override public boolean isWarnEnabled() { return this.logger.isWarnEnabled(); } + @Override public boolean isInfoEnabled() { return this.logger.isInfoEnabled(); } + @Override public boolean isDebugEnabled() { return this.logger.isDebugEnabled(); } + @Override public boolean isTraceEnabled() { return this.logger.isTraceEnabled(); } + @Override public void fatal(Object message) { error(message); } + @Override public void fatal(Object message, Throwable exception) { error(message, exception); } + @Override public void error(Object message) { if (message instanceof String || this.logger.isErrorEnabled()) { this.logger.error(String.valueOf(message)); } } + @Override public void error(Object message, Throwable exception) { if (message instanceof String || this.logger.isErrorEnabled()) { this.logger.error(String.valueOf(message), exception); } } + @Override public void warn(Object message) { if (message instanceof String || this.logger.isWarnEnabled()) { this.logger.warn(String.valueOf(message)); } } + @Override public void warn(Object message, Throwable exception) { if (message instanceof String || this.logger.isWarnEnabled()) { this.logger.warn(String.valueOf(message), exception); } } + @Override public void info(Object message) { if (message instanceof String || this.logger.isInfoEnabled()) { this.logger.info(String.valueOf(message)); } } + @Override public void info(Object message, Throwable exception) { if (message instanceof String || this.logger.isInfoEnabled()) { this.logger.info(String.valueOf(message), exception); } } + @Override public void debug(Object message) { if (message instanceof String || this.logger.isDebugEnabled()) { this.logger.debug(String.valueOf(message)); } } + @Override public void debug(Object message, Throwable exception) { if (message instanceof String || this.logger.isDebugEnabled()) { this.logger.debug(String.valueOf(message), exception); } } + @Override public void trace(Object message) { if (message instanceof String || this.logger.isTraceEnabled()) { this.logger.trace(String.valueOf(message)); } } + @Override public void trace(Object message, Throwable exception) { if (message instanceof String || this.logger.isTraceEnabled()) { this.logger.trace(String.valueOf(message), exception); @@ -486,74 +509,92 @@ public JavaUtilLog(String name) { this.logger = java.util.logging.Logger.getLogger(name); } + @Override public boolean isFatalEnabled() { return isErrorEnabled(); } + @Override public boolean isErrorEnabled() { return this.logger.isLoggable(java.util.logging.Level.SEVERE); } + @Override public boolean isWarnEnabled() { return this.logger.isLoggable(java.util.logging.Level.WARNING); } + @Override public boolean isInfoEnabled() { return this.logger.isLoggable(java.util.logging.Level.INFO); } + @Override public boolean isDebugEnabled() { return this.logger.isLoggable(java.util.logging.Level.FINE); } + @Override public boolean isTraceEnabled() { return this.logger.isLoggable(java.util.logging.Level.FINEST); } + @Override public void fatal(Object message) { error(message); } + @Override public void fatal(Object message, Throwable exception) { error(message, exception); } + @Override public void error(Object message) { log(java.util.logging.Level.SEVERE, message, null); } + @Override public void error(Object message, Throwable exception) { log(java.util.logging.Level.SEVERE, message, exception); } + @Override public void warn(Object message) { log(java.util.logging.Level.WARNING, message, null); } + @Override public void warn(Object message, Throwable exception) { log(java.util.logging.Level.WARNING, message, exception); } + @Override public void info(Object message) { log(java.util.logging.Level.INFO, message, null); } + @Override public void info(Object message, Throwable exception) { log(java.util.logging.Level.INFO, message, exception); } + @Override public void debug(Object message) { log(java.util.logging.Level.FINE, message, null); } + @Override public void debug(Object message, Throwable exception) { log(java.util.logging.Level.FINE, message, exception); } + @Override public void trace(Object message) { log(java.util.logging.Level.FINEST, message, null); } + @Override public void trace(Object message, Throwable exception) { log(java.util.logging.Level.FINEST, message, exception); } From 51b8ba334ad91bf1f50b1b73cef35255505a2e28 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 28 Jan 2020 21:40:16 +0100 Subject: [PATCH 109/322] Polishing --- .../beans/support/ArgumentConvertingMethodInvoker.java | 7 ++++--- .../java/org/springframework/core/GenericTypeResolver.java | 5 +++-- .../type/classreading/MethodMetadataReadingVisitor.java | 5 +++-- .../main/java/org/apache/commons/logging/LogAdapter.java | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java b/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java index 006da60e4d0..bdd72f24d38 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -146,8 +146,9 @@ protected Method doFindMatchingMethod(Object[] arguments) { for (Method candidate : candidates) { if (candidate.getName().equals(targetMethod)) { // Check if the inspected method has the correct number of parameters. - Class[] paramTypes = candidate.getParameterTypes(); - if (paramTypes.length == argCount) { + int parameterCount = candidate.getParameterCount(); + if (parameterCount == argCount) { + Class[] paramTypes = candidate.getParameterTypes(); Object[] convertedArguments = new Object[argCount]; boolean match = true; for (int j = 0; j < argCount && match; j++) { diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index 55c7aab1894..d9cb505f7dc 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -169,11 +169,12 @@ else if (genericType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) genericType; Class[] generics = new Class[parameterizedType.getActualTypeArguments().length]; Type[] typeArguments = parameterizedType.getActualTypeArguments(); + ResolvableType contextType = ResolvableType.forClass(contextClass); for (int i = 0; i < typeArguments.length; i++) { Type typeArgument = typeArguments[i]; if (typeArgument instanceof TypeVariable) { ResolvableType resolvedTypeArgument = resolveVariable( - (TypeVariable) typeArgument, ResolvableType.forClass(contextClass)); + (TypeVariable) typeArgument, contextType); if (resolvedTypeArgument != ResolvableType.NONE) { generics[i] = resolvedTypeArgument.resolve(); } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java index 97dc10024ee..e0526ae38f2 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -149,9 +149,10 @@ public MultiValueMap getAllAnnotationAttributes(String annotatio MultiValueMap allAttributes = new LinkedMultiValueMap<>(); List attributesList = this.attributesMap.get(annotationName); if (attributesList != null) { + String annotatedElement = "method '" + getMethodName() + '\''; for (AnnotationAttributes annotationAttributes : attributesList) { AnnotationAttributes convertedAttributes = AnnotationReadingVisitorUtils.convertClassValues( - "method '" + getMethodName() + "'", this.classLoader, annotationAttributes, classValuesAsString); + annotatedElement, this.classLoader, annotationAttributes, classValuesAsString); convertedAttributes.forEach(allAttributes::add); } } diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java index e6d6f42f0f3..c918ee7c2df 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java @@ -608,8 +608,8 @@ private void log(java.util.logging.Level level, Object message, Throwable except else { rec = new LocationResolvingLogRecord(level, String.valueOf(message)); rec.setLoggerName(this.name); - rec.setResourceBundleName(logger.getResourceBundleName()); - rec.setResourceBundle(logger.getResourceBundle()); + rec.setResourceBundleName(this.logger.getResourceBundleName()); + rec.setResourceBundle(this.logger.getResourceBundle()); rec.setThrown(exception); } logger.log(rec); From a59a33846e65ddac1ec71b1c3a079bd810df437b Mon Sep 17 00:00:00 2001 From: Christoph Dreis Date: Thu, 30 Jan 2020 14:17:22 +0100 Subject: [PATCH 110/322] Polish documentation format See gh-24462 --- src/docs/asciidoc/core/core-validation.adoc | 1 + src/docs/asciidoc/web/webflux.adoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 3e7d0d87e83..d597322c956 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -1618,6 +1618,7 @@ the Spring Validation API, as the following example shows: ==== Configuring Custom Constraints Each bean validation constraint consists of two parts: + * A `@Constraint` annotation that declares the constraint and its configurable properties. * An implementation diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 3d047c642b7..978ee19ce6e 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -3102,7 +3102,7 @@ use `FormatterRegistrar` implementations. By default, if <> is present on the classpath (for example, the Hibernate Validator), the `LocalValidatorFactoryBean` is registered as a global <> for use with `@Valid` and -`Validated` on `@Controller` method arguments. +`@Validated` on `@Controller` method arguments. In your Java configuration, you can customize the global `Validator` instance, as the following example shows: From 0d7494ac52e38ab4b5fbd4073506edd8e46f7624 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 6 Feb 2020 10:01:13 +0100 Subject: [PATCH 111/322] Honour ObjectMapper feature in Jackson2Tokenizer After this commit, Jackson2Tokenizer honours ObjectMapper's DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS feature when creating TokenBuffers. Closes gh-24479 --- .../http/codec/json/Jackson2Tokenizer.java | 35 ++++++++++++---- .../codec/json/Jackson2TokenizerTests.java | 42 +++++++++++++++++-- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 9b4b2842762..292888f7ea1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -27,6 +27,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.async.ByteArrayFeeder; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.util.TokenBuffer; @@ -36,6 +37,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; /** * {@link Function} to transform a JSON stream of arbitrary size, byte array @@ -55,16 +57,19 @@ final class Jackson2Tokenizer { private final boolean tokenizeArrayElements; - private TokenBuffer tokenBuffer; + private final boolean forceUseOfBigDecimal; + + private final int maxInMemorySize; private int objectDepth; private int arrayDepth; - private final int maxInMemorySize; - private int byteCount; + @Nullable // yet initialized by calling createToken() in the constructor + private TokenBuffer tokenBuffer; + // TODO: change to ByteBufferFeeder when supported by Jackson // See https://github.com/FasterXML/jackson-core/issues/478 @@ -72,17 +77,19 @@ final class Jackson2Tokenizer { private Jackson2Tokenizer(JsonParser parser, DeserializationContext deserializationContext, - boolean tokenizeArrayElements, int maxInMemorySize) { + boolean tokenizeArrayElements, boolean forceUseOfBigDecimal, int maxInMemorySize) { this.parser = parser; this.deserializationContext = deserializationContext; this.tokenizeArrayElements = tokenizeArrayElements; - this.tokenBuffer = new TokenBuffer(parser, deserializationContext); + this.forceUseOfBigDecimal = forceUseOfBigDecimal; this.inputFeeder = (ByteArrayFeeder) this.parser.getNonBlockingInputFeeder(); this.maxInMemorySize = maxInMemorySize; + createToken(); } + private Flux tokenize(DataBuffer dataBuffer) { int bufferSize = dataBuffer.readableByteCount(); byte[] bytes = new byte[dataBuffer.readableByteCount()]; @@ -132,6 +139,9 @@ else if (token == null ) { // !previousNull previousNull = true; continue; } + else { + previousNull = false; + } updateDepth(token); if (!this.tokenizeArrayElements) { processTokenNormal(token, result); @@ -165,7 +175,7 @@ private void processTokenNormal(JsonToken token, List result) throw if ((token.isStructEnd() || token.isScalarValue()) && this.objectDepth == 0 && this.arrayDepth == 0) { result.add(this.tokenBuffer); - this.tokenBuffer = new TokenBuffer(this.parser, this.deserializationContext); + createToken(); } } @@ -178,10 +188,15 @@ private void processTokenArray(JsonToken token, List result) throws if (this.objectDepth == 0 && (this.arrayDepth == 0 || this.arrayDepth == 1) && (token == JsonToken.END_OBJECT || token.isScalarValue())) { result.add(this.tokenBuffer); - this.tokenBuffer = new TokenBuffer(this.parser, this.deserializationContext); + createToken(); } } + private void createToken() { + this.tokenBuffer = new TokenBuffer(this.parser, this.deserializationContext); + this.tokenBuffer.forceUseOfBigDecimal(this.forceUseOfBigDecimal); + } + private boolean isTopLevelArrayToken(JsonToken token) { return this.objectDepth == 0 && ((token == JsonToken.START_ARRAY && this.arrayDepth == 1) || (token == JsonToken.END_ARRAY && this.arrayDepth == 0)); @@ -229,7 +244,9 @@ public static Flux tokenize(Flux dataBuffers, JsonFacto context = ((DefaultDeserializationContext) context).createInstance( objectMapper.getDeserializationConfig(), parser, objectMapper.getInjectableValues()); } - Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrays, maxInMemorySize); + boolean forceUseOfBigDecimal = objectMapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrays, forceUseOfBigDecimal, + maxInMemorySize); return dataBuffers.flatMap(tokenizer::tokenize, Flux::error, tokenizer::endOfInput); } catch (IOException ex) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index aa25442cb00..ffda9a8ffc9 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -19,10 +19,14 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.List; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.TokenBuffer; import org.json.JSONException; @@ -37,8 +41,10 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferLimitException; -import static java.util.Arrays.*; -import static java.util.Collections.*; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; /** * @author Arjen Poutsma @@ -259,6 +265,36 @@ public void jsonEOFExceptionIsWrappedAsDecodingError() { .verify(); } + @Test + public void useBigDecimalForFloats() { + for (boolean useBigDecimalForFloats : Arrays.asList(false, true)) { + this.objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, useBigDecimalForFloats); + + Flux source = Flux.just(stringBuffer("1E+2")); + Flux tokens = + Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1); + + StepVerifier.create(tokens) + .assertNext(tokenBuffer -> { + try { + JsonParser parser = tokenBuffer.asParser(); + JsonToken token = parser.nextToken(); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, token); + JsonParser.NumberType numberType = parser.getNumberType(); + if (useBigDecimalForFloats) { + assertEquals(JsonParser.NumberType.BIG_DECIMAL, numberType); + } + else { + assertEquals(JsonParser.NumberType.DOUBLE, numberType); + } + } + catch (IOException ex) { + fail(ex.getMessage()); + } + }) + .verifyComplete(); + } + } private Flux decode(List source, boolean tokenize, int maxInMemorySize) { From 17117bd21adae1e4cb7cfb3219db35a6a987c55f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 7 Feb 2020 14:43:24 +0100 Subject: [PATCH 112/322] Force TokenBuffer to use BigDecimal if necessary This commit makes the Jackson2Tokenizer enable TokenBuffer.forceUseOfBigDecimal if the element type given to the Decoder is BigDecimal. Previous to this commit, values would be converted to floats. Closes gh-24479 --- .../codec/json/AbstractJackson2Decoder.java | 21 ++++++++--- .../http/codec/json/Jackson2Tokenizer.java | 7 ++-- .../codec/json/Jackson2JsonDecoderTests.java | 35 ++++++++++++++----- .../codec/json/Jackson2TokenizerTests.java | 14 ++++---- 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index 95586080ae5..b0df7898245 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -18,11 +18,13 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; @@ -113,8 +115,13 @@ public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), this.jsonFactory, getObjectMapper(), true, getMaxInMemorySize()); + boolean forceUseOfBigDecimal = getObjectMapper().isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + forceUseOfBigDecimal = true; + } + + Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), + true, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints); } @@ -122,8 +129,12 @@ public Flux decode(Publisher input, ResolvableType elementTy public Mono decodeToMono(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), this.jsonFactory, getObjectMapper(), false, getMaxInMemorySize()); + boolean forceUseOfBigDecimal = getObjectMapper().isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + forceUseOfBigDecimal = true; + } + Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), + false, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 292888f7ea1..4861ced5d18 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -27,7 +27,6 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.async.ByteArrayFeeder; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.util.TokenBuffer; @@ -232,10 +231,13 @@ private void raiseLimitException() { * @param objectMapper the current mapper instance * @param tokenizeArrays if {@code true} and the "top level" JSON object is * an array, each element is returned individually immediately after it is received + * @param forceUseOfBigDecimal if {@code true}, any floating point values encountered in source will use + * {@link java.math.BigDecimal} + * @param maxInMemorySize maximum memory size * @return the resulting token buffers */ public static Flux tokenize(Flux dataBuffers, JsonFactory jsonFactory, - ObjectMapper objectMapper, boolean tokenizeArrays, int maxInMemorySize) { + ObjectMapper objectMapper, boolean tokenizeArrays, boolean forceUseOfBigDecimal, int maxInMemorySize) { try { JsonParser parser = jsonFactory.createNonBlockingByteArrayParser(); @@ -244,7 +246,6 @@ public static Flux tokenize(Flux dataBuffers, JsonFacto context = ((DefaultDeserializationContext) context).createInstance( objectMapper.getDeserializationConfig(), parser, objectMapper.getInjectableValues()); } - boolean forceUseOfBigDecimal = objectMapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrays, forceUseOfBigDecimal, maxInMemorySize); return dataBuffers.flatMap(tokenizer::tokenize, Flux::error, tokenizer::endOfInput); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index 3a0e941adda..e3b158ee73d 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,6 +17,7 @@ package org.springframework.http.codec.json; import java.io.IOException; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; @@ -42,13 +43,21 @@ import org.springframework.http.codec.Pojo; import org.springframework.util.MimeType; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.junit.Assert.*; -import static org.springframework.core.ResolvableType.*; -import static org.springframework.http.MediaType.*; -import static org.springframework.http.codec.json.Jackson2JsonDecoder.*; -import static org.springframework.http.codec.json.JacksonViewBean.*; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; +import static org.springframework.http.MediaType.APPLICATION_XML; +import static org.springframework.http.codec.json.Jackson2JsonDecoder.JSON_VIEW_HINT; +import static org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1; +import static org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3; /** * Unit tests for {@link Jackson2JsonDecoder}. @@ -199,6 +208,16 @@ public void customDeserializer() { ); } + @Test + public void bigDecimalFlux() { + Flux input = stringBuffer("[ 1E+2 ]").flux(); + + testDecode(input, BigDecimal.class, step -> step + .expectNext(new BigDecimal("1E+2")) + .verifyComplete() + ); + } + private Mono stringBuffer(String value) { return Mono.defer(() -> { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index ffda9a8ffc9..9b6e90d1a32 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -26,7 +26,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.TokenBuffer; import org.json.JSONException; @@ -248,7 +247,8 @@ public void testLimitTokenized() { public void errorInStream() { DataBuffer buffer = stringBuffer("{\"id\":1,\"name\":"); Flux source = Flux.just(buffer).concatWith(Flux.error(new RuntimeException())); - Flux result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true, -1); + Flux result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true, + false, -1); StepVerifier.create(result) .expectError(RuntimeException.class) @@ -258,7 +258,8 @@ public void errorInStream() { @Test // SPR-16521 public void jsonEOFExceptionIsWrappedAsDecodingError() { Flux source = Flux.just(stringBuffer("{\"status\": \"noClosingQuote}")); - Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1); + Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, + false, -1); StepVerifier.create(tokens) .expectError(DecodingException.class) @@ -268,11 +269,10 @@ public void jsonEOFExceptionIsWrappedAsDecodingError() { @Test public void useBigDecimalForFloats() { for (boolean useBigDecimalForFloats : Arrays.asList(false, true)) { - this.objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, useBigDecimalForFloats); - Flux source = Flux.just(stringBuffer("1E+2")); Flux tokens = - Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1); + Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, + useBigDecimalForFloats, -1); StepVerifier.create(tokens) .assertNext(tokenBuffer -> { @@ -300,7 +300,7 @@ private Flux decode(List source, boolean tokenize, int maxInMemo Flux tokens = Jackson2Tokenizer.tokenize( Flux.fromIterable(source).map(this::stringBuffer), - this.jsonFactory, this.objectMapper, tokenize, maxInMemorySize); + this.jsonFactory, this.objectMapper, tokenize, false, maxInMemorySize); return tokens .map(tokenBuffer -> { From da02b7a1417c99af327205e703966860781bff95 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 12 Feb 2020 15:33:01 +0100 Subject: [PATCH 113/322] Support SpEL compilation of interface methods again Spring Framework 5.1.8 introduced a regression for the compilation of SpEL expressions referencing a method declared in an interface. An attempt to compile such an expression resulted in a SpelEvaluationException caused by an IncompatibleClassChangeError. This commit fixes this regression by adding explicit support in ReflectivePropertyAccessor.OptimalPropertyAccessor.generateCode() for methods declared in interfaces. Closes gh-24357 --- .../support/ReflectivePropertyAccessor.java | 10 +++- .../spel/standard/SpelCompilerTests.java | 59 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 99c763da52b..d426524dd43 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -55,6 +55,7 @@ * @author Andy Clement * @author Juergen Hoeller * @author Phillip Webb + * @author Sam Brannen * @since 3.0 * @see StandardEvaluationContext * @see SimpleEvaluationContext @@ -765,8 +766,11 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { } if (this.member instanceof Method) { - mv.visitMethodInsn((isStatic ? INVOKESTATIC : INVOKEVIRTUAL), classDesc, this.member.getName(), - CodeFlow.createSignatureDescriptor((Method) this.member), false); + Method method = (Method) this.member; + boolean isInterface = method.getDeclaringClass().isInterface(); + int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL); + mv.visitMethodInsn(opcode, classDesc, method.getName(), + CodeFlow.createSignatureDescriptor(method), isInterface); } else { mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, this.member.getName(), diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java new file mode 100644 index 00000000000..a9439b08a46 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel.standard; + +import java.util.stream.IntStream; + +import org.junit.Test; + +import org.springframework.core.Ordered; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelParserConfiguration; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for the {@link SpelCompiler}. + * + * @author Sam Brannen + * @since 5.1.14 + */ +public class SpelCompilerTests { + + @Test // gh-24357 + public void expressionCompilesWhenMethodComesFromPublicInterface() { + SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + SpelExpressionParser parser = new SpelExpressionParser(config); + + OrderedComponent component = new OrderedComponent(); + Expression expression = parser.parseExpression("order"); + + // Evaluate the expression multiple times to ensure that it gets compiled. + IntStream.rangeClosed(1, 5).forEach(i -> assertEquals(42, expression.getValue(component))); + } + + + static class OrderedComponent implements Ordered { + + @Override + public int getOrder() { + return 42; + } + } + +} From 4c1d2277764d15471ae800c797e44764232568e9 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 12 Feb 2020 15:47:17 +0100 Subject: [PATCH 114/322] Polish documentation format Closes gh-24462 --- src/docs/asciidoc/core/core-validation.adoc | 8 +++----- src/docs/asciidoc/web/webmvc.adoc | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index d597322c956..ada8d5643dc 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -1619,11 +1619,9 @@ the Spring Validation API, as the following example shows: Each bean validation constraint consists of two parts: -* A `@Constraint` annotation -that declares the constraint and its configurable properties. -* An implementation -of the `javax.validation.ConstraintValidator` interface that implements the constraint's -behavior. +* A `@Constraint` annotation that declares the constraint and its configurable properties. +* An implementation of the `javax.validation.ConstraintValidator` interface that implements +the constraint's behavior. To associate a declaration with an implementation, each `@Constraint` annotation references a corresponding `ConstraintValidator` implementation class. At runtime, a diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 44345bcdd90..506177fcb6e 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -4251,7 +4251,7 @@ FormatterRegistrar implementations. By default, if <> is present on the classpath (for example, Hibernate Validator), the `LocalValidatorFactoryBean` is registered as a global <> for use with `@Valid` and -`Validated` on controller method arguments. +`@Validated` on `@Controller` method arguments. In Java configuration, you can customize the global `Validator` instance, as the following example shows: From d12619cf1f7a772440622c8102d6bca1cf46b5be Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Feb 2020 16:48:42 +0100 Subject: [PATCH 115/322] Raise log level for exceptions from EntityManager close call Closes gh-24501 --- .../jms/connection/SingleConnectionFactory.java | 4 ++-- .../orm/hibernate5/SessionFactoryUtils.java | 7 ++----- .../springframework/orm/jpa/EntityManagerFactoryUtils.java | 7 ++----- .../org/springframework/orm/jpa/JpaTransactionManager.java | 6 +++--- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java index bc1f74c48c3..84b2378ab05 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -505,7 +505,7 @@ protected void closeConnection(Connection con) { logger.debug("Ignoring Connection state exception - assuming already closed: " + ex); } catch (Throwable ex) { - logger.debug("Could not close shared JMS Connection", ex); + logger.warn("Could not close shared JMS Connection", ex); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java index 79b0cecf2fe..87ba5cc2a07 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -170,11 +170,8 @@ public static void closeSession(@Nullable Session session) { try { session.close(); } - catch (HibernateException ex) { - logger.debug("Could not close Hibernate Session", ex); - } catch (Throwable ex) { - logger.debug("Unexpected exception on closing Hibernate Session", ex); + logger.error("Failed to release Hibernate Session", ex); } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java index b5a148ed3d8..e8fb5e15fb8 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -427,11 +427,8 @@ public static void closeEntityManager(@Nullable EntityManager em) { em.close(); } } - catch (PersistenceException ex) { - logger.debug("Could not close JPA EntityManager", ex); - } catch (Throwable ex) { - logger.debug("Unexpected exception on closing JPA EntityManager", ex); + logger.error("Failed to release JPA EntityManager", ex); } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java index 5c33b474d1c..c4c1385cca7 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -603,9 +603,9 @@ protected void doCleanupAfterCompletion(Object transaction) { getJpaDialect().releaseJdbcConnection(conHandle, txObject.getEntityManagerHolder().getEntityManager()); } - catch (Exception ex) { + catch (Throwable ex) { // Just log it, to keep a transaction-related exception. - logger.error("Could not close JDBC connection after transaction", ex); + logger.error("Failed to release JDBC connection after transaction", ex); } } } From af75c6db2fc3a5ee9731cf3268f07f31d2e68502 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Feb 2020 16:49:02 +0100 Subject: [PATCH 116/322] Consistent ROLE_INFRASTRUCTURE declarations for configuration classes Closes gh-24509 --- .../scheduling/aspectj/AspectJAsyncConfiguration.java | 3 ++- .../aspectj/AspectJJtaTransactionManagementConfiguration.java | 3 ++- .../aspectj/AspectJTransactionManagementConfiguration.java | 3 ++- .../context/annotation/LoadTimeWeavingConfiguration.java | 3 ++- .../context/annotation/MBeanExportConfiguration.java | 3 ++- .../annotation/ProxyTransactionManagementConfiguration.java | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java index ef93148652a..e13a595b42d 100644 --- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -37,6 +37,7 @@ * @see org.springframework.scheduling.annotation.ProxyAsyncConfiguration */ @Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJAsyncConfiguration extends AbstractAsyncConfiguration { @Bean(name = TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME) diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java index caa7cc3c2f7..ec733d3cf2b 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -36,6 +36,7 @@ * @see TransactionManagementConfigurationSelector */ @Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJJtaTransactionManagementConfiguration extends AspectJTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_BEAN_NAME) diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java index 2784a733e7c..2c99c305074 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -38,6 +38,7 @@ * @see AspectJJtaTransactionManagementConfiguration */ @Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java index 65cbe15a275..8c54ca4971b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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,6 +42,7 @@ * @see ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME */ @Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class LoadTimeWeavingConfiguration implements ImportAware, BeanClassLoaderAware { @Nullable diff --git a/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java index 02a9d40b929..04fb3ff00bc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -50,6 +50,7 @@ * @see EnableMBeanExport */ @Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class MBeanExportConfiguration implements ImportAware, EnvironmentAware, BeanFactoryAware { private static final String MBEAN_EXPORTER_BEAN_NAME = "mbeanExporter"; diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java index eb8627b31c1..f126d4d681f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -35,6 +35,7 @@ * @see TransactionManagementConfigurationSelector */ @Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) From f536819c5d2b90c04741446fe4fa9c158252f1fd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 11 Feb 2020 14:01:21 +0000 Subject: [PATCH 117/322] Update advice on RestTemplate Closes gh-24503 --- .../springframework/web/client/RestTemplate.java | 13 +++++-------- src/docs/asciidoc/integration.adoc | 8 ++++---- src/docs/asciidoc/web/webmvc-client.adoc | 10 ++++------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index a1444b05d99..74ec11a14ef 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -72,13 +72,10 @@ * addition to the generalized {@code exchange} and {@code execute} methods that * support of less frequent cases. * - *

NOTE: As of 5.0, the non-blocking, reactive - * {@code org.springframework.web.reactive.client.WebClient} offers a - * modern alternative to the {@code RestTemplate} with efficient support for - * both sync and async, as well as streaming scenarios. The {@code RestTemplate} - * will be deprecated in a future version and will not have major new features - * added going forward. See the WebClient section of the Spring Framework reference - * documentation for more details and example code. + *

NOTE: As of 5.0 this class is in maintenance mode, with + * only minor requests for changes and bugs to be accepted going forward. Please, + * consider using the {@code org.springframework.web.reactive.client.WebClient} + * which has a more modern API and supports sync, async, and streaming scenarios. * * @author Arjen Poutsma * @author Brian Clozel diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index d93394766fd..bb454b6624e 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -973,10 +973,10 @@ method API. * <>: a non-blocking, reactive alternative that supports both synchronous and asynchronous as well as streaming scenarios. -NOTE: As of 5.0, the non-blocking, reactive `WebClient` offers a modern alternative to the -`RestTemplate` with efficient support for both synchronous and asynchronous as well as streaming -scenarios. The `RestTemplate` will be deprecated in a future version and will not have -major new features added going forward. +NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only minor requests for +changes and bugs to be accepted going forward. Please, consider using the +<> which offers a more modern API and +supports sync, async, and streaming scenarios. [[rest-resttemplate]] diff --git a/src/docs/asciidoc/web/webmvc-client.adoc b/src/docs/asciidoc/web/webmvc-client.adoc index b1184ee030f..147344c78d6 100644 --- a/src/docs/asciidoc/web/webmvc-client.adoc +++ b/src/docs/asciidoc/web/webmvc-client.adoc @@ -13,12 +13,10 @@ This section describes options for client-side access to REST endpoints. Spring REST client and exposes a simple, template-method API over underlying HTTP client libraries. -NOTE: As of 5.0, the non-blocking, reactive `WebClient` offers a modern alternative to the -`RestTemplate`, with efficient support for both -<>, as well as -streaming scenarios. The `RestTemplate` will be deprecated in a future version and will -not have major new features added going forward. - +NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only minor requests for +changes and bugs to be accepted going forward. Please, consider using the +<> which offers a more modern API and +supports sync, async, and streaming scenarios. See <> for details. From ebd2ec57fc23e901d6b73ccb5fbca5ffb2514cd8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Feb 2020 17:58:26 +0100 Subject: [PATCH 118/322] Polishing --- .../annotation/ConfigurationClassParser.java | 16 +++++----------- .../annotation/DeferredImportSelector.java | 3 ++- .../context/annotation/ImportSelector.java | 3 ++- .../context/annotation/ImportSelectorTests.java | 5 +++-- .../datasource/init/DatabasePopulatorUtils.java | 8 ++++---- .../http/codec/json/AbstractJackson2Decoder.java | 10 ++++++---- .../http/codec/json/AbstractJackson2Encoder.java | 12 ++++++------ .../http/codec/json/Jackson2Tokenizer.java | 16 +++++++--------- .../springframework/web/client/RestTemplate.java | 3 ++- 9 files changed, 37 insertions(+), 39 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 3b7be52f8b0..bee96e974f8 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -748,8 +748,7 @@ private class DeferredImportSelectorHandler { * @param importSelector the selector to handle */ public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { - DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder( - configClass, importSelector); + DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector); if (this.deferredImportSelectors == null) { DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); handler.register(holder); @@ -775,7 +774,6 @@ public void process() { this.deferredImportSelectors = new ArrayList<>(); } } - } @@ -786,8 +784,7 @@ private class DeferredImportSelectorGroupingHandler { private final Map configurationClasses = new HashMap<>(); public void register(DeferredImportSelectorHolder deferredImport) { - Class group = deferredImport.getImportSelector() - .getImportGroup(); + Class group = deferredImport.getImportSelector().getImportGroup(); DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent( (group != null ? group : deferredImport), key -> new DeferredImportSelectorGrouping(createGroup(group))); @@ -799,8 +796,7 @@ public void register(DeferredImportSelectorHolder deferredImport) { public void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { grouping.getImports().forEach(entry -> { - ConfigurationClass configurationClass = this.configurationClasses.get( - entry.getMetadata()); + ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata()); try { processImports(configurationClass, asSourceClass(configurationClass), asSourceClasses(entry.getImportClassName()), false); @@ -818,8 +814,7 @@ public void processGroupImports() { } private Group createGroup(@Nullable Class type) { - Class effectiveType = (type != null ? type - : DefaultDeferredImportSelectorGroup.class); + Class effectiveType = (type != null ? type : DefaultDeferredImportSelectorGroup.class); Group group = BeanUtils.instantiateClass(effectiveType); ParserStrategyUtils.invokeAwareMethods(group, ConfigurationClassParser.this.environment, @@ -827,7 +822,6 @@ private Group createGroup(@Nullable Class type) { ConfigurationClassParser.this.registry); return group; } - } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java index 6bca9156781..dedb068b649 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -51,6 +51,7 @@ default Class getImportGroup() { /** * Interface used to group results from different import selectors. + * @since 5.0 */ interface Group { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java index 2e41305958d..e7305c2b63d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2020 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. @@ -50,6 +50,7 @@ public interface ImportSelector { /** * Select and return the names of which class(es) should be imported based on * the {@link AnnotationMetadata} of the importing @{@link Configuration} class. + * @return the class names, or an empty array if none */ String[] selectImports(AnnotationMetadata importingClassMetadata); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java index c7aec9cc585..66eba0278c1 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -530,9 +530,10 @@ static void cleanup() { static Map> allImports() { return TestImportGroup.imports.entrySet() .stream() - .collect(Collectors.toMap((entry) -> entry.getKey().getClassName(), + .collect(Collectors.toMap(entry -> entry.getKey().getClassName(), Map.Entry::getValue)); } + private final List instanceImports = new ArrayList<>(); @Override diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java index 534df4a0821..3861cb5e54b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -52,10 +52,10 @@ public static void execute(DatabasePopulator populator, DataSource dataSource) t DataSourceUtils.releaseConnection(connection, dataSource); } } + catch (ScriptException ex) { + throw ex; + } catch (Throwable ex) { - if (ex instanceof ScriptException) { - throw (ScriptException) ex; - } throw new UncategorizedScriptException("Failed to execute database script", ex); } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index b0df7898245..98e22e3a055 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -116,7 +116,7 @@ public Flux decode(Publisher input, ResolvableType elementTy @Nullable MimeType mimeType, @Nullable Map hints) { boolean forceUseOfBigDecimal = getObjectMapper().isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); - if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + if (BigDecimal.class.equals(elementType.getType())) { forceUseOfBigDecimal = true; } @@ -130,9 +130,10 @@ public Mono decodeToMono(Publisher input, ResolvableType ele @Nullable MimeType mimeType, @Nullable Map hints) { boolean forceUseOfBigDecimal = getObjectMapper().isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); - if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + if (BigDecimal.class.equals(elementType.getType())) { forceUseOfBigDecimal = true; } + Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), false, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); @@ -177,7 +178,7 @@ private Flux decodeInternal(Flux tokens, ResolvableType ele } - // HttpMessageDecoder... + // HttpMessageDecoder @Override public Map getDecodeHints(ResolvableType actualType, ResolvableType elementType, @@ -191,7 +192,8 @@ public List getDecodableMimeTypes() { return getMimeTypes(); } - // Jackson2CodecSupport ... + + // Jackson2CodecSupport @Override protected A getAnnotation(MethodParameter parameter, Class annotType) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index d7274fbeb42..621405cfd00 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -170,8 +170,7 @@ public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); } catch (IOException ex) { - throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", - ex); + throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex); } byte[] bytes = byteBuilder.toByteArray(); @@ -197,8 +196,7 @@ private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFa throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); } catch (IOException ex) { - throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", - ex); + throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex); } byte[] bytes = byteArrayBuilder.toByteArray(); @@ -233,6 +231,7 @@ private void logValue(@Nullable Map hints, Object value) { private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + JavaType javaType = getJavaType(valueType.getType(), null); Class jsonView = (hints != null ? (Class) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); ObjectWriter writer = (jsonView != null ? @@ -280,7 +279,7 @@ protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) { } - // HttpMessageEncoder... + // HttpMessageEncoder @Override public List getEncodableMimeTypes() { @@ -299,7 +298,8 @@ public Map getEncodeHints(@Nullable ResolvableType actualType, R return (actualType != null ? getHints(actualType) : Hints.none()); } - // Jackson2CodecSupport ... + + // Jackson2CodecSupport @Override protected A getAnnotation(MethodParameter parameter, Class annotType) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 4861ced5d18..4a173521fbd 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -36,7 +36,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; /** * {@link Function} to transform a JSON stream of arbitrary size, byte array @@ -66,7 +65,6 @@ final class Jackson2Tokenizer { private int byteCount; - @Nullable // yet initialized by calling createToken() in the constructor private TokenBuffer tokenBuffer; @@ -84,7 +82,7 @@ private Jackson2Tokenizer(JsonParser parser, DeserializationContext deserializat this.forceUseOfBigDecimal = forceUseOfBigDecimal; this.inputFeeder = (ByteArrayFeeder) this.parser.getNonBlockingInputFeeder(); this.maxInMemorySize = maxInMemorySize; - createToken(); + this.tokenBuffer = createToken(); } @@ -174,9 +172,8 @@ private void processTokenNormal(JsonToken token, List result) throw if ((token.isStructEnd() || token.isScalarValue()) && this.objectDepth == 0 && this.arrayDepth == 0) { result.add(this.tokenBuffer); - createToken(); + this.tokenBuffer = createToken(); } - } private void processTokenArray(JsonToken token, List result) throws IOException { @@ -187,13 +184,14 @@ private void processTokenArray(JsonToken token, List result) throws if (this.objectDepth == 0 && (this.arrayDepth == 0 || this.arrayDepth == 1) && (token == JsonToken.END_OBJECT || token.isScalarValue())) { result.add(this.tokenBuffer); - createToken(); + this.tokenBuffer = createToken(); } } - private void createToken() { - this.tokenBuffer = new TokenBuffer(this.parser, this.deserializationContext); - this.tokenBuffer.forceUseOfBigDecimal(this.forceUseOfBigDecimal); + private TokenBuffer createToken() { + TokenBuffer tokenBuffer = new TokenBuffer(this.parser, this.deserializationContext); + tokenBuffer.forceUseOfBigDecimal(this.forceUseOfBigDecimal); + return tokenBuffer; } private boolean isTopLevelArrayToken(JsonToken token) { diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 74ec11a14ef..321dcca570f 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -89,7 +89,7 @@ */ public class RestTemplate extends InterceptingHttpAccessor implements RestOperations { - private static boolean romePresent; + private static final boolean romePresent; private static final boolean jaxb2Present; @@ -200,6 +200,7 @@ public RestTemplate(List> messageConverters) { this.uriTemplateHandler = initUriTemplateHandler(); } + private static DefaultUriBuilderFactory initUriTemplateHandler() { DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory(); uriFactory.setEncodingMode(EncodingMode.URI_COMPONENT); // for backwards compatibility.. From 4e552625219dc324c5ceff7dc6383f0c86774d1a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Feb 2020 16:26:17 +0000 Subject: [PATCH 119/322] Upgrade to Reactor Californium snapshots See gh-24527 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 87ad6abb1c8..5703f4f8dab 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ ext { kotlinVersion = "1.2.71" log4jVersion = "2.11.2" nettyVersion = "4.1.44.Final" - reactorVersion = "Californium-SR15" + reactorVersion = "Californium-BUILD-SNAPSHOT" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" rxjava2Version = "2.2.17" @@ -148,6 +148,7 @@ configure(allprojects) { project -> repositories { maven { url "https://repo.spring.io/libs-release" } + maven { url "https://repo.spring.io/libs-snapshot" } // Reactor snapshots mavenLocal() } From 439ffe2e8ae113f6c2731e604979ddad6ba705e4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 20 Feb 2020 10:48:41 +0100 Subject: [PATCH 120/322] Convert non-UTF-8 JSON Jackson's asynchronous parser does not support any encoding except UTF-8 (or ASCII). This commit converts non-UTF-8/ASCII encoded JSON to UTF-8. Closes gh-24489 --- .../codec/json/AbstractJackson2Decoder.java | 23 +++++++++- .../http/codec/json/Jackson2JsonDecoder.java | 45 ++++++++++++++++++- .../codec/json/Jackson2JsonDecoderTests.java | 37 ++++++++++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index 98e22e3a055..c77e49263b1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -120,11 +120,29 @@ public Flux decode(Publisher input, ResolvableType elementTy forceUseOfBigDecimal = true; } - Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), + Flux processed = processInput(input, elementType, mimeType, hints); + Flux tokens = Jackson2Tokenizer.tokenize(processed, this.jsonFactory, getObjectMapper(), true, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints); } + /** + * Process the input publisher into a flux. Default implementation returns + * {@link Flux#from(Publisher)}, but subclasses can choose to to customize + * this behaviour. + * @param input the {@code DataBuffer} input stream to process + * @param elementType the expected type of elements in the output stream + * @param mimeType the MIME type associated with the input stream (optional) + * @param hints additional information about how to do encode + * @return the processed flux + * @since 5.1.14 + */ + protected Flux processInput(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return Flux.from(input); + } + @Override public Mono decodeToMono(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { @@ -134,7 +152,8 @@ public Mono decodeToMono(Publisher input, ResolvableType ele forceUseOfBigDecimal = true; } - Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), + Flux processed = processInput(input, elementType, mimeType, hints); + Flux tokens = Jackson2Tokenizer.tokenize(processed, this.jsonFactory, getObjectMapper(), false, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java index b9372ff5831..861fa05be26 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -16,10 +16,24 @@ package org.springframework.http.codec.json; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; + import com.fasterxml.jackson.databind.ObjectMapper; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.lang.Nullable; import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * Decode a byte stream into JSON and convert to Object's with Jackson 2.9, @@ -32,6 +46,11 @@ */ public class Jackson2JsonDecoder extends AbstractJackson2Decoder { + private static final StringDecoder STRING_DECODER = StringDecoder.textPlainOnly(Arrays.asList(",", "\n"), false); + + private static final ResolvableType STRING_TYPE = ResolvableType.forClass(String.class); + + public Jackson2JsonDecoder() { super(Jackson2ObjectMapperBuilder.json().build()); } @@ -40,4 +59,28 @@ public Jackson2JsonDecoder(ObjectMapper mapper, MimeType... mimeTypes) { super(mapper, mimeTypes); } + @Override + protected Flux processInput(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux flux = Flux.from(input); + if (mimeType == null) { + return flux; + } + + // Jackson asynchronous parser only supports UTF-8 + Charset charset = mimeType.getCharset(); + if (charset == null || StandardCharsets.UTF_8.equals(charset) || StandardCharsets.US_ASCII.equals(charset)) { + return flux; + } + + // Potentially, the memory consumption of this conversion could be improved by using CharBuffers instead + // of allocating Strings, but that would require refactoring the buffer tokenization code from StringDecoder + + MimeType textMimeType = new MimeType(MimeTypeUtils.TEXT_PLAIN, charset); + Flux decoded = STRING_DECODER.decode(input, STRING_TYPE, textMimeType, null); + DataBufferFactory factory = new DefaultDataBufferFactory(); + return decoded.map(s -> factory.wrap(s.getBytes(StandardCharsets.UTF_8))); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index e3b158ee73d..5252083c0b9 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.math.BigDecimal; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; @@ -34,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.codec.AbstractDecoderTestCase; import org.springframework.core.codec.CodecException; @@ -218,9 +220,42 @@ public void bigDecimalFlux() { ); } + @Test + public void decodeNonUtf8Encoding() { + Mono input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16); + + testDecode(input, ResolvableType.forType(new ParameterizedTypeReference>() {}), + step -> step.assertNext(o -> { + Map map = (Map) o; + assertEquals("bar", map.get("foo")); + }) + .verifyComplete(), + MediaType.parseMediaType("application/json; charset=utf-16"), + null); + } + + @Test + public void decodeMonoNonUtf8Encoding() { + Mono input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16); + + testDecodeToMono(input, ResolvableType.forType(new ParameterizedTypeReference>() { + }), + step -> step.assertNext(o -> { + Map map = (Map) o; + assertEquals("bar", map.get("foo")); + }) + .verifyComplete(), + MediaType.parseMediaType("application/json; charset=utf-16"), + null); + } + private Mono stringBuffer(String value) { + return stringBuffer(value, StandardCharsets.UTF_8); + } + + private Mono stringBuffer(String value, Charset charset) { return Mono.defer(() -> { - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + byte[] bytes = value.getBytes(charset); DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return Mono.just(buffer); From 10c9d2fcecb1e4d18586eb1592dee59da13751b5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 24 Feb 2020 14:57:34 +0000 Subject: [PATCH 121/322] Upgrade to Reactor Calfiornium-SR16 Closes gh-24527 --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5703f4f8dab..32fe9ebcf56 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ ext { kotlinVersion = "1.2.71" log4jVersion = "2.11.2" nettyVersion = "4.1.44.Final" - reactorVersion = "Californium-BUILD-SNAPSHOT" + reactorVersion = "Californium-SR16" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" rxjava2Version = "2.2.17" @@ -148,7 +148,6 @@ configure(allprojects) { project -> repositories { maven { url "https://repo.spring.io/libs-release" } - maven { url "https://repo.spring.io/libs-snapshot" } // Reactor snapshots mavenLocal() } From cc376ac5edeb970ebc7faf3e4534269c153fec4a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 24 Feb 2020 19:08:13 +0100 Subject: [PATCH 122/322] Clarify setCacheMillis/setCacheSeconds vs java.util.ResourceBundle Closes gh-24563 --- .../support/AbstractResourceBasedMessageSource.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java index d9b09d50cf9..d378dc003bf 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -160,8 +160,9 @@ protected boolean isFallbackToSystemLocale() { /** * Set the number of seconds to cache loaded properties files. *