Spring使用SseEmitter实现后端流式传输和前端Vue数据接收
4.1 前言
在构建web应用时,实时数据推送是一个非常重要的需求,比如AI对话流式想用、实时通知推送等。在SpringBoot项目中,SseEmitter是一个非常方便的工具,它可以让后端向前端进行流式数据传输(Server-> Sent Events,简称SSE)。
SseEmitter是SpringWeb提供的一个用于服务器推送事件(SSE,Server-Send Events)的工具。它可以让服务器端不断向客户端推送数据,而不需要客户端不断轮询。
相比于WebSocket,SSE具有以下优点:
基于HTTP连接,无需额外的协议支持,前端可以直接使用EventSource进行接收。
支持自动重连,如果连接断开,浏览器会自动尝试重新连接。
更适合单向推送数据的场景,如AI生成流式文本、服务器消息推送等。
4.2 在SpringBoot项目中使用SseEmitter
(1)添加依赖
如果你的项目使用的是 Spring MVC(一般的SpringBoot项目),无需额外的依赖。但如果你使用的是 Spring WebFlux,请确保你的 pom.xml
中包含以下依赖(仅 WebFlux 需要):
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
(2)创建后端流式接口
Controller层
@RestController
@RequestMapping("/multiChat")
public class MultiChatController {
@AutowiredMultiChatService multiChatService;
@GetMapping(value = "/streamMore", produces = "text/event-stream;charset=UTF-8")public SseEmitter streamChatMore(@RequestParam(value = "userQuestion") String userQuestion,@RequestParam(value = "titleId") String titleId,@RequestParam(value = "createBy", required = false) String createBy) {ChatRecord chatRecord = new ChatRecord();chatRecord.setUserQuestion(userQuestion);chatRecord.setTitleId(Long.parseLong(titleId));chatRecord.setCreateBy(createBy);
chatRecord.setRecordId(UIDCom.randomUUID());chatRecord.setQuestionTime(DateUtils.getNowDate());SseEmitter emitter = new SseEmitter(0L);// 综合框架multiChatService.chatStreamMoreMessage(chatRecord, emitter);return emitter;}
}
其中SseEmitter emitter = new SseEmitter(0L); // 0L 表示永不超时
MultiChatService层
public interface MultiChatService {/*** 多路聊天* deepseek聊天 + 系统菜单功能 + 报告检索* @param chatRecord* @param emitter*/void chatStreamMoreMessage(ChatRecord chatRecord, SseEmitter emitter);
Result testChat(ChatRecord chatRecord);
}
MultiChatServiceImpl层
@Service
@Slf4j
public class MultiChatServiceImpl implements MultiChatService {@Overridepublic void chatStreamMoreMessage(ChatRecord chatRecord, SseEmitter emitter) {new Thread(() -> {try {Thread.sleep(1000);// 要输出的完整文本
// String content = "9 × 9 的计算结果是 81。\n这是乘法表中的基本运算之一,也被称为“九九八十一”。 😊";// 按 codePoint 遍历,保证 emoji 不会被拆开int[] codePoints = chatRecord.getUserQuestion().codePoints().toArray();for (int cp : codePoints) {String chunk = new String(Character.toChars(cp)); // 把 codePoint 转回字符串emitter.send(SseEmitter.event().name("message").data(chunk));Thread.sleep(100);}Thread.sleep(3000); // 停留3s钟结束emitter.send(SseEmitter.event().name("message").data("[DONE]")); // 发送结束标记emitter.complete();// 回调if (onComplete != null) {onComplete.accept(content);}} catch (Exception e) {emitter.completeWithError(e);}}).start();}
}
(3)前端接收流式数据
使用fetch处理SSE(流式数据),fetch默认不会处理流式数据,因此我们需要手动解析ReadableStream以逐步接收数据。
const aiMsg = reactive({ sender: "AI助手", text: "", loading: true }); // ✅ 增加 loading 标志this.messages.push(aiMsg);const eventSource = new EventSource(`/searcher/multiChat/streamMore?userQuestion=${encodeURIComponent(text)}&titleId=${chatId}&createBy=99999`);eventSource.onmessage = (event) => {if(event.data === "[DONE]") {eventSource.close();return;}if (aiMsg.loading) {aiMsg.loading = false; // ✅ 第一次收到数据就关闭“正在输入”}aiMsg.text += event.data; // 逐步拼接eventSource.onerror = (err) => {// console.error("SSE 连接错误:", err);this.showTip("SSE 连接错误:"+err, 1);aiMsg.loading = false; // ✅ SSE 结束也要关掉eventSource.close();};
注意:/searcher 会先经过vue.config.js配置的服务器id然后转发到设置的后端服务器。