1
1
/*
2
- * Copyright 2002-2024 the original author or authors.
2
+ * Copyright 2002-2025 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
21
21
import java .util .LinkedHashSet ;
22
22
import java .util .List ;
23
23
import java .util .Set ;
24
- import java .util .concurrent .atomic .AtomicBoolean ;
24
+ import java .util .concurrent .atomic .AtomicReference ;
25
25
import java .util .function .Consumer ;
26
26
27
27
import org .springframework .http .MediaType ;
@@ -73,21 +73,20 @@ public class ResponseBodyEmitter {
73
73
@ Nullable
74
74
private Handler handler ;
75
75
76
+ private final AtomicReference <State > state = new AtomicReference <>(State .START );
77
+
76
78
/** Store send data before handler is initialized. */
77
79
private final Set <DataWithMediaType > earlySendAttempts = new LinkedHashSet <>(8 );
78
80
79
- /** Store successful completion before the handler is initialized. */
80
- private final AtomicBoolean complete = new AtomicBoolean ();
81
-
82
81
/** Store an error before the handler is initialized. */
83
82
@ Nullable
84
83
private Throwable failure ;
85
84
86
- private final DefaultCallback timeoutCallback = new DefaultCallback ();
85
+ private final TimeoutCallback timeoutCallback = new TimeoutCallback ();
87
86
88
87
private final ErrorCallback errorCallback = new ErrorCallback ();
89
88
90
- private final DefaultCallback completionCallback = new DefaultCallback ();
89
+ private final CompletionCallback completionCallback = new CompletionCallback ();
91
90
92
91
93
92
/**
@@ -128,7 +127,7 @@ synchronized void initialize(Handler handler) throws IOException {
128
127
this .earlySendAttempts .clear ();
129
128
}
130
129
131
- if (this .complete .get ()) {
130
+ if (this .state .get () == State . COMPLETE ) {
132
131
if (this .failure != null ) {
133
132
this .handler .completeWithError (this .failure );
134
133
}
@@ -144,7 +143,7 @@ synchronized void initialize(Handler handler) throws IOException {
144
143
}
145
144
146
145
void initializeWithError (Throwable ex ) {
147
- if (this .complete .compareAndSet (false , true )) {
146
+ if (this .state .compareAndSet (State . START , State . COMPLETE )) {
148
147
this .failure = ex ;
149
148
this .earlySendAttempts .clear ();
150
149
this .errorCallback .accept (ex );
@@ -186,8 +185,7 @@ public void send(Object object) throws IOException {
186
185
* @throws java.lang.IllegalStateException wraps any other errors
187
186
*/
188
187
public synchronized void send (Object object , @ Nullable MediaType mediaType ) throws IOException {
189
- Assert .state (!this .complete .get (), () -> "ResponseBodyEmitter has already completed" +
190
- (this .failure != null ? " with error: " + this .failure : "" ));
188
+ assertNotComplete ();
191
189
if (this .handler != null ) {
192
190
try {
193
191
this .handler .send (object , mediaType );
@@ -214,11 +212,15 @@ public synchronized void send(Object object, @Nullable MediaType mediaType) thro
214
212
* @since 6.0.12
215
213
*/
216
214
public synchronized void send (Set <DataWithMediaType > items ) throws IOException {
217
- Assert .state (!this .complete .get (), () -> "ResponseBodyEmitter has already completed" +
218
- (this .failure != null ? " with error: " + this .failure : "" ));
215
+ assertNotComplete ();
219
216
sendInternal (items );
220
217
}
221
218
219
+ private void assertNotComplete () {
220
+ Assert .state (this .state .get () == State .START , () -> "ResponseBodyEmitter has already completed" +
221
+ (this .failure != null ? " with error: " + this .failure : "" ));
222
+ }
223
+
222
224
private void sendInternal (Set <DataWithMediaType > items ) throws IOException {
223
225
if (items .isEmpty ()) {
224
226
return ;
@@ -248,7 +250,7 @@ private void sendInternal(Set<DataWithMediaType> items) throws IOException {
248
250
* related events such as an error while {@link #send(Object) sending}.
249
251
*/
250
252
public void complete () {
251
- if (this . complete . compareAndSet ( false , true ) && this .handler != null ) {
253
+ if (trySetComplete ( ) && this .handler != null ) {
252
254
this .handler .complete ();
253
255
}
254
256
}
@@ -265,14 +267,19 @@ public void complete() {
265
267
* {@link #send(Object) sending}.
266
268
*/
267
269
public void completeWithError (Throwable ex ) {
268
- if (this . complete . compareAndSet ( false , true )) {
270
+ if (trySetComplete ( )) {
269
271
this .failure = ex ;
270
272
if (this .handler != null ) {
271
273
this .handler .completeWithError (ex );
272
274
}
273
275
}
274
276
}
275
277
278
+ private boolean trySetComplete () {
279
+ return (this .state .compareAndSet (State .START , State .COMPLETE ) ||
280
+ (this .state .compareAndSet (State .TIMEOUT , State .COMPLETE )));
281
+ }
282
+
276
283
/**
277
284
* Register code to invoke when the async request times out. This method is
278
285
* called from a container thread when an async request times out.
@@ -369,7 +376,7 @@ public MediaType getMediaType() {
369
376
}
370
377
371
378
372
- private class DefaultCallback implements Runnable {
379
+ private class TimeoutCallback implements Runnable {
373
380
374
381
private final List <Runnable > delegates = new ArrayList <>(1 );
375
382
@@ -379,9 +386,10 @@ public synchronized void addDelegate(Runnable delegate) {
379
386
380
387
@ Override
381
388
public void run () {
382
- ResponseBodyEmitter .this .complete .compareAndSet (false , true );
383
- for (Runnable delegate : this .delegates ) {
384
- delegate .run ();
389
+ if (ResponseBodyEmitter .this .state .compareAndSet (State .START , State .TIMEOUT )) {
390
+ for (Runnable delegate : this .delegates ) {
391
+ delegate .run ();
392
+ }
385
393
}
386
394
}
387
395
}
@@ -397,11 +405,51 @@ public synchronized void addDelegate(Consumer<Throwable> callback) {
397
405
398
406
@ Override
399
407
public void accept (Throwable t ) {
400
- ResponseBodyEmitter .this .complete .compareAndSet (false , true );
401
- for (Consumer <Throwable > delegate : this .delegates ) {
402
- delegate .accept (t );
408
+ if (ResponseBodyEmitter .this .state .compareAndSet (State .START , State .COMPLETE )) {
409
+ for (Consumer <Throwable > delegate : this .delegates ) {
410
+ delegate .accept (t );
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+
417
+ private class CompletionCallback implements Runnable {
418
+
419
+ private final List <Runnable > delegates = new ArrayList <>(1 );
420
+
421
+ public synchronized void addDelegate (Runnable delegate ) {
422
+ this .delegates .add (delegate );
423
+ }
424
+
425
+ @ Override
426
+ public void run () {
427
+ if (ResponseBodyEmitter .this .state .compareAndSet (State .START , State .COMPLETE )) {
428
+ for (Runnable delegate : this .delegates ) {
429
+ delegate .run ();
430
+ }
403
431
}
404
432
}
405
433
}
406
434
435
+
436
+ /**
437
+ * Represents a state for {@link ResponseBodyEmitter}.
438
+ * <p><pre>
439
+ * START ----+
440
+ * | |
441
+ * v |
442
+ * TIMEOUT |
443
+ * | |
444
+ * v |
445
+ * COMPLETE <--+
446
+ * </pre>
447
+ * @since 6.2.4
448
+ */
449
+ private enum State {
450
+ START ,
451
+ TIMEOUT , // handling a timeout
452
+ COMPLETE
453
+ }
454
+
407
455
}
0 commit comments