Spring Async Streaming: ResponseBodyEmitter, SseEmitter, and StreamingResponseBody Explained
This article explains three Spring mechanisms for asynchronous streaming responses—ResponseBodyEmitter for progressive data sending, SseEmitter for server-sent events with auto-reconnect, and StreamingResponseBody for large file streaming—with code examples and use cases for each.
ResponseBodyEmitter
ResponseBodyEmitteris suitable for scenarios where content is generated dynamically and sent to the client incrementally, such as file upload progress or real-time logs. It allows sending updates to the client during task execution.
For example, when using GPT, the answer appears gradually rather than all at once, creating a more natural interaction experience.
Note: If ResponseBodyEmitter timeout is set to 0 or -1 , the connection will not time out; otherwise, the default timeout will close the connection. The same applies to the other two tools.
@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
// Create a ResponseBodyEmitter, -1 means no timeout
ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
// Execute time-consuming operation asynchronously
CompletableFuture.runAsync(() -> {
try {
// Simulate time-consuming operation
for (int i = 0; i < 10000; i++) {
System.out.println("bodyEmitter " + i);
// Send data
emitter.send("bodyEmitter " + i + " @ " + new Date() + "
");
Thread.sleep(2000);
}
// Complete
emitter.complete();
} catch (Exception e) {
// End with error on exception
emitter.completeWithError(e);
}
});
return emitter;
}The implementation is straightforward. By simulating a response every 2 seconds, the page shows data being generated dynamically, similar to GPT's streaming output.
SseEmitter
SseEmitteris a subclass of ResponseBodyEmitter and also enables dynamic content generation. It is primarily used for server-to-client real-time data push, such as real-time message push and status updates. A previous article details Server-Sent Events (SSE) technology.
SSE opens a one-way channel between server and client. The server responds with a text/event-stream data stream instead of a one-time data packet, pushing updates to the client whenever data changes.
The concept resembles online video streaming: a continuous flow of data to the browser, akin to a long download.
On the client side, a single HTTP request establishes a connection and waits for messages. The server creates an SseEmitter object for each connection and sends messages through this channel.
<body>
<div id="content" style="text-align: center;">
<h1>SSE Receive Server-Sent Event Messages</h1>
<div id="message">Waiting for connection...</div>
</div>
<script>
let source = null;
let userId = 7777;
function setMessageInnerHTML(message) {
const messageDiv = document.getElementById("message");
const newParagraph = document.createElement("p");
newParagraph.textContent = message;
messageDiv.appendChild(newParagraph);
}
if (window.EventSource) {
// Establish connection
source = new EventSource('http://127.0.0.1:9033/subSseEmitter/' + userId);
setMessageInnerHTML("Connected user=" + userId);
/**
* Connection established triggers open event
* Alternative: source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("Connection established...");
}, false);
/**
* Client receives data from server
* Alternative: source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("Your browser does not support SSE");
}
</script>
</body>On the server side, we persist the SseEmitter sender objects so that when a message occurs, we can retrieve the corresponding emitter and call send to push.
private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();
@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
log.info("sseEmitter: {}", userId);
SseEmitter emitterTmp = new SseEmitter(-1L);
EMITTER_MAP.put(userId, emitterTmp);
CompletableFuture.runAsync(() -> {
try {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("sseEmitter" + userId + " @ " + LocalTime.now())
.id(String.valueOf(userId))
.name("sseEmitter");
emitterTmp.send(event);
} catch (Exception ex) {
emitterTmp.completeWithError(ex);
}
});
return emitterTmp;
}
@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
SseEmitter sseEmitter = EMITTER_MAP.get(userId);
if (sseEmitter == null) {
return;
}
sseEmitter.send(msg);
}Sending a message to userId=7777 via 127.0.0.1:9033/sendSseMsg/7777?msg=Welcome displays the message in real time on the page.
Moreover, SSE supports auto-reconnect : once a connection is established, even if the server restarts, the client can automatically reconnect.
StreamingResponseBody
StreamingResponseBodydiffers slightly from the other response handlers. It is mainly used for transmitting large data volumes or continuous data streams, supporting writing data directly to an OutputStream.
For instance, when downloading a very large file, StreamingResponseBody avoids loading the entire file into memory. Instead, it continuously streams the file to the client, solving the common memory overflow issue when downloading large files.
The interface returns a StreamingResponseBody object, writes data to the output stream, and flushes. Each flush call writes data to the client once.
@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {
StreamingResponseBody stream = out -> {
String message = "streamingResponse";
for (int i = 0; i < 1000; i++) {
try {
out.write(((message + i) + "
").getBytes());
out.write("
".getBytes());
// Each flush writes data to the frontend once
out.flush();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(stream);
}The demo outputs a simple text stream; for file downloads, converting to a file stream works the same way.
Summary
This article introduces three tools for implementing asynchronous streaming interfaces in Spring. They are simple to use and have no major pitfalls, but their application scenarios in real business are extensive. Through these tools, system performance and responsiveness can be effectively improved.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
