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 1fd2c1a8f3..e8de69083d 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 @@ -33,6 +33,7 @@ import java.nio.file.StandardOpenOption; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executor; +import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -457,23 +458,19 @@ public abstract class DataBufferUtils { } /** - * Subscribes to given {@link Publisher} and returns subscription - * as {@link InputStream} that allows reading all propagated {@link DataBuffer} messages via its imperative API. - * Given the {@link InputStream} implementation buffers messages as per configuration. - * The returned {@link InputStream} is considered terminated when the given {@link Publisher} signaled one of the - * terminal signal ({@link Subscriber#onComplete() or {@link Subscriber#onError(Throwable)}}) - * and all the stored {@link DataBuffer} polled from the internal buffer. - * The returned {@link InputStream} will call {@link Subscription#cancel()} and release all stored {@link DataBuffer} - * when {@link InputStream#close()} is called. - *

- * Note: The implementation of the returned {@link InputStream} disallow concurrent call on - * any of the {@link InputStream#read} methods - *

- * Note: {@link Subscription#request(long)} happens eagerly for the first time upon subscription - * and then repeats every time {@code bufferSize - (bufferSize >> 2)} consumed. - * @param publisher the source of {@link DataBuffer} which should be represented as an {@link InputStream} - * @param demand the maximum number of buffers to request from the Publisher and buffer on an ongoing basis - * @return an {@link InputStream} instance representing given {@link Publisher} messages + * Subscribe to given {@link Publisher} of {@code DataBuffer}s, and return an + * {@link InputStream} to consume the byte content with. + *

Byte buffers are stored in a queue. The {@code demand} constructor value + * determines the number of buffers requested initially. When storage falls + * below a {@code (demand - (demand >> 2))} limit, a request is made to refill + * the queue. + *

The {@code InputStream} terminates after an onError or onComplete signal, + * and stored buffers are read. If the {@code InputStream} is closed, + * the {@link Flow.Subscription} is cancelled, and stored buffers released. + * @param publisher the source of {@code DataBuffer}s + * @param demand the number of buffers to request initially, and buffer + * internally on an ongoing basis. + * @return an {@link InputStream} backed by the {@link Publisher} */ public static InputStream subscriberInputStream(Publisher publisher, int demand) { Assert.notNull(publisher, "Publisher must not be null"); diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java b/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java index a4d9e465f4..4fc6d0eae7 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java @@ -22,12 +22,12 @@ import java.util.ConcurrentModificationException; import java.util.Objects; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.ReentrantLock; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.Exceptions; @@ -36,7 +36,17 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Bridges between {@link Publisher Publisher<DataBuffer>} and {@link InputStream}. + * An {@link InputStream} backed by {@link Flow.Subscriber Flow.Subscriber} + * receiving byte buffers from a {@link Flow.Publisher} source. + * + *

Byte buffers are stored in a queue. The {@code demand} constructor value + * determines the number of buffers requested initially. When storage falls + * below a {@code (demand - (demand >> 2))} limit, a request is made to refill + * the queue. + * + *

The {@code InputStream} terminates after an onError or onComplete signal, + * and stored buffers are read. If the {@code InputStream} is closed, + * the {@link Flow.Subscription} is cancelled, and stored buffers released. * *

Note that this class has a near duplicate in * {@link org.springframework.http.client.SubscriberInputStream}. @@ -82,6 +92,11 @@ final class SubscriberInputStream extends InputStream implements Subscriber> 2)); diff --git a/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java b/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java index c114c83584..ec3834d717 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java +++ b/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java @@ -34,12 +34,21 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.Exceptions; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Bridges between {@link Flow.Publisher Flow.Publisher<T>} and {@link InputStream}. + * An {@link InputStream} backed by {@link Flow.Subscriber Flow.Subscriber} + * receiving byte buffers from a {@link Flow.Publisher} source. + * + *

Byte buffers are stored in a queue. The {@code demand} constructor value + * determines the number of buffers requested initially. When storage falls + * below a {@code (demand - (demand >> 2))} limit, a request is made to refill + * the queue. + * + *

The {@code InputStream} terminates after an onError or onComplete signal, + * and stored buffers are read. If the {@code InputStream} is closed, + * the {@link Flow.Subscription} is cancelled, and stored buffers released. * *

Note that this class has a near duplicate in * {@link org.springframework.core.io.buffer.SubscriberInputStream}. @@ -94,7 +103,20 @@ final class SubscriberInputStream extends InputStream implements Flow.Subscri private Throwable error; - private SubscriberInputStream(Function mapper, Consumer onDiscardHandler, int demand) { + /** + * Create an instance. + * @param mapper function to transform byte buffers to {@code byte[]}; + * the function should also release the byte buffer if necessary. + * @param onDiscardHandler a callback to release byte buffers if the + * {@link InputStream} is closed prematurely. + * @param demand the number of buffers to request initially, and buffer + * internally on an ongoing basis. + */ + SubscriberInputStream(Function mapper, Consumer onDiscardHandler, int demand) { + Assert.notNull(mapper, "mapper must not be null"); + Assert.notNull(onDiscardHandler, "onDiscardHandler must not be null"); + Assert.isTrue(demand > 0, "demand must be greater than 0"); + this.mapper = mapper; this.onDiscardHandler = onDiscardHandler; this.prefetch = demand; @@ -104,38 +126,6 @@ final class SubscriberInputStream extends InputStream implements Flow.Subscri } - /** - * Subscribes to given {@link Flow.Publisher} and returns subscription - * as {@link InputStream} that allows reading all propagated {@link DataBuffer} messages via its imperative API. - * Given the {@link InputStream} implementation buffers messages as per configuration. - * The returned {@link InputStream} is considered terminated when the given {@link Flow.Publisher} signaled one of the - * terminal signal ({@link Flow.Subscriber#onComplete() or {@link Flow.Subscriber#onError(Throwable)}}) - * and all the stored {@link DataBuffer} polled from the internal buffer. - * The returned {@link InputStream} will call {@link Flow.Subscription#cancel()} and release all stored {@link DataBuffer} - * when {@link InputStream#close()} is called. - *

- * Note: The implementation of the returned {@link InputStream} disallow concurrent call on - * any of the {@link InputStream#read} methods - *

- * Note: {@link Flow.Subscription#request(long)} happens eagerly for the first time upon subscription - * and then repeats every time {@code bufferSize - (bufferSize >> 2)} consumed. - * @param publisher the source of {@link DataBuffer} which should be represented as an {@link InputStream} - * @param mapper function to transform <T> element to {@code byte[]}. Note, <T> should be released during the mapping if needed. - * @param onDiscardHandler <T> element consumer if returned {@link InputStream} is closed prematurely. - * @param demand the maximum number of buffers to request from the Publisher and buffer on an ongoing basis - * @return an {@link InputStream} instance representing given {@link Flow.Publisher} messages - */ - public static InputStream subscribeTo(Flow.Publisher publisher, Function mapper, Consumer onDiscardHandler, int demand) { - Assert.notNull(publisher, "Flow.Publisher must not be null"); - Assert.notNull(mapper, "mapper must not be null"); - Assert.notNull(onDiscardHandler, "onDiscardHandler must not be null"); - Assert.isTrue(demand > 0, "demand must be greater than 0"); - - SubscriberInputStream iss = new SubscriberInputStream<>(mapper, onDiscardHandler, demand); - publisher.subscribe(iss); - return iss; - } - @Override public void onSubscribe(Flow.Subscription subscription) { if (this.subscription != null) { @@ -222,7 +212,7 @@ final class SubscriberInputStream extends InputStream implements Flow.Subscri if (this.parkedThread != READY) { Object old = this.parkedThread.getAndSet(READY); if (old != READY) { - LockSupport.unpark((Thread)old); + LockSupport.unpark((Thread) old); } } } diff --git a/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java b/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java index 634885da00..1caa91d2bb 100644 --- a/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java @@ -17,7 +17,6 @@ package org.springframework.http.client; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.List; @@ -28,7 +27,6 @@ import java.util.concurrent.Flow; import org.junit.jupiter.api.Test; import org.reactivestreams.FlowAdapters; -import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import static java.nio.charset.StandardCharsets.UTF_8; @@ -79,8 +77,8 @@ class SubscriberInputStreamTests { }, this.byteMapper, this.executor, null); - StepVerifier.create(toStringFlux(publisher)) - .assertNext(s -> assertThat(s).isEqualTo("foobarbaz")) + StepVerifier.create(FlowAdapters.toPublisher(publisher)) + .assertNext(s -> assertThat(s).containsExactly("foobarbaz".getBytes(UTF_8))) .verifyComplete(); } @@ -98,17 +96,20 @@ class SubscriberInputStreamTests { this.byteMapper, this.executor, null); - try (InputStream is = SubscriberInputStream.subscribeTo( - toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) { + try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) { + publisher.subscribe(is); byte[] chunk = new byte[3]; assertThat(is.read(chunk)).isEqualTo(3); assertThat(chunk).containsExactly(FOO); + assertThat(is.read(chunk)).isEqualTo(3); assertThat(chunk).containsExactly(BAR); + assertThat(is.read(chunk)).isEqualTo(3); assertThat(chunk).containsExactly(BAZ); + assertThat(is.read(chunk)).isEqualTo(-1); } } @@ -123,8 +124,8 @@ class SubscriberInputStreamTests { }, this.byteMapper, this.executor, 2); - try (InputStream is = SubscriberInputStream.subscribeTo( - toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) { + try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) { + publisher.subscribe(is); StringBuilder stringBuilder = new StringBuilder(); byte[] chunk = new byte[3]; @@ -148,7 +149,7 @@ class SubscriberInputStreamTests { } @Test - void cancel() throws InterruptedException { + void cancel() throws InterruptedException, IOException { CountDownLatch latch = new CountDownLatch(1); Flow.Publisher publisher = new OutputStreamPublisher<>( @@ -165,27 +166,23 @@ class SubscriberInputStreamTests { }, this.byteMapper, this.executor, null); - List discarded = new ArrayList<>(); - - try (InputStream is = SubscriberInputStream.subscribeTo( - toStringPublisher(publisher), s -> s.getBytes(UTF_8), discarded::add, 1)) { + List discarded = new ArrayList<>(); + try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, discarded::add, 1)) { + publisher.subscribe(is); byte[] chunk = new byte[3]; assertThat(is.read(chunk)).isEqualTo(3); assertThat(chunk).containsExactly(FOO); } - catch (IOException e) { - throw new RuntimeException(e); - } latch.await(); - assertThat(discarded).containsExactly("bar"); + assertThat(discarded).containsExactly("bar".getBytes(UTF_8)); } @Test - void closed() throws InterruptedException { + void closed() throws InterruptedException, IOException { CountDownLatch latch = new CountDownLatch(1); Flow.Publisher publisher = new OutputStreamPublisher<>( @@ -198,18 +195,14 @@ class SubscriberInputStreamTests { }, this.byteMapper, this.executor, null); - try (InputStream is = SubscriberInputStream.subscribeTo( - toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) { - + try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) { + publisher.subscribe(is); byte[] chunk = new byte[3]; assertThat(is.read(chunk)).isEqualTo(3); assertThat(chunk).containsExactly(FOO); assertThat(is.read(chunk)).isEqualTo(-1); } - catch (IOException e) { - throw new RuntimeException(e); - } latch.await(); } @@ -234,19 +227,11 @@ class SubscriberInputStreamTests { Throwable savedEx = null; StringBuilder sb = new StringBuilder(); - try (InputStream is = SubscriberInputStream.subscribeTo( - publisher, s -> { throw new NullPointerException("boom"); }, s -> {}, 1)) { + try (SubscriberInputStream is = new SubscriberInputStream<>( + s -> { throw new NullPointerException("boom"); }, s -> {}, 1)) { - byte[] chunk = new byte[3]; - - sb.append(new String(new byte[]{(byte)is.read()}, UTF_8)); - assertThat(is.read(chunk)).isEqualTo(3); - sb.append(new String(chunk, UTF_8)); - assertThat(is.read(chunk)).isEqualTo(3); - sb.append(new String(chunk, UTF_8)); - assertThat(is.read(chunk)).isEqualTo(2); - sb.append(new String(chunk,0, 2, UTF_8)); - assertThat(is.read()).isEqualTo(-1); + publisher.subscribe(is); + sb.append(new String(new byte[] {(byte) is.read()}, UTF_8)); } catch (Throwable ex) { savedEx = ex; @@ -258,12 +243,4 @@ class SubscriberInputStreamTests { assertThat(savedEx).hasMessage("boom"); } - private static Flow.Publisher toStringPublisher(Flow.Publisher publisher) { - return FlowAdapters.toFlowPublisher(toStringFlux(publisher)); - } - - private static Flux toStringFlux(Flow.Publisher publisher) { - return Flux.from(FlowAdapters.toPublisher(publisher)).map(bytes -> new String(bytes, UTF_8)); - } - }