好的,这个任务很简单,我直接为你整理一篇脱敏后的 CSDN 博客文章。已移除所有公司、项目、包名、业务相关信息,替换为通用示例代码。


Spring Boot 中的 SseEmitter 详解:轻松实现服务端流式推送(附 AI 打字机效果实战)

关键词:SseEmitter、Server-Sent Events、SSE、Spring Boot、流式响应、AI 流式输出

一、前言

在使用 ChatGPT 等 AI 产品时,你一定见过"打字机效果"——回答一个字一个字地蹦出来,而不是等全部生成完才显示。这背后最常用的技术之一就是 SSE(Server-Sent Events),而在 Spring 生态中,实现它的核心类就是 SseEmitter

本文带你彻底搞懂 SseEmitter 的原理、用法和踩坑点。

二、SseEmitter 是什么?

SseEmitter 是 Spring MVC 提供的一个类,全限定名为:

org.springframework.web.servlet.mvc.method.annotation.SseEmitter

它用于实现 SSE(Server-Sent Events,服务器推送事件)——一种基于普通 HTTP 协议、让服务端通过一个长连接持续、单向地向客户端推送数据的技术。

与普通 HTTP 请求的区别

普通的 HTTP 请求是"一问一答":

客户端 ──请求──► 服务端
客户端 ◄──响应── 服务端(连接关闭)

而 SSE 是"一问多答":

客户端 ──发起一次请求──► 服务端
客户端 ◄─ 数据片段1 ◄─ 数据片段2 ◄─ 数据片段3 ... ◄─ 完成/关闭

Controller 方法返回 SseEmitter 后,HTTP 连接不会立即关闭,服务端可以在任意时刻(通常是异步线程中)不断向客户端推送消息。

三、核心 API 一览

方法 作用
new SseEmitter() 创建实例,使用默认超时时间
new SseEmitter(timeoutMs) 创建实例并指定超时时间(毫秒),0L 表示永不超时
emitter.send(data) 推送一条消息
emitter.send(SseEmitter.event().name("xxx").data(...)) 推送带事件名的消息
emitter.complete() 正常结束连接
emitter.completeWithError(e) 以异常方式结束连接
emitter.onTimeout(Runnable) 超时回调
emitter.onCompletion(Runnable) 完成回调
emitter.onError(Consumer) 出错回调

四、快速上手:一个最小示例

1. 后端 Controller

@RestController
@RequestMapping("/sse")
public class SseDemoController {

    // 建议使用线程池,避免每次都 new Thread
    private final ExecutorService executor = Executors.newCachedThreadPool();

    @GetMapping("/stream")
    public SseEmitter stream() {
        // 设置 60 秒超时
        SseEmitter emitter = new SseEmitter(60_000L);

        executor.execute(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    // 推送带事件名的消息
                    emitter.send(SseEmitter.event()
                            .name("message")
                            .data("这是第 " + i + " 条推送"));
                    Thread.sleep(1000);
                }
                emitter.complete(); // 正常结束
            } catch (Exception e) {
                emitter.completeWithError(e); // 异常结束
            }
        });

        // 方法立即返回,连接保持打开
        return emitter;
    }
}

关键点return emitter 之后请求线程就释放了,真正的推送发生在异步线程中,这也是 SSE 能"细水长流"的原因。

2. 前端接收(浏览器原生支持)

const source = new EventSource('/sse/stream');

// 监听指定事件名
source.addEventListener('message', (e) => {
    console.log('收到:', e.data);
});

source.onerror = () => {
    console.log('连接关闭或出错');
    source.close();
};

无需任何第三方库,浏览器原生 EventSource 即可对接。

五、实战场景:AI 流式问答(打字机效果)

这是 SseEmitter 目前最火的应用场景。典型流程如下:

@PostMapping("/chat")
public SseEmitter chat(@RequestBody ChatRequest req) {
    SseEmitter emitter = new SseEmitter(120_000L); // AI 生成较慢,超时设长一些

    executor.execute(() -> {
        try {
            // 1. 参数校验失败时,推送 error 事件并结束
            if (StringUtils.isBlank(req.getQuestion())) {
                emitter.send(SseEmitter.event()
                        .name("error")
                        .data("问题不能为空,请重试!"));
                emitter.complete();
                return;
            }

            // 2. 调用大模型的流式接口,每收到一个 token 就转发给前端
            llmClient.streamChat(req.getQuestion(), token -> {
                try {
                    emitter.send(SseEmitter.event()
                            .name("data")
                            .data(token));
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            });

            // 3. 生成结束,推送完成事件
            emitter.send(SseEmitter.event().name("done").data("[DONE]"));
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });

    return emitter;
}

设计要点:

  1. 用事件名区分消息类型:如 data(正文)、error(错误提示)、done(结束标记),前端按事件名分别处理。
  2. 错误也通过 SSE 推送:连接已经建立,HTTP 状态码没法再改,所以业务错误要用 error 事件告知前端,而不是抛异常。
  3. 超时时间要结合业务设置:AI 生成可能耗时较长,默认超时往往不够,建议定义常量统一管理。

六、SSE 与 WebSocket 怎么选?

对比项 SSE (SseEmitter) WebSocket
通信方向 单向(服务端 → 客户端) 双向
底层协议 普通 HTTP 独立协议(ws:// / wss://)
浏览器支持 原生 EventSource 原生 WebSocket
断线重连 EventSource 自动重连 需自行实现
实现复杂度 简单,无需额外依赖 较重,需握手升级协议
典型场景 AI 流式输出、进度条、消息通知 聊天室、实时协作、游戏

一句话总结:只需要服务端往客户端推,选 SSE,简单省事;需要双向实时通信,才上 WebSocket。

七、常见踩坑点

  1. 忘记异步:如果在 Controller 主线程里循环 send,会一直占用 Tomcat 工作线程,高并发下线程池被打满。务必将推送逻辑放到独立线程池。
  2. 超时未处理:默认超时后 Spring 会抛 AsyncRequestTimeoutException,记得通过 onTimeout 回调做清理,或按需调大超时时间。
  3. Nginx 缓冲:经过 Nginx 代理时,响应可能被缓冲导致"不流式"。需要在代理配置中关闭缓冲:
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    
  4. 连接已断开仍在 send:客户端关闭页面后继续 send 会抛 IOException,捕获后应调用 completeWithError 并终止后续推送。
  5. EventSource 只支持 GET:浏览器原生 EventSource 不支持 POST。若接口必须是 POST(如需要传大 JSON),前端可改用 fetch + ReadableStream 解析 SSE 格式。

八、总结

  • SseEmitter 是 Spring MVC 实现 服务端单向流式推送 的核心类;
  • 核心用法:Controller 返回 SseEmitter,异步线程中 send() 推送、complete() 结束;
  • 最典型的应用就是 AI 流式问答的打字机效果
  • 与 WebSocket 相比更轻量,单向推送场景首选 SSE。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连!有问题可以在评论区留言讨论~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐