响应式编程 之 SseEmitter
在 Spring MVC(基于 Servlet 的传统 Web 框架) 中,若需实现异步、流式、非阻塞(相对而言)的 HTTP 响应,可以使用 Spring 提供的两个核心类:
SseEmitter:专用于 Server-Sent Events (SSE) 场景ResponseBodyEmitter:通用的异步响应流,支持任意格式(JSON、文本、自定义等)
虽然它们都基于 Servlet 3.0+ 的 异步处理机制(AsyncContext),但在用途、协议、数据格式和客户端兼容性上有显著区别。
下面从 原理 → 区别 → 使用示例 → 注意事项 四个维度详细解析。
一、底层原理:Servlet 异步处理
1. 传统同步模型的问题
@GetMapping("/slow")
public String slow() {
Thread.sleep(5000); // 阻塞 Tomcat 线程 5 秒!
return "done";
}
- 每个请求占用一个 Tomcat 线程,高并发时线程耗尽。
2. 异步模型(AsyncContext)
- 调用
request.startAsync()后,立即释放容器线程。 - 后续由业务线程写入响应(通过
AsyncContext.getResponse())。 - Spring 封装为
ResponseBodyEmitter/SseEmitter,简化使用。
✅ 关键点:
这些 emitter 不是真正的非阻塞 I/O(如 Netty),而是“释放请求线程 + 异步写响应”,仍基于 Servlet 容器(Tomcat/Jetty)。
二、SseEmitter vs ResponseBodyEmitter 对比
| 特性 | SseEmitter |
ResponseBodyEmitter |
|---|---|---|
| 用途 | 专用于 SSE(Server-Sent Events) | 通用异步响应流(任意格式) |
| Content-Type | 固定为 text/event-stream |
可自定义(如 application/json, text/plain) |
| 数据格式 | 自动封装为 SSE 格式(data: ...\n\n) |
原始对象,由 HttpMessageConverter 序列化 |
| 客户端 | 必须用 EventSource(浏览器) |
任意 HTTP 客户端(如 fetch, curl) |
| 事件支持 | 支持 id, event, retry 等字段 |
无事件概念,纯数据流 |
| 超时控制 | 构造时指定(毫秒),默认 30 秒 | 同左 |
| 完成方式 | complete(), completeWithError() |
同左 |
| 典型场景 | 实时通知、日志推送 | 文件下载、大结果集分块返回 |
三、使用示例详解
示例 1:SseEmitter —— 实现 SSE 推送
@RestController
public class SseController {
private final Set<SseEmitter> emitters =
Collections.synchronizedSet(new HashSet<>());
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter(60_000L); // 60秒超时
// 注册生命周期回调
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> {
emitters.remove(emitter);
emitter.complete();
});
emitter.onError(e -> {
emitters.remove(emitter);
// 日志记录异常
});
emitters.add(emitter);
// 异步发送数据(避免阻塞请求线程)
CompletableFuture.runAsync(() -> {
try {
for (int i = 0; i < 5; i++) {
// 发送标准 SSE 事件
emitter.send(SseEmitter.event()
.id(String.valueOf(i))
.name("update") // 事件类型
.data("Message-" + i)
.reconnectTime(3000) // 建议重连时间(毫秒)
);
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
// 广播消息给所有连接
@PostMapping("/broadcast")
public void broadcast(@RequestBody String msg) {
emitters.forEach(emitter -> {
try {
emitter.send(msg);
} catch (IOException e) {
emitters.remove(emitter);
}
});
}
}
前端调用:
const es = new EventSource('/sse');
es.addEventListener('update', event => {
console.log('收到 update 事件:', event.data);
});
es.onerror = () => es.close();
示例 2:ResponseBodyEmitter —— 通用流式响应
@RestController
public class StreamController {
@GetMapping(value = "/stream-json", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseBodyEmitter streamJson() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
CompletableFuture.runAsync(() -> {
try {
// 发送多个 JSON 对象(注意:这不是合法 JSON 数组!)
emitter.send(Map.of("step", 1, "status", "start"));
Thread.sleep(1000);
emitter.send(Map.of("step", 2, "status", "processing"));
Thread.sleep(1000);
emitter.send(Map.of("step", 3, "status", "done"));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
// 流式文件下载(逐块发送)
@GetMapping("/download")
public ResponseBodyEmitter downloadFile() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
CompletableFuture.runAsync(() -> {
try (InputStream is = new FileInputStream("large-file.zip")) {
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
emitter.send(buffer, 0, len); // 发送原始字节
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
客户端调用(JavaScript):
// 注意:这不是标准 JSON,需逐行解析(类似 NDJSON)
const response = await fetch('/stream-json');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
console.log('Received chunk:', chunk); // 可能是 {"step":1,...}{"step":2,...}
}
⚠️ 重要:
ResponseBodyEmitter发送多个对象时,不会自动加逗号或换行!需自行设计协议(如每条 JSON 后加\n)。
四、关键注意事项
1. 必须异步处理
- 在 Controller 方法中直接
emitter.send()会阻塞请求线程。 - 务必使用
CompletableFuture,@Async, 或线程池提交任务。
2. 超时与资源泄漏
- 默认超时 30 秒(可通过构造函数设置)。
- 必须监听
onTimeout()/onCompletion()/onError()并清理资源(如从集合中移除)。
3. 线程安全
SseEmitter和ResponseBodyEmitter不是线程安全的!- 不要多个线程同时调用
send()。
4. 错误处理
- 客户端断开连接时,
send()会抛IOException。 - 必须捕获并调用
completeWithError(),否则连接会卡住。
5. 与 WebFlux 的区别
- 这些 emitter 仅适用于 Spring MVC(Servlet)。
- 在 WebFlux 中应使用
Flux<ServerSentEvent>或Flux<T>直接返回。
五、何时使用哪个?
| 场景 | 推荐方案 |
|---|---|
| 浏览器实时接收服务器推送(通知、日志) | ✅ SseEmitter |
| 向非浏览器客户端流式返回数据(如服务间调用) | ✅ ResponseBodyEmitter |
| 返回分块 JSON(NDJSON) | ✅ ResponseBodyEmitter + 手动加 \n |
| 文件下载、视频流 | ✅ ResponseBodyEmitter(发送 byte[]) |
| 需要双向通信 | ❌ 改用 WebSocket |
六、总结
| 类型 | 协议 | 数据格式 | 客户端 | 适用场景 |
|---|---|---|---|---|
SseEmitter |
HTTP + SSE | data: ...\n\n |
浏览器 EventSource |
实时单向推送 |
ResponseBodyEmitter |
普通 HTTP | 任意(JSON/文本/二进制) | 任意 HTTP 客户端 | 通用异步流 |
💡 最佳实践:
- 优先考虑是否真的需要流式响应。
- 若面向浏览器且只需服务器推,选
SseEmitter。- 若需灵活控制响应格式或对接非浏览器客户端,选
ResponseBodyEmitter。- 高并发场景下,评估是否应迁移到 Spring WebFlux 以获得真正的非阻塞能力。
本文来自博客园,作者:蓝迷梦,转载请注明原文链接:https://www.cnblogs.com/hewei-blogs/articles/19570412

浙公网安备 33010602011771号