Three Core Ways to Implement Asynchronous Streaming Responses in Spring Boot
This article explains three Spring Boot 3.5.0 techniques—ResponseBodyEmitter, SseEmitter, and StreamingResponseBody—for delivering multiple asynchronous values as a stream, complete with code samples, curl demonstrations, and a front‑end EventSource example.
Environment: Spring Boot 3.5.0.
1. Introduction
In a Spring application a single asynchronous return value can be handled with DeferredResult or Callable. The following example shows a minimal endpoint that returns a DeferredResult<String> which will be completed by another thread.
@GetMapping("/message")
@ResponseBody
public DeferredResult<String> asyncMessage() {
DeferredResult<String> deferredResult = new DeferredResult<>();
return deferredResult;
}
// other thread sets the final result
deferredResult.setResult(result);When a use case requires sending multiple asynchronous values—such as real‑time push— DeferredResult and Callable are insufficient. Spring provides three core implementations that can write multiple values to the HTTP response.
2. Practical Implementations
2.1 ResponseBodyEmitter
ResponseBodyEmitterstreams objects that are serialized by an HttpMessageConverter. It can be returned directly or wrapped in ResponseEntity.
@RestController
@RequestMapping("/streams")
public class AsyncStreamController {
private final Map<Object, ResponseBodyEmitter> bodyEmitter = new ConcurrentHashMap<>();
@GetMapping("/bodyemitter/{id}")
public ResponseBodyEmitter body(@PathVariable String id) throws Exception {
ResponseBodyEmitter emitter = this.bodyEmitter.computeIfAbsent(id, key -> {
ResponseBodyEmitter rbe = new ResponseBodyEmitter();
rbe.onCompletion(() -> this.bodyEmitter.remove(id));
return rbe;
});
return emitter;
}
@GetMapping("/send/{id}")
public void send(@PathVariable String id) throws Exception {
ResponseBodyEmitter emitter = this.bodyEmitter.get(id);
if (emitter != null) {
emitter.send(
Map.of("code", 0, "data", "T - %s".formatted(System.currentTimeMillis())),
MediaType.APPLICATION_JSON);
}
}
@GetMapping("/complete/{id}")
public void complete(@PathVariable String id) throws Exception {
ResponseBodyEmitter emitter = this.bodyEmitter.get(id);
if (emitter != null) {
emitter.complete();
}
}
}Command‑line curl can be used to receive the streamed data. The screenshots below illustrate the interaction:
2.2 SseEmitter
SseEmitter(a subclass of ResponseBodyEmitter) implements the Server‑Sent Events (SSE) specification. It can be used to push events that conform to the W3C SSE format.
private final Map<Object, SseEmitter> sseEmitter = new ConcurrentHashMap<>();
@GetMapping(path="/sse/{id}", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle(@PathVariable String id) {
SseEmitter emitter = this.sseEmitter.computeIfAbsent(id, key -> {
SseEmitter sse = new SseEmitter();
sse.onCompletion(() -> this.sseEmitter.remove(id));
sse.onError(System.err::println);
return sse;
});
return emitter;
}
@GetMapping("/msg/{id}")
public void msg(@PathVariable String id) throws Exception {
SseEmitter sse = this.sseEmitter.get(id);
if (sse != null) {
sse.send(
Map.of("code", 0, "data", "T - %s".formatted(System.currentTimeMillis())),
MediaType.APPLICATION_JSON);
}
}The front‑end page uses the native EventSource API to consume the SSE stream. The essential HTML/JavaScript is shown below.
<body>
<div class="container">
<h1>Server‑Sent Events (SSE) Demo</h1>
<div class="button-container">
<div class="input-group">
<input type="text" id="username" placeholder="Enter username" autocomplete="off">
</div>
<button class="btn-primary" type="button" onclick="connect()" id="connectBtn">Connect</button>
<button class="btn-danger" type="button" onclick="closeSse()" id="disconnectBtn" disabled>Close Connection</button>
</div>
<div class="status" id="status">Waiting for connection...</div>
<hr/>
<ul id="list"></ul>
</div>
<script>
const statusElement = document.querySelector("#status");
const eventList = document.querySelector("#list");
const disconnectBtn = document.querySelector("#disconnectBtn");
const userName = document.querySelector("#username");
let eventSource;
function connect() {
eventSource = new EventSource(`/streams/sse/${userName.value}`);
eventSource.onmessage = event => {
const li = document.createElement("li");
li.innerHTML = "<strong>Received message:</strong> " + event.data;
eventList.appendChild(li);
};
eventSource.onopen = () => {
statusElement.textContent = "Connection established ✓";
statusElement.style.color = "#27ae60";
disconnectBtn.disabled = false;
console.log('Connection opened');
};
eventSource.onerror = () => {
statusElement.textContent = "Connection error ✗";
statusElement.style.color = "#e74c3c";
disconnectBtn.disabled = true;
console.error('Error occurred');
};
}
function closeSse() {
eventSource.close();
statusElement.textContent = "Connection closed";
statusElement.style.color = "#e74c3c";
disconnectBtn.disabled = true;
console.log('Connection closed');
}
</script>
</body>2.3 StreamingResponseBody
When the goal is to bypass message conversion and write raw bytes directly to the response output stream—common in file‑download scenarios— StreamingResponseBody can be used.
@GetMapping("/streambody")
public ResponseEntity<StreamingResponseBody> streamUsers() {
List<User> datas = List.of(
new User("张三", 22),
new User("李四", 23),
new User("王五", 24),
new User("Pack_xg", 33),
new User("刘飞", 44));
StreamingResponseBody responseBody = os -> {
datas.forEach(user -> {
try {
String json = this.objectMapper.writeValueAsString(user) + "
";
os.write(json.getBytes());
os.flush();
TimeUnit.MILLISECONDS.sleep(500);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
};
return ResponseEntity.ok()
.header("Content-Type", "text/plain;charset=utf-8")
.body(responseBody);
}
public static record User(String name, Integer age) {}Invoking the endpoint with curl prints each JSON line sequentially, as shown in the screenshot. The same stream can also be consumed via an EventSource on the client side.
The article demonstrates that Spring Boot offers flexible mechanisms— ResponseBodyEmitter, SseEmitter, and StreamingResponseBody —to implement asynchronous streaming responses suitable for real‑time messaging, server‑sent events, and raw byte streaming.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.
