diff --git a/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java index a690dff3d4..5061cd121c 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java @@ -29,9 +29,15 @@ import org.springframework.util.MimeTypeUtils; /** * Simple pass-through decoder for {@link DataBuffer DataBuffers}. - *

Note that the "decoded" buffers returned by instances of this class should - * be released after usage by calling - * {@link org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer)}. + * + *

Note: The data buffers should be released via + * {@link org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer)} + * after they have been consumed. In addition, if using {@code Flux} or + * {@code Mono} operators such as flatMap, reduce, and others that prefetch, + * cache, and skip or filter out data items internally, please add + * {@code doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)} to the + * composition chain to ensure cached data buffers are released prior to an + * error or cancellation signal. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -48,7 +54,8 @@ public class DataBufferDecoder extends AbstractDataBufferDecoder { @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { Class clazz = elementType.getRawClass(); - return (super.canDecode(elementType, mimeType) && clazz != null && DataBuffer.class.isAssignableFrom(clazz)); + return (super.canDecode(elementType, mimeType) && + clazz != null && DataBuffer.class.isAssignableFrom(clazz)); } @Override 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 39e29fdad3..6525afcb0b 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 @@ -27,7 +27,6 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; @@ -36,7 +35,6 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.core.log.LogFormatUtils; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -59,25 +57,26 @@ public final class StringDecoder extends AbstractDataBufferDecoder { private static final DataBuffer END_FRAME = new DefaultDataBufferFactory().wrap(new byte[0]); - /** - * The default charset to use, i.e. "UTF-8". - */ + /** The default charset to use, i.e. "UTF-8". */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - /** - * The default delimiter strings to use, i.e. {@code \n} and {@code \r\n}. - */ + /** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */ public static final List DEFAULT_DELIMITERS = Arrays.asList("\r\n", "\n"); + private static final List DEFAULT_DELIMITER_BYTES = DEFAULT_DELIMITERS.stream() + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + @Nullable private final List delimiters; private final boolean stripDelimiter; - private StringDecoder(List delimiters, boolean stripDelimiter, MimeType... mimeTypes) { + + private StringDecoder(@Nullable List delimiters, boolean stripDelimiter, MimeType... mimeTypes) { super(mimeTypes); - Assert.notEmpty(delimiters, "'delimiters' must not be empty"); - this.delimiters = new ArrayList<>(delimiters); + this.delimiters = delimiters != null ? new ArrayList<>(delimiters) : null; this.stripDelimiter = stripDelimiter; } @@ -92,36 +91,32 @@ public final class StringDecoder extends AbstractDataBufferDecoder { public Flux decode(Publisher inputStream, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - List delimiterBytes = getDelimiterBytes(mimeType); + List delimiterBytes = this.delimiters != null ? + this.delimiters.stream().map(s -> s.getBytes(getCharset(mimeType))).collect(Collectors.toList()) : + DEFAULT_DELIMITER_BYTES; Flux inputFlux = Flux.from(inputStream) - .flatMap(dataBuffer -> splitOnDelimiter(dataBuffer, delimiterBytes)) + .flatMapIterable(dataBuffer -> splitOnDelimiter(dataBuffer, delimiterBytes)) .bufferUntil(StringDecoder::isEndFrame) - .flatMap(StringDecoder::joinUntilEndFrame) + .map(StringDecoder::joinUntilEndFrame) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); - return super.decode(inputFlux, elementType, mimeType, hints); - } - private List getDelimiterBytes(@Nullable MimeType mimeType) { - Charset charset = getCharset(mimeType); - return this.delimiters.stream() - .map(s -> s.getBytes(charset)) - .collect(Collectors.toList()); + return super.decode(inputFlux, elementType, mimeType, hints); } /** * Splits the given data buffer on delimiter boundaries. The returned Flux contains a * {@link #END_FRAME} buffer after each delimiter. */ - private Flux splitOnDelimiter(DataBuffer dataBuffer, List delimiterBytes) { + private List splitOnDelimiter(DataBuffer dataBuffer, List delimiterBytes) { List frames = new ArrayList<>(); do { int length = Integer.MAX_VALUE; byte[] matchingDelimiter = null; for (byte[] delimiter : delimiterBytes) { - int idx = indexOf(dataBuffer, delimiter); - if (idx >= 0 && idx < length) { - length = idx; + int index = indexOf(dataBuffer, delimiter); + if (index >= 0 && index < length) { + length = index; matchingDelimiter = delimiter; } } @@ -148,12 +143,12 @@ public final class StringDecoder extends AbstractDataBufferDecoder { while (dataBuffer.readableByteCount() > 0); DataBufferUtils.release(dataBuffer); - return Flux.fromIterable(frames); + return frames; } /** - * Finds the given delimiter in the given data buffer. Return the index of the delimiter, or - * -1 if not found. + * Find the given delimiter in the given data buffer. + * @return the index of the delimiter, or -1 if not found. */ private static int indexOf(DataBuffer dataBuffer, byte[] delimiter) { for (int i = dataBuffer.readPosition(); i < dataBuffer.writePosition(); i++) { @@ -172,7 +167,6 @@ public final class StringDecoder extends AbstractDataBufferDecoder { } delimiterPos++; } - if (delimiterPos == delimiter.length) { return i - dataBuffer.readPosition(); } @@ -188,17 +182,17 @@ public final class StringDecoder extends AbstractDataBufferDecoder { } /** - * Joins the given list of buffers into a single buffer. + * Joins the given list of buffers into a single buffer, also removing + * the (inserted) {@link #END_FRAME}. */ - private static Mono joinUntilEndFrame(List dataBuffers) { + private static DataBuffer joinUntilEndFrame(List dataBuffers) { if (!dataBuffers.isEmpty()) { int lastIdx = dataBuffers.size() - 1; if (isEndFrame(dataBuffers.get(lastIdx))) { dataBuffers.remove(lastIdx); } } - Flux flux = Flux.fromIterable(dataBuffers); - return DataBufferUtils.join(flux); + return dataBuffers.get(0).factory().join(dataBuffers); } @Override @@ -241,15 +235,18 @@ public final class StringDecoder extends AbstractDataBufferDecoder { * Create a {@code StringDecoder} for {@code "text/plain"}. */ public static StringDecoder textPlainOnly() { - return textPlainOnly(DEFAULT_DELIMITERS, true); + return textPlainOnly(null, true); } /** * Create a {@code StringDecoder} for {@code "text/plain"}. + * @param delimiters delimiter strings to use to split the input stream, if + * {@code null} by default {@link #DEFAULT_DELIMITERS} is used. + * @param stripDelimiter whether to remove delimiters from the resulting + * input strings. */ - public static StringDecoder textPlainOnly(List delimiters, boolean stripDelimiter) { - return new StringDecoder(delimiters, stripDelimiter, - new MimeType("text", "plain", DEFAULT_CHARSET)); + public static StringDecoder textPlainOnly(@Nullable List delimiters, boolean stripDelimiter) { + return new StringDecoder(delimiters, stripDelimiter, new MimeType("text", "plain", DEFAULT_CHARSET)); } /** @@ -267,16 +264,19 @@ public final class StringDecoder extends AbstractDataBufferDecoder { * Create a {@code StringDecoder} that supports all MIME types. */ public static StringDecoder allMimeTypes() { - return allMimeTypes(DEFAULT_DELIMITERS, true); + return allMimeTypes(null, true); } /** * Create a {@code StringDecoder} that supports all MIME types. + * @param delimiters delimiter strings to use to split the input stream, if + * {@code null} by default {@link #DEFAULT_DELIMITERS} is used. + * @param stripDelimiter whether to remove delimiters from the resulting + * input strings. */ - public static StringDecoder allMimeTypes(List delimiters, boolean stripDelimiter) { + public static StringDecoder allMimeTypes(@Nullable List delimiters, boolean stripDelimiter) { return new StringDecoder(delimiters, stripDelimiter, new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL); } - } 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 a24b5f9cb2..848334a78d 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 @@ -422,13 +422,16 @@ public abstract class DataBufferUtils { } /** - * Return a new {@code DataBuffer} composed of the {@code dataBuffers} elements joined together. - * Depending on the {@link DataBuffer} implementation, the returned buffer may be a single - * buffer containing all data of the provided buffers, or it may be a true composite that - * contains references to the buffers. - *

If {@code dataBuffers} contains an error signal, then all buffers that preceded the error - * will be {@linkplain #release(DataBuffer) released}, and the error is stored in the - * returned {@code Mono}. + * Return a new {@code DataBuffer} composed from joining together the given + * {@code dataBuffers} elements. Depending on the {@link DataBuffer} type, + * the returned buffer may be a single buffer containing all data of the + * provided buffers, or it may be a zero-copy, composite with references to + * the given buffers. + *

If {@code dataBuffers} produces an error or if there is a cancel + * signal, then all accumulated buffers will be + * {@linkplain #release(DataBuffer) released}. + *

Note that the given data buffers do not have to be + * released. They will be released as part of the returned composite. * @param dataBuffers the data buffers that are to be composed * @return a buffer that is composed from the {@code dataBuffers} argument * @since 5.0.3 @@ -439,10 +442,7 @@ public abstract class DataBufferUtils { return Flux.from(dataBuffers) .collectList() .filter(list -> !list.isEmpty()) - .map(list -> { - DataBufferFactory bufferFactory = list.get(0).factory(); - return bufferFactory.join(list); - }) + .map(list -> list.get(0).factory().join(list)) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java index 18b55a5e57..c123635444 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java @@ -92,8 +92,7 @@ public class NettyDataBufferFactory implements DataBufferFactory { CompositeByteBuf composite = this.byteBufAllocator.compositeBuffer(dataBuffers.size()); for (DataBuffer dataBuffer : dataBuffers) { Assert.isInstanceOf(NettyDataBuffer.class, dataBuffer); - NettyDataBuffer nettyDataBuffer = (NettyDataBuffer) dataBuffer; - composite.addComponent(true, nettyDataBuffer.getNativeBuffer()); + composite.addComponent(true, ((NettyDataBuffer) dataBuffer).getNativeBuffer()); } return new NettyDataBuffer(composite, this); }