SseEmitter 是 Spring MVC 提供的一个类,用于实现 服务器向客户端的实时推送(Server-Sent Events,简称 SSE)。
一、核心概念
SSE(Server-Sent Events)是一种基于 HTTP 的单向通信机制:
- 服务器 → 浏览器(单向)
- 浏览器通过
EventSource建立长连接 - 服务器可以持续向客户端推送数据(流式返回)
二、SseEmitter 是干什么的?
简单说:
SseEmitter用于在 Spring 后端持续向前端发送"流式数据"。
典型使用场景:
- AI 对话流式输出(类似 ChatGPT)
- 实时日志推送
- 进度更新
- 消息通知
三、代码里的作用解析
你截图中的代码:
java
public SseEmitter chatStream(...)
说明:
- 这个接口返回的不是普通 JSON
- 而是一个 流式响应(text/event-stream)
- 客户端会一直保持连接,不断接收数据
四、基本用法
1️⃣ 创建 emitter
java
SseEmitter emitter = new SseEmitter(30000L); // 超时时间
2️⃣ 发送数据
java
emitter.send("hello");
emitter.send(SseEmitter.event().data("流式数据"));
3️⃣ 结束流
java
emitter.complete();
4️⃣ 异常处理
java
emitter.completeWithError(e);
五、完整示例(典型流式接口)
java
@GetMapping("/stream")
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
emitter.send("data: " + i);
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
六、前端如何接收?
javascript
const source = new EventSource('/stream');
source.onmessage = function(event) {
console.log(event.data);
};
七、和 WebSocket 的区别
| 特性 | SSE (SseEmitter) |
WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | WS |
| 实现难度 | 简单 | 较复杂 |
| 适合场景 | 流式输出、通知 | 聊天、游戏 |
八、你这段代码的实际用途
你这个方法名:
java
chatStream
结合 SseEmitter,非常典型:
👉 用于实现 AI 对话的流式返回(像 ChatGPT 一样一边生成一边输出)
九、常见坑(实战经验)
-
必须异步线程发送数据
- 否则会阻塞请求
-
要处理超时
javaemitter.onTimeout(() -> emitter.complete()); -
Nginx 需要关闭缓冲
nginxproxy_buffering off; -
连接数要控制
- 每个用户一个长连接
总结一句话
SseEmitter= Spring 里实现"流式返回 / 实时推送"的核心工具(基于 SSE 协议)
例子
我给你一个**贴近你截图场景(AI 流式输出)**的完整例子,从后端到前端一步讲清楚。
🎯 场景
做一个类似 ChatGPT 的接口:
👉 用户输入:"你好"
👉 页面逐字输出:"你好呀,很高兴见到你..."
🧩 一、后端(Spring Boot + SseEmitter)
1️⃣ Controller
java
@RestController
@RequestMapping("/api/llm")
public class ChatController {
@GetMapping("/chat/stream")
public SseEmitter streamChat(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(0L); // 不超时
new Thread(() -> {
try {
// 模拟 AI 一点点返回
String[] tokens = {"你好", "呀,", "很高兴", "见到你", "!"};
for (String token : tokens) {
Thread.sleep(500); // 模拟生成延迟
emitter.send(token); // 👈 核心:推送数据
}
emitter.send("[DONE]");
emitter.complete(); // 👈 结束
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
}
🧠 这一段在干嘛?
👉 本质就是:
text
服务器每 0.5 秒说一句话
等价于:
text
你好
呀,
很高兴
见到你
!
[DONE]
🌐 二、前端(接收流)
方法1:用 fetch(和你图里一样)
javascript
async function streamChat() {
const response = await fetch('/api/llm/chat/stream?message=你好');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
console.log("收到:", chunk);
result += chunk;
// 👉 更新页面
document.getElementById("output").innerText = result;
}
}
页面效果(真实体验)
text
第1秒:你好
第2秒:你好呀,
第3秒:你好呀,很高兴
第4秒:你好呀,很高兴见到你
第5秒:你好呀,很高兴见到你!
👉 这就是"流式输出"
🔁 三、对比普通接口
❌ 普通接口
text
等待 5 秒
一次性返回:你好呀,很高兴见到你!
✅ SSE(SseEmitter)
text
边生成边返回:
你好 → 你好呀 → 你好呀,很高兴 → ...
👉 用户体验:像 AI 在实时"打字"
🔧 四、你截图里的真实对应关系
你图里的代码可以映射成这样:
| 你图里的东西 | 这个例子里 |
|---|---|
SseEmitter |
emitter |
emitter.send(...) |
推 token |
streamingModel.chat() |
模拟 tokens 数组 |
onPartialResponse |
for 循环 |
[DONE] |
结束标志 |
| 前端 fetch | reader.read() |
⚠️ 五、实际项目注意点(很关键)
1️⃣ 一定要处理超时
java
new SseEmitter(0L); // 或设置长时间
2️⃣ 要有结束标志
java
emitter.send("[DONE]");
3️⃣ 异常要关闭连接
java
emitter.completeWithError(e);
4️⃣ 不能阻塞主线程
👉 必须用线程 / 异步(你图里也是这样)
✅ 一句话总结
👉 这个例子本质就是:
后端每生成一点内容 → 立刻用 SseEmitter 推给前端 → 前端边收边显示
如果你下一步想更贴近你项目(比如 LangChain4j + OpenAI streaming 那种),我可以给你一个完全对标你截图代码的真实企业级写法。
实战
这段代码的作用可以概括成一句话:
它实现了一个"AI 聊天流式接口"
前端调用 /chat/stream 后,后端会把大模型生成的内容一边生成、一边推送给前端,而不是等全部生成完再一次性返回。
我按"整体功能 → 执行流程 → 每段代码作用"给你拆开讲。
一、这个接口是干什么的
java
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, Object> request)
这行的含义是:
- 这是一个 POST 接口
- 路径是
/chat/stream - 返回类型是
text/event-stream - 返回值是
SseEmitter
通俗讲:
👉 前端发来一条聊天请求
👉 后端调用大模型
👉 大模型每吐出一点内容,就立刻发给前端
👉 前端就能看到"打字机效果"
这就是典型的 SSE 流式聊天接口。
二、这个接口最终实现了哪些功能
它实际做了 6 件事:
- 拿到当前登录用户
- 读取前端传来的提问内容
- 检查用户有没有配置大模型 API Key
- 把用户消息先保存到数据库
- 调用大模型进行流式输出
- 把模型回复实时推给前端,最后再保存完整回复
三、整体执行流程
你可以把它理解成下面这条链路:
前端发起聊天请求
→ 后端取用户信息和参数
→ 检查 API Key
→ 保存用户消息
→ 调用大模型流式生成
→ 每生成一个 token 就发给前端
→ 全部生成完后保存 assistant 消息
→ 给前端发 [DONE]
→ 结束连接
四、逐段解释代码作用
1)定义接口:返回 SSE 流
java
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, Object> request)
作用
这是一个 聊天流式接口。
关键点
produces = MediaType.TEXT_EVENT_STREAM_VALUE
表示这个接口返回的不是普通 JSON,而是:
text/event-stream
也就是 SSE 格式。
通俗理解
不是一次性返回完整答案,
而是"后端不断往前端推送消息"。
2)获取用户和请求参数
java
long userId = StpUtil.getLoginIdAsLong();
String prompt = (String) request.get("prompt");
String context = (String) request.get("context");
Long conversationId = request.get("conversationId") != null ? Long.valueOf(request.get("conversationId").toString()) : null;
作用
从当前请求里拿出聊天所需的数据。
分别是什么意思
userId
当前登录用户 ID。
java
long userId = StpUtil.getLoginIdAsLong();
说明这个接口是登录后才能调用的,系统要知道"是谁在提问"。
prompt
用户当前输入的问题。
比如:
"帮我写一份周报"
context
上下文内容。
比如可能是前面几轮聊天记录,或者额外背景说明。
conversationId
会话 ID。
用于标识这条消息属于哪个聊天会话。
如果没有,就可能是新会话。
3)创建 SSE 通道和完整回复缓存
java
SseEmitter emitter = new SseEmitter(300000L);
StringBuilder fullResponse = new StringBuilder();
SseEmitter emitter
创建一个 SSE 推送对象。
java
new SseEmitter(300000L)
表示:
- 创建一条 SSE 长连接
- 超时时间 300000 毫秒
- 也就是 5 分钟
通俗理解
像开了一根"消息管道",后面模型生成的内容都会从这根管道发给前端。
StringBuilder fullResponse
用于把模型返回的所有 token 拼起来。
因为模型是一个字一个字、一段一段返回的,最后要把完整答案拼出来,保存到数据库。
4)外层 try:整个流程的总保护
java
try {
...
} catch (Exception e) {
...
}
作用
防止接口在初始化阶段直接崩掉。
比如:
- 取 API Key 失败
- 创建模型失败
- 参数异常
- 数据库保存失败
如果这里出错,会给前端发一个错误消息并结束连接。
5)检查用户有没有配置大模型 Key
java
var llmKey = llmKeyService.getDefaultLLMKey(userId);
if (llmKey == null) {
emitter.send(SseEmitter.event().data("{\"event\":\"error\",\"data\":\"请先配置大模型 API Key\"}"));
emitter.complete();
return emitter;
}
作用
先检查当前用户有没有配置默认的大模型 API Key。
为什么要做这个
因为后面调用大模型必须依赖:
- API Key
- Base URL
- 模型名
如果这些都没有,根本没法请求模型。
处理方式
如果没配置:
- 通过 SSE 给前端发错误信息
- 结束连接
- 直接返回
通俗理解
像系统先检查"你有没有油卡",没有就别开车了。
6)拼接最终 prompt
java
String fullPrompt = context != null && !context.isEmpty() ? context + "\n" + prompt : prompt;
作用
把上下文和当前问题拼成最终发给模型的输入。
逻辑
-
如果有
context,就:context + 换行 + prompt
-
如果没有,就只用
prompt
举个例子
假设:
context = "你是一个Java老师"
prompt = "解释一下SSE"
拼出来就是:
你是一个Java老师
解释一下SSE
通俗理解
这是在告诉模型:
"这是背景,这是本次问题,请一起参考回答。"
7)先保存用户消息
java
chatMessageService.saveMessage(userId, conversationId, "user", fullPrompt, llmKey.getModelName());
作用
把用户这次提问先存到数据库。
存了什么
大概率包括:
- 用户 ID
- 会话 ID
- 角色:
user - 内容:
fullPrompt - 模型名
为什么先存
因为这样聊天记录能完整保存下来,后面可以在会话列表里查看。
8)获取流式大模型对象
java
var streamingModel = llmService.getStreamingChatModel(
llmKey.getApiKey(),
llmKey.getBaseUrl(),
llmKey.getModelName()
);
作用
根据用户配置的参数,创建一个"支持流式输出"的模型客户端。
这里用到了
- API Key
- Base URL
- 模型名
通俗理解
这一步是在"接通大模型服务"。
9)保存模型名
java
String modelName = llmKey.getModelName();
作用
把模型名单独保存下来,方便在回调内部使用。
因为匿名内部类里引用外部变量时,通常希望变量明确、稳定。
10)发起流式聊天
java
streamingModel.chat(fullPrompt, new StreamingChatResponseHandler() {
作用
调用大模型开始生成回答,并注册回调处理器。
通俗理解
你可以理解成:
把问题交给模型
然后告诉它:
- 你每吐一点内容,就通知我
- 全部吐完了,也通知我
- 出错了,也通知我
这个 StreamingChatResponseHandler 就是专门处理这些过程事件的。
五、三个核心回调方法
这是整段代码最关键的地方。
1)onPartialResponse:收到部分内容时
java
@Override
public void onPartialResponse(String token) {
try {
fullResponse.append(token);
emitter.send(SseEmitter.event().data(token));
} catch (IOException e) {
log.error("Failed to send token: {}", e.getMessage());
emitter.completeWithError(e);
}
}
作用
每当模型生成一点内容,就会进这个方法。
这里做了两件事
第一件:拼接到完整答案里
java
fullResponse.append(token);
比如模型依次返回:
你
好
,很
高兴
见到你
这里会慢慢拼成:
你好,很高兴见到你
第二件:实时推给前端
java
emitter.send(SseEmitter.event().data(token));
把当前 token 立刻通过 SSE 发给前端。
通俗理解
模型每"说一个字",后端就立刻转发给页面。
所以前端看到的是"边生成边显示"。
异常处理
java
catch (IOException e) {
...
emitter.completeWithError(e);
}
如果发送失败,比如:
- 前端断开了
- 网络中断
- SSE 通道出问题
那就记录日志,并把连接按异常结束。
2)onCompleteResponse:模型全部生成完成时
java
@Override
public void onCompleteResponse(ChatResponse response) {
try {
chatMessageService.saveMessage(userId, conversationId, "assistant", fullResponse.toString(), modelName);
emitter.send(SseEmitter.event().data("[DONE]"));
emitter.complete();
} catch (IOException e) {
log.error("Failed to send complete event: {}", e.getMessage());
emitter.completeWithError(e);
}
}
作用
当模型已经完整回答完时执行。
这里做了三件事
第一件:保存 assistant 的完整回答
java
chatMessageService.saveMessage(userId, conversationId, "assistant", fullResponse.toString(), modelName);
把刚才拼出来的完整回复保存到数据库。
保存的是:
- 用户 ID
- 会话 ID
- 角色:
assistant - 完整内容
- 模型名
这就形成一条完整聊天记录。
第二件:给前端发送结束标志
java
emitter.send(SseEmitter.event().data("[DONE]"));
告诉前端:
模型已经输出完了
前端一般会根据 [DONE]:
- 停止 loading
- 停止继续拼接流
- 把消息状态改为完成
第三件:关闭 SSE 连接
java
emitter.complete();
正常结束。
通俗理解
这相当于:
模型说完了
→ 把完整答案存档
→ 通知前端"结束了"
→ 挂断连接
3)onError:模型处理出错时
java
@Override
public void onError(Throwable error) {
try {
emitter.send(SseEmitter.event().data("[ERROR] " + error.getMessage()));
emitter.complete();
} catch (IOException e) {
log.error("Failed to send error event: {}", e.getMessage());
emitter.completeWithError(e);
}
}
作用
如果模型生成过程中出错,会进入这里。
比如:
- 模型接口调用失败
- API Key 无效
- 网络异常
- 上游服务超时
处理方式
- 给前端发错误信息
- 结束连接
通俗理解
不是正常说完,而是中途出故障了,于是告诉前端"报错了"。
六、外层 catch:启动阶段异常处理
java
} catch (Exception e) {
log.error("Failed to start streaming chat: {}", e.getMessage(), e);
try {
emitter.send(SseEmitter.event().data("[ERROR] " + e.getMessage()));
} catch (IOException ioException) {
log.error("Failed to send error event: {}", ioException.getMessage());
}
emitter.complete();
}
作用
处理"还没进入流式回调之前"的异常。
比如:
- 获取默认 Key 失败
- 拼 prompt 出错
- 保存用户消息失败
- 创建 streamingModel 失败
逻辑
- 记录错误日志
- 尝试通知前端
- 结束 SSE 连接
七、超时和完成回调
1)超时处理
java
emitter.onTimeout(() -> {
log.warn("SSE emitter timeout");
emitter.complete();
});
作用
如果 SSE 超时了,就记日志并结束连接。
因为你前面设置的是:
java
new SseEmitter(300000L)
所以 5 分钟没完成,就会触发超时处理。
通俗理解
为了防止连接一直挂着不结束。
2)完成回调
java
emitter.onCompletion(() -> {
log.debug("SSE emitter completed");
});
作用
当 SSE 连接最终结束时,打一个调试日志。
无论是:
- 正常结束
- 异常结束
- 超时结束
最后都可能触发 completion。
通俗理解
相当于留个"收尾记录"。
八、最后返回 emitter
java
return emitter;
作用
把这个 SSE 通道返回给前端。
返回后,前端和后端之间就建立了流式连接,后面后端就可以持续 send(...) 数据过去。
九、这段代码的核心价值
这段代码最核心的功能是:
1. 支持 AI 流式输出
不是等模型全答完,而是逐步返回。
2. 支持会话记录保存
用户消息和 AI 回复都会入库。
3. 支持多会话
通过 conversationId 区分聊天上下文。
4. 支持异常处理
没配置 Key、模型报错、发送失败,都有处理。
5. 支持超时控制
避免 SSE 长时间挂死。
十、把它翻译成大白话
如果把整段代码翻成生活语言,就是:
用户发来一个问题
→ 系统先确认这个用户是谁
→ 看他有没有配置大模型账号
→ 把用户的问题记下来
→ 去问大模型
→ 模型每说一句,就立刻转发给前端
→ 前端就能实时显示
→ 模型说完后,把完整回答存起来
→ 告诉前端"结束了"
→ 关闭连接
十一、这段代码里最关键的几个点
你要抓住这几个核心点:
SseEmitter
负责"流式推送"
streamingModel.chat(...)
负责"调用支持流式输出的大模型"
onPartialResponse
负责"每来一点内容就发给前端"
fullResponse.append(token)
负责"把零散 token 拼成完整答案"
saveMessage(...)
负责"存聊天记录"
[DONE]
负责"告诉前端本次流结束"
十二、这段代码的一个小风险点
这段代码能跑,但从工程角度有一个值得注意的点:
java
streamingModel.chat(fullPrompt, new StreamingChatResponseHandler() {
如果这个 chat(...) 底层不是异步的,而是阻塞当前请求线程,那么高并发下可能占住 Web 线程较久。
也就是说,要看 streamingModel.chat 的底层实现是不是异步回调模型。
如果它本身是异步的,这样写没问题。
如果它不是,生产环境一般会再做线程池隔离。
十三、一句话总结
这段代码实现了一个"带会话保存、异常处理、超时控制的 AI 流式聊天接口",核心机制是用 SseEmitter 把大模型生成的内容实时推送给前端。
如果你愿意,我下一条可以继续帮你做两件事里的一个:
要么给你画这段代码的执行流程图,要么逐行解释每一行代码。