Skip to content

Commit 7051cdd

Browse files
committed
Align OutputStreamPublisher's
Align internal handling and contracts. The core copy could do without those contracts, but it helps with alignment, and it's internal to the implementation. Closes gh-33592
1 parent f6c31bb commit 7051cdd

File tree

3 files changed

+99
-81
lines changed

3 files changed

+99
-81
lines changed

spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,27 +431,29 @@ static void closeChannel(@Nullable Channel channel) {
431431
* <li>Any exceptions thrown from {@code outputStreamHandler} will
432432
* be dispatched to the {@linkplain Subscriber#onError(Throwable) Subscriber}.
433433
* </ul>
434-
* @param outputStreamConsumer invoked when the first buffer is requested
434+
* @param consumer invoked when the first buffer is requested
435435
* @param executor used to invoke the {@code outputStreamHandler}
436436
* @return a {@code Publisher<DataBuffer>} based on bytes written by
437437
* {@code outputStreamHandler}
438438
* @since 6.1
439439
*/
440-
public static Publisher<DataBuffer> outputStreamPublisher(Consumer<OutputStream> outputStreamConsumer,
441-
DataBufferFactory bufferFactory, Executor executor) {
440+
public static Publisher<DataBuffer> outputStreamPublisher(
441+
Consumer<OutputStream> consumer, DataBufferFactory bufferFactory, Executor executor) {
442442

443-
return new OutputStreamPublisher(outputStreamConsumer, bufferFactory, executor, null);
443+
return new OutputStreamPublisher<>(
444+
consumer::accept, new DataBufferMapper(bufferFactory), executor, null);
444445
}
445446

446447
/**
447448
* Variant of {@link #outputStreamPublisher(Consumer, DataBufferFactory, Executor)}
448449
* providing control over the chunk sizes to be produced by the publisher.
449450
* @since 6.1
450451
*/
451-
public static Publisher<DataBuffer> outputStreamPublisher(Consumer<OutputStream> outputStreamConsumer,
452-
DataBufferFactory bufferFactory, Executor executor, int chunkSize) {
452+
public static Publisher<DataBuffer> outputStreamPublisher(
453+
Consumer<OutputStream> consumer, DataBufferFactory bufferFactory, Executor executor, int chunkSize) {
453454

454-
return new OutputStreamPublisher(outputStreamConsumer, bufferFactory, executor, chunkSize);
455+
return new OutputStreamPublisher<>(
456+
consumer::accept, new DataBufferMapper(bufferFactory), executor, chunkSize);
455457
}
456458

457459

@@ -1256,4 +1258,29 @@ public Context currentContext() {
12561258
private record Attachment(ByteBuffer byteBuffer, DataBuffer dataBuffer, DataBuffer.ByteBufferIterator iterator) {}
12571259
}
12581260

1261+
1262+
private static final class DataBufferMapper implements OutputStreamPublisher.ByteMapper<DataBuffer> {
1263+
1264+
private final DataBufferFactory bufferFactory;
1265+
1266+
private DataBufferMapper(DataBufferFactory bufferFactory) {
1267+
this.bufferFactory = bufferFactory;
1268+
}
1269+
1270+
@Override
1271+
public DataBuffer map(int b) {
1272+
DataBuffer buffer = this.bufferFactory.allocateBuffer(1);
1273+
buffer.write((byte) b);
1274+
return buffer;
1275+
}
1276+
1277+
@Override
1278+
public DataBuffer map(byte[] b, int off, int len) {
1279+
DataBuffer buffer = this.bufferFactory.allocateBuffer(len);
1280+
buffer.write(b, off, len);
1281+
return buffer;
1282+
}
1283+
1284+
}
1285+
12591286
}

spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.concurrent.atomic.AtomicLong;
2525
import java.util.concurrent.atomic.AtomicReference;
2626
import java.util.concurrent.locks.LockSupport;
27-
import java.util.function.Consumer;
2827

2928
import org.reactivestreams.Publisher;
3029
import org.reactivestreams.Subscriber;
@@ -36,21 +35,27 @@
3635
/**
3736
* Bridges between {@link OutputStream} and {@link Publisher Publisher&lt;DataBuffer&gt;}.
3837
*
38+
* <p>When there is demand on the Reactive Streams subscription, any write to
39+
* the OutputStream is mapped to a buffer and published to the subscriber.
40+
* If there is no demand, writes block until demand materializes.
41+
* If the subscription is cancelled, further writes raise {@code IOException}.
42+
*
3943
* <p>Note that this class has a near duplicate in
4044
* {@link org.springframework.http.client.OutputStreamPublisher}.
4145
*
4246
* @author Oleh Dokuka
4347
* @author Arjen Poutsma
4448
* @since 6.1
49+
* @param <T> the published byte buffer type
4550
*/
46-
final class OutputStreamPublisher implements Publisher<DataBuffer> {
51+
final class OutputStreamPublisher<T> implements Publisher<T> {
4752

4853
private static final int DEFAULT_CHUNK_SIZE = 1024;
4954

5055

51-
private final Consumer<OutputStream> outputStreamConsumer;
56+
private final OutputStreamHandler outputStreamHandler;
5257

53-
private final DataBufferFactory bufferFactory;
58+
private final ByteMapper<T> byteMapper;
5459

5560
private final Executor executor;
5661

@@ -59,50 +64,74 @@ final class OutputStreamPublisher implements Publisher<DataBuffer> {
5964

6065
/**
6166
* Create an instance.
62-
* @param outputStreamConsumer invoked when the first buffer is requested
63-
* @param bufferFactory to create data buffers with
67+
* @param outputStreamHandler invoked when the first buffer is requested
68+
* @param byteMapper maps written bytes to {@code T}
6469
* @param executor used to invoke the {@code outputStreamHandler}
6570
* @param chunkSize the chunk sizes to be produced by the publisher
6671
*/
6772
OutputStreamPublisher(
68-
Consumer<OutputStream> outputStreamConsumer, DataBufferFactory bufferFactory,
73+
OutputStreamHandler outputStreamHandler, ByteMapper<T> byteMapper,
6974
Executor executor, @Nullable Integer chunkSize) {
7075

71-
Assert.notNull(outputStreamConsumer, "OutputStreamConsumer must not be null");
72-
Assert.notNull(bufferFactory, "BufferFactory must not be null");
76+
Assert.notNull(outputStreamHandler, "OutputStreamHandler must not be null");
77+
Assert.notNull(byteMapper, "ByteMapper must not be null");
7378
Assert.notNull(executor, "Executor must not be null");
7479
Assert.isTrue(chunkSize == null || chunkSize > 0, "ChunkSize must be larger than 0");
7580

76-
this.outputStreamConsumer = outputStreamConsumer;
77-
this.bufferFactory = bufferFactory;
81+
this.outputStreamHandler = outputStreamHandler;
82+
this.byteMapper = byteMapper;
7883
this.executor = executor;
7984
this.chunkSize = (chunkSize != null ? chunkSize : DEFAULT_CHUNK_SIZE);
8085
}
8186

8287

8388
@Override
84-
public void subscribe(Subscriber<? super DataBuffer> subscriber) {
89+
public void subscribe(Subscriber<? super T> subscriber) {
8590
// We don't use Assert.notNull(), because a NullPointerException is required
8691
// for Reactive Streams compliance.
8792
Objects.requireNonNull(subscriber, "Subscriber must not be null");
8893

89-
OutputStreamSubscription subscription = new OutputStreamSubscription(
90-
subscriber, this.outputStreamConsumer, this.bufferFactory, this.chunkSize);
94+
OutputStreamSubscription<T> subscription = new OutputStreamSubscription<>(
95+
subscriber, this.outputStreamHandler, this.byteMapper, this.chunkSize);
9196

9297
subscriber.onSubscribe(subscription);
9398
this.executor.execute(subscription::invokeHandler);
9499
}
95100

96101

97-
private static final class OutputStreamSubscription extends OutputStream implements Subscription {
102+
/**
103+
* Contract to provide callback access to the {@link OutputStream}.
104+
*/
105+
@FunctionalInterface
106+
public interface OutputStreamHandler {
107+
108+
void handle(OutputStream outputStream) throws Exception;
109+
110+
}
111+
112+
113+
/**
114+
* Maps from bytes to byte buffers.
115+
* @param <T> the type of byte buffer to map to
116+
*/
117+
public interface ByteMapper<T> {
118+
119+
T map(int b);
120+
121+
T map(byte[] b, int off, int len);
122+
123+
}
124+
125+
126+
private static final class OutputStreamSubscription<T> extends OutputStream implements Subscription {
98127

99128
private static final Object READY = new Object();
100129

101-
private final Subscriber<? super DataBuffer> actual;
130+
private final Subscriber<? super T> actual;
102131

103-
private final Consumer<OutputStream> outputStreamHandler;
132+
private final OutputStreamHandler outputStreamHandler;
104133

105-
private final DataBufferFactory bufferFactory;
134+
private final ByteMapper<T> byteMapper;
106135

107136
private final int chunkSize;
108137

@@ -116,24 +145,20 @@ private static final class OutputStreamSubscription extends OutputStream impleme
116145
private long produced;
117146

118147
OutputStreamSubscription(
119-
Subscriber<? super DataBuffer> actual, Consumer<OutputStream> outputStreamConsumer,
120-
DataBufferFactory bufferFactory, int chunkSize) {
148+
Subscriber<? super T> actual, OutputStreamHandler outputStreamHandler,
149+
ByteMapper<T> byteMapper, int chunkSize) {
121150

122151
this.actual = actual;
123-
this.outputStreamHandler = outputStreamConsumer;
124-
this.bufferFactory = bufferFactory;
152+
this.outputStreamHandler = outputStreamHandler;
153+
this.byteMapper = byteMapper;
125154
this.chunkSize = chunkSize;
126155
}
127156

128157
@Override
129158
public void write(int b) throws IOException {
130159
checkDemandAndAwaitIfNeeded();
131-
132-
DataBuffer next = this.bufferFactory.allocateBuffer(1);
133-
next.write((byte) b);
134-
160+
T next = this.byteMapper.map(b);
135161
this.actual.onNext(next);
136-
137162
this.produced++;
138163
}
139164

@@ -145,12 +170,8 @@ public void write(byte[] b) throws IOException {
145170
@Override
146171
public void write(byte[] b, int off, int len) throws IOException {
147172
checkDemandAndAwaitIfNeeded();
148-
149-
DataBuffer next = this.bufferFactory.allocateBuffer(len);
150-
next.write(b, off, len);
151-
173+
T next = this.byteMapper.map(b, off, len);
152174
this.actual.onNext(next);
153-
154175
this.produced++;
155176
}
156177

@@ -190,7 +211,7 @@ private void invokeHandler() {
190211
// use BufferedOutputStream, so that written bytes are buffered
191212
// before publishing as byte buffer
192213
try (OutputStream outputStream = new BufferedOutputStream(this, this.chunkSize)) {
193-
this.outputStreamHandler.accept(outputStream);
214+
this.outputStreamHandler.handle(outputStream);
194215
}
195216
catch (Exception ex) {
196217
long previousState = tryTerminate();

spring-web/src/main/java/org/springframework/http/client/OutputStreamPublisher.java

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
/**
3333
* Bridges between {@link OutputStream} and {@link Flow.Publisher Flow.Publisher&lt;T&gt;}.
3434
*
35+
* <p>When there is demand on the Reactive Streams subscription, any write to
36+
* the OutputStream is mapped to a buffer and published to the subscriber.
37+
* If there is no demand, writes block until demand materializes.
38+
* If the subscription is cancelled, further writes raise {@code IOException}.
39+
*
3540
* <p>Note that this class has a near duplicate in
3641
* {@link org.springframework.core.io.buffer.OutputStreamPublisher}.
3742
*
@@ -92,61 +97,32 @@ public void subscribe(Flow.Subscriber<? super T> subscriber) {
9297

9398

9499
/**
95-
* Defines the contract for handling the {@code OutputStream} provided by
96-
* the {@code OutputStreamPublisher}.
100+
* Contract to provide callback access to the {@link OutputStream}.
97101
*/
98102
@FunctionalInterface
99103
public interface OutputStreamHandler {
100104

101-
/**
102-
* Use the given stream for writing.
103-
* <ul>
104-
* <li>If the linked subscription has
105-
* {@linkplain Flow.Subscription#request(long) demand}, any
106-
* {@linkplain OutputStream#write(byte[], int, int) written} bytes
107-
* will be {@linkplain ByteMapper#map(byte[], int, int) mapped}
108-
* and {@linkplain Flow.Subscriber#onNext(Object) published} to the
109-
* {@link Flow.Subscriber Subscriber}.</li>
110-
* <li>If there is no demand, any
111-
* {@link OutputStream#write(byte[], int, int) write()} invocations will
112-
* block until there is demand.</li>
113-
* <li>If the linked subscription is
114-
* {@linkplain Flow.Subscription#cancel() cancelled},
115-
* {@link OutputStream#write(byte[], int, int) write()} invocations will
116-
* result in a {@code IOException}.</li>
117-
* </ul>
118-
* @param outputStream the stream to write to
119-
* @throws IOException any thrown I/O errors will be dispatched to the
120-
* {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}
121-
*/
122-
void handle(OutputStream outputStream) throws IOException;
105+
void handle(OutputStream outputStream) throws Exception;
123106

124107
}
125108

126109

127110
/**
128-
* Maps bytes written to in {@link OutputStreamHandler#handle(OutputStream)}
129-
* to published items.
130-
* @param <T> the type to map to
111+
* Maps from bytes to byte buffers.
112+
* @param <T> the type of byte buffer to map to
131113
*/
132114
public interface ByteMapper<T> {
133115

134-
/**
135-
* Maps a single byte to {@code T}.
136-
*/
137116
T map(int b);
138117

139-
/**
140-
* Maps a byte array to {@code T}.
141-
*/
142118
T map(byte[] b, int off, int len);
143119

144120
}
145121

146122

147123
private static final class OutputStreamSubscription<T> extends OutputStream implements Flow.Subscription {
148124

149-
static final Object READY = new Object();
125+
private static final Object READY = new Object();
150126

151127
private final Flow.Subscriber<? super T> actual;
152128

@@ -178,11 +154,8 @@ private static final class OutputStreamSubscription<T> extends OutputStream impl
178154
@Override
179155
public void write(int b) throws IOException {
180156
checkDemandAndAwaitIfNeeded();
181-
182157
T next = this.byteMapper.map(b);
183-
184158
this.actual.onNext(next);
185-
186159
this.produced++;
187160
}
188161

@@ -194,11 +167,8 @@ public void write(byte[] b) throws IOException {
194167
@Override
195168
public void write(byte[] b, int off, int len) throws IOException {
196169
checkDemandAndAwaitIfNeeded();
197-
198170
T next = this.byteMapper.map(b, off, len);
199-
200171
this.actual.onNext(next);
201-
202172
this.produced++;
203173
}
204174

@@ -240,7 +210,7 @@ private void invokeHandler() {
240210
try (OutputStream outputStream = new BufferedOutputStream(this, this.chunkSize)) {
241211
this.outputStreamHandler.handle(outputStream);
242212
}
243-
catch (IOException ex) {
213+
catch (Exception ex) {
244214
long previousState = tryTerminate();
245215
if (isCancelled(previousState)) {
246216
return;

0 commit comments

Comments
 (0)