Master Real-Time Data Streaming in Spring Boot 3: StreamingResponseBody vs SSE
This article compares Spring Boot 3's StreamingResponseBody and SseEmitter approaches for server‑side streaming, providing complete backend and frontend implementations, code samples, and performance considerations to help developers choose the right solution for real‑time data delivery.
When building real‑time web applications, the traditional request‑response model falls short for continuous data push. Spring Boot offers three server‑side streaming options: StreamingResponseBody for low‑level stream control, SseEmitter following the Server‑Sent Events (SSE) standard, and WebSocket for full duplex communication. This article focuses on StreamingResponseBody and SseEmitter, showing their backend and frontend implementations and highlighting their differences.
Environment
Spring Boot 3.4.2
1. Introduction
StreamingResponseBody provides high flexibility by writing directly to the output stream, suitable for large file downloads or custom stream transmission. SseEmitter implements the SSE protocol, offering a lightweight, HTTP‑based, browser‑native one‑way push mechanism ideal for notifications and log streams. WebSocket delivers powerful full‑duplex communication but incurs higher overhead.
2. Practical Example – StreamingResponseBody
Backend implementation using ResponseEntity<StreamingResponseBody>:
private final ObjectMapper objectMapper;
public UserStreamingController(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@GetMapping("/stream")
public ResponseEntity<StreamingResponseBody> streamUsers() {
StreamingResponseBody responseBody = os -> {
datas.forEach(user -> {
try {
String json = this.objectMapper.writeValueAsString(user) + "
";
os.write(json.getBytes());
os.flush();
// simulate delay
TimeUnit.MILLISECONDS.sleep(500);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
};
return ResponseEntity.ok()
.header("Content-Type", "text/plain;charset=utf-8")
.body(responseBody);
}Frontend JavaScript to read the stream:
async function fetchStream() {
const response = await fetch('/users/stream');
if (!response.body) {
console.error('ReadableStream not supported');
return;
}
const container = document.querySelector('#stream_data');
container.replaceChildren();
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
console.log('Stream complete');
break;
}
const chunk = decoder.decode(value, {stream: true});
buffer += chunk;
const lines = buffer.split('
');
buffer = lines.pop(); // keep incomplete line
lines.forEach(line => {
if (line) {
appendData(container, line);
}
});
}
if (buffer) {
appendData(container, buffer);
}
}
function appendData(container, data) {
const li = document.createElement('li');
li.textContent = data;
container.appendChild(li);
}Simple HTML page:
<body>
<button type="button" onclick="fetchStream()">获取数据</button>
<button type="button" onclick="fetchSse()">SSE获取数据</button>
<hr/>
<ul id="stream_data"></ul>
</body>Resulting effect:
3. Practical Example – SseEmitter (SSE)
Backend implementation using SseEmitter:
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter sseUsers() {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
Executors.newSingleThreadExecutor().submit(() -> {
try {
for (User user : datas) {
try {
String json = objectMapper.writeValueAsString(user);
emitter.send(SseEmitter.event()
.id(UUID.randomUUID().toString())
.name("user-data")
.data(json)
.reconnectTime(5000));
Thread.sleep(300);
} catch (JsonProcessingException e) {
emitter.send(SseEmitter.event().name("error").data("JSON error: " + e.getMessage()));
} catch (IOException e) {
emitter.completeWithError(e);
return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
emitter.completeWithError(e);
return;
}
}
emitter.send(SseEmitter.event().name("complete").data("done").id(UUID.randomUUID().toString()));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}Frontend JavaScript using EventSource:
async function fetchSse() {
const container = document.querySelector('#stream_data');
container.replaceChildren();
const eventSource = new EventSource('/users/sse');
eventSource.onopen = () => console.log('SSE connection opened');
eventSource.addEventListener('user-data', event => {
appendData(container, `${event.data}`);
});
eventSource.addEventListener('complete', () => {
appendData(container, '✅ 所有数据加载完毕');
eventSource.close();
});
eventSource.onerror = event => {
console.error('SSE error:', event);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};
}Resulting effect:
Both implementations demonstrate how Spring Boot can push real‑time data to the browser, with StreamingResponseBody offering low‑level control and SseEmitter providing a standards‑based, lightweight solution.
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.
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.
