Spring Boot 后台返回流式内容,常见有两种做法:
- Spring MVC + SseEmitter
- Spring WebFlux + Flux / ServerSentEvent
它们都可以实现类似 ChatGPT 那种“边生成边返回”的效果,但底层模型、适合场景、复杂度不太一样。
一句话结论
如果你现在是普通 Spring Boot MVC 项目,想快速实现 AI 流式输出:
优先用 SseEmitter。简单、直接、够用。
如果你的项目本身就是 WebFlux 技术栈,或者你要做高并发、全链路非阻塞:
用 WebFlux + Flux / ServerSentEvent 更合适。
1. SseEmitter 是什么?
SseEmitter是 Spring MVC 里的流式响应工具。
它适合这种项目:
spring-boot-starter-web也就是传统 Spring MVC 项目,底层一般是:
Tomcat / Jetty / Undertow用法大概是:
@GetMapping(value="/chat",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchat(Stringmessage){SseEmitteremitter=newSseEmitter(0L);streamingChatModel.chat(message,newStreamingChatResponseHandler(){@OverridepublicvoidonPartialResponse(StringpartialResponse){try{emitter.send(partialResponse);}catch(IOExceptione){emitter.completeWithError(e);}}@OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){emitter.complete();}@OverridepublicvoidonError(Throwableerror){emitter.completeWithError(error);}});returnemitter;}核心思想是:
后端拿到一段模型输出,就调用
emitter.send(...)推给前端。
2. WebFlux 是什么?
WebFlux 是 Spring 的响应式编程框架。
它适合这种项目:
spring-boot-starter-webflux底层通常是:
Netty它通过Flux表示一串连续的数据流。
比如:
@GetMapping(value="/chat",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<String>>chat(Stringmessage){returnFlux.create(sink->{streamingChatModel.chat(message,newStreamingChatResponseHandler(){@OverridepublicvoidonPartialResponse(StringpartialResponse){sink.next(ServerSentEvent.builder(partialResponse).event("answer").build());}@OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){sink.next(ServerSentEvent.builder("[DONE]").event("done").build());sink.complete();}@OverridepublicvoidonError(Throwableerror){sink.error(error);}});});}核心思想是:
Controller 直接返回一个
Flux,前端不断接收这个 Flux 里面的数据。
3. 它俩都能做 SSE
这一点要先明确:
SseEmitter和WebFlux都可以返回 SSE 流。
SSE 的本质是 HTTP 长连接,服务端不断往客户端推送文本事件。
前端都可以这样接收:
consteventSource=newEventSource("/api/ai/chat");eventSource.addEventListener("answer",event=>{console.log(event.data);});eventSource.addEventListener("done",event=>{eventSource.close();});区别主要在后端实现方式和底层线程模型。
4. 核心区别对比
| 对比项 | SseEmitter | WebFlux |
|---|---|---|
| 所属技术栈 | Spring MVC | Spring WebFlux |
| 常用依赖 | spring-boot-starter-web | spring-boot-starter-webflux |
| 编程模型 | 命令式、回调式 | 响应式、声明式 |
| 返回类型 | SseEmitter | Flux<T>/Flux<ServerSentEvent<T>> |
| 底层服务器 | Tomcat 常见 | Netty 常见 |
| 线程模型 | Servlet 异步 | 事件循环 + 非阻塞 |
| 学习成本 | 低 | 较高 |
| 适合项目 | 普通 Spring MVC 项目 | 响应式项目 |
| 高并发能力 | 可以,但资源占用相对高 | 更适合大量长连接 |
| 背压支持 | 弱 | 更好 |
| 和传统业务代码结合 | 很自然 | 需要响应式思维 |
5. 最大区别:线程模型不同
5.1 SseEmitter:传统 MVC 异步流
SseEmitter虽然是异步的,但它还是属于 Spring MVC 体系。
你可以理解为:
一个请求进来 ↓ Spring MVC 创建 SseEmitter ↓ 请求线程先释放 ↓ 后台有数据时调用 emitter.send() ↓ 不断写回浏览器它比普通接口好,因为请求线程不会一直阻塞着等 AI 完整生成。
但是它仍然是传统 Servlet 体系,整体不是完全响应式的。
5.2 WebFlux:响应式非阻塞流
WebFlux 的设计理念是:
数据来了就推 没数据就不占线程 通过事件驱动处理它更适合大量连接长期挂着的场景。
比如:
- 大量用户同时和 AI 聊天
- 每个回答持续几十秒
- 后端需要维持很多 SSE 长连接
- 你的数据库、Redis、HTTP Client 也都是响应式的
这时候 WebFlux 的资源利用率通常更好。
6. 开发体验区别
6.1 SseEmitter 更符合普通 Spring Boot 开发习惯
如果你平时写的是:
@RestController@Service@Mapper然后 Controller 返回:
StringResultVOList<User>那SseEmitter会更自然。
你只需要掌握:
emitter.send(...)emitter.complete()emitter.completeWithError(...)就可以了。
示例:
@GetMapping(value="/stream",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstream(){SseEmitteremitter=newSseEmitter(0L);newThread(()->{try{emitter.send("第一段");Thread.sleep(1000);emitter.send("第二段");Thread.sleep(1000);emitter.send("第三段");emitter.complete();}catch(Exceptione){emitter.completeWithError(e);}}).start();returnemitter;}简单直接。
6.2 WebFlux 需要适应 Flux / Mono
WebFlux 代码更像这样:
@GetMapping(value="/stream",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>stream(){returnFlux.just("第一段","第二段","第三段").delayElements(Duration.ofSeconds(1));}看起来很简洁,但真实业务里你要理解:
MonoFluxsubscribesinkbackpressureSchedulers- 非阻塞调用
- 响应式链路
如果只是为了 AI 流式输出而引入 WebFlux,学习成本会高一些。
7. LangChain4j 结合时有什么区别?
LangChain4j 的低层流式接口本身是回调式的:
newStreamingChatResponseHandler(){@OverridepublicvoidonPartialResponse(StringpartialResponse){}@OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){}@OverridepublicvoidonError(Throwableerror){}}所以它天然就很适合和SseEmitter搭配:
onPartialResponse(...)↓ emitter.send(...)非常直观。
如果你用 WebFlux,需要把回调式 API 转换成Flux:
onPartialResponse(...)↓ sink.next(...)例如:
@GetMapping(value="/chat",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<String>>chat(@RequestParamStringmessage){returnFlux.create(sink->{streamingChatModel.chat(message,newStreamingChatResponseHandler(){@OverridepublicvoidonPartialThinking(PartialThinkingpartialThinking){sink.next(ServerSentEvent.builder(partialThinking.text()).event("thinking").build());}@OverridepublicvoidonPartialResponse(StringpartialResponse){sink.next(ServerSentEvent.builder(partialResponse).event("answer").build());}@OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){sink.next(ServerSentEvent.builder("[DONE]").event("done").build());sink.complete();}@OverridepublicvoidonError(Throwableerror){sink.error(error);}});});}也不难,但比SseEmitter多了一层响应式封装。
8. 带思考内容时,两种方式怎么写?
8.1 SseEmitter 写法
@GetMapping(value="/chat-sse",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchatSse(@RequestParamStringmessage){SseEmitteremitter=newSseEmitter(0L);streamingChatModel.chat(message,newStreamingChatResponseHandler(){@OverridepublicvoidonPartialThinking(PartialThinkingpartialThinking){send(emitter,"thinking",partialThinking.text());}@OverridepublicvoidonPartialResponse(StringpartialResponse){send(emitter,"answer",partialResponse);}@OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){send(emitter,"done","[DONE]");emitter.complete();}@OverridepublicvoidonError(Throwableerror){send(emitter,"error",error.getMessage());emitter.completeWithError(error);}});returnemitter;}privatevoidsend(SseEmitteremitter,StringeventName,Stringdata){try{emitter.send(SseEmitter.event().name(eventName).data(data));}catch(IOExceptione){emitter.completeWithError(e);}}8.2 WebFlux 写法
@GetMapping(value="/chat-flux",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<String>>chatFlux(@RequestParamStringmessage){returnFlux.create(sink->{streamingChatModel.chat(message,newStreamingChatResponseHandler(){@OverridepublicvoidonPartialThinking(PartialThinkingpartialThinking){sink.next(ServerSentEvent.builder(partialThinking.text()).event("thinking").build());}@OverridepublicvoidonPartialResponse(StringpartialResponse){sink.next(ServerSentEvent.builder(partialResponse).event("answer").build());}@OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){sink.next(ServerSentEvent.builder("[DONE]").event("done").build());sink.complete();}@OverridepublicvoidonError(Throwableerror){sink.error(error);}});sink.onCancel(()->{System.out.println("前端断开连接");});});}9. 用哪个好?
推荐一:普通 Spring Boot 项目,用 SseEmitter
如果你的项目是这种:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>那建议用:
SseEmitter原因:
- 简单
- 和 MVC 项目兼容好
- 不用引入 WebFlux 思维
- 和 LangChain4j 回调模型搭配自然
- 对大多数 AI 聊天系统已经够用
适合:
- 公司内部 AI 助手
- 知识库问答
- 普通客服机器人
- 后台管理系统里的 AI 功能
- 用户量不是特别夸张的应用
推荐二:项目本来就是 WebFlux,用 Flux
如果你的项目本身是:
spring-boot-starter-webflux那就直接用:
Flux<ServerSentEvent<String>>不要为了用SseEmitter再切回 MVC。
适合:
- 全链路响应式项目
- 高并发长连接
- 大量用户同时流式聊天
- 网关层、BFF 层
- 响应式数据库、响应式 Redis、响应式 HTTP Client 都已经在用
推荐三:不要为了“看起来高级”强行上 WebFlux
很多人会觉得:
WebFlux = 高性能这个理解不完全对。
WebFlux 的优势要发挥出来,需要你的整个调用链路都尽量非阻塞。
如果你 WebFlux 里面还是大量调用阻塞代码,比如:
- MyBatis 阻塞查询
- 普通 RedisTemplate
- 阻塞 HTTP Client
- 本地文件阻塞读写
- 同步调用第三方接口
那 WebFlux 的优势会被削弱,甚至代码还更复杂。
10. 性能角度怎么选?
简单理解:
SseEmitter
中小并发,开发简单,够用比如:
- 几十个并发流
- 几百个并发流
- 公司内部使用
- 业务系统内嵌 AI 功能
SseEmitter一般没问题。
WebFlux
大量长连接,更适合比如:
- 上千甚至更多并发 SSE
- 每个连接持续几十秒甚至几分钟
- 资源利用率要求高
- 你愿意使用响应式技术栈
可以考虑 WebFlux。
11. 一个重要提醒:不要随便混用 MVC 和 WebFlux
Spring Boot 里如果同时引入:
spring-boot-starter-web spring-boot-starter-webflux默认情况下,Spring Boot 通常会优先以 Spring MVC 模式启动。
这会导致有些人以为自己用了 WebFlux,但其实应用还是 MVC 模式。
如果你想真正使用 WebFlux,一般只引入:
spring-boot-starter-webflux如果你是普通 MVC 项目,一般只引入:
spring-boot-starter-web不要两个都乱加。
12. 最实际的建议
你现在的情况是:
Spring Boot 后台通过接口返回 AI 流式内容,前端接收并显示,还包括思考内容。
我的建议是:
第一阶段:先用 SseEmitter
先把功能跑通:
LangChain4j StreamingChatModel + StreamingChatResponseHandler + SseEmitter + 前端 EventSource这样最容易理解整个流程。
第二阶段:如果遇到性能瓶颈,再考虑 WebFlux
如果后面发现:
- 并发连接很多
- Tomcat 线程压力大
- SSE 长连接很多
- 需要更强的非阻塞能力
再升级为:
WebFlux + Flux<ServerSentEvent<String>>13. 最终选择表
| 你的情况 | 推荐 |
|---|---|
| 普通 Spring Boot MVC 项目 | SseEmitter |
已经用了spring-boot-starter-web | SseEmitter |
| 想快速实现 ChatGPT 式流式输出 | SseEmitter |
| 刚学习 LangChain4j | SseEmitter |
| 项目本来就是 WebFlux | Flux<ServerSentEvent<?>> |
| 大量长连接高并发 | WebFlux |
| 团队熟悉响应式编程 | WebFlux |
| 项目里大量阻塞调用 | 不建议强行 WebFlux |
14. 简单总结
可以这么记:
SseEmitter: 传统 Spring MVC 里的 SSE 工具,简单直接,适合大多数业务系统。 WebFlux: 响应式流式方案,更适合高并发、全链路非阻塞,但学习和维护成本更高。对于你当前学习 LangChain4j 和 Spring Boot AI 流式输出:
先用 SseEmitter 更合适。等你把流式响应、思考内容、正式回答、前端 EventSource 都跑通之后,再学习 WebFlux。