Spring Boot 中基于 WebClient 的 SSE 流式接口实战
SSE(Server-Sent Events)是一种服务器主动推送的 HTTP 通信方式:基于 HTTP单向(服务端 → 客户端)长连接文本流(data: 你好data: 我是data: AI客户端可以一边接收,一边渲染。→ 行不通Feign 和 WebClient 的边界同步接口和流式接口在架构层面的本质差异AI 场景对交互模型的倒逼如果你现在也在做:AI 问答长文本生成实时推送那么,SSE 几
—— 从 Feign 到 WebClient 的一次真实踩坑记录
一、背景:为什么我要做 SSE?
在最近的一个项目中,我负责接入一个 AI 问答服务。
一开始的接口形态非常常规:
@PostMapping("/health_manager")
public RespBean<HealthManagerQueryDataVO> sendQuery(...)
客户端发请求,服务端等 AI 全部生成完内容,再一次性返回。
问题很快就暴露了:
-
AI 返回慢(10 秒甚至更久)
-
用户页面“卡死”,体验极差
-
其实 AI 是“边生成边返回”的,但我们完全浪费了这个能力
于是,目标就很明确了:
把原有同步接口,改造成支持 SSE(Server-Sent Events)的流式接口
二、什么是 SSE?为什么适合 AI 问答?
1️⃣ SSE 是什么?
SSE(Server-Sent Events)是一种 服务器主动推送 的 HTTP 通信方式:
-
基于 HTTP
-
单向(服务端 → 客户端)
-
长连接
-
文本流(
text/event-stream)
返回的数据长这样:
data: 你好
data: 我是
data: AI
客户端可以一边接收,一边渲染。
2️⃣ 为什么 SSE 特别适合 AI 场景?
| 技术 | 适配度 |
|---|---|
| HTTP 普通接口 | ❌ 等全部生成 |
| WebSocket | ❌ 太重 |
| SSE | ✅ 天生流式 |
AI 的输出特征是:
-
token 级 / 句子级生成
-
可边生成边消费
-
用户随时可能中断
👉 SSE 几乎是最优解
三、第一个坑:Feign 不支持 SSE
项目里原本调用 AI 服务用的是 Feign:
@FeignClient("mb-ai")
RespBean sendQuery(...)
一开始我尝试“硬改”,但很快发现:
Feign 本质是一次性 HTTP 调用,它不支持流式消费响应体
哪怕 AI 服务是 SSE,Feign 也会:
-
等完整响应
-
再反序列化
-
流式直接失效
结论很明确:
❌ Feign 不能用于 SSE
✅ SSE 必须用 WebClient / HttpClient
四、正确姿势:WebClient + SseEmitter
1️⃣ Controller 层:返回 SseEmitter
SSE 接口和普通接口最大的不同是:
返回值不再是业务对象,而是一个“连接本身”
@PostMapping(
value = "/health_manager/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE
)
public SseEmitter healthManagerStream(
@RequestBody HealthManagerQueryDTO request) {
SseEmitter emitter = new SseEmitter(0L); // 不超时
aiService.streamQuery(request, emitter);
return emitter;
}
关键点:
-
produces = text/event-stream -
返回
SseEmitter -
业务逻辑交给 Service
2️⃣ Service 层:WebClient 真正消费 AI 流
webClient.post()
.uri("/health_manager")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)
.bodyValue(request)
.retrieve()
.bodyToFlux(String.class)
.subscribe(
data -> emitter.send(data),
error -> emitter.completeWithError(error),
emitter::complete
);
这段代码的含义是:
-
AI 每吐一段数据
-
我就
emitter.send() -
前端立刻收到
真正实现了“边生成、边返回、边渲染”
五、第二个大坑:UnknownHostException: mb-ai
代码写完,一跑,直接报错:
java.net.UnknownHostException: mb-ai
第一反应:
“不对啊,Feign 一直是能调用 mb-ai 的”
原因分析
-
Feign:自动走注册中心(Nacos / Eureka)
-
WebClient:只认 DNS
.baseUrl("http://mb-ai")
在 WebClient 看来:
mb-ai 就是一个普通域名
但 DNS 根本不认识它
六、正确解法:WebClient 接入服务发现
1️⃣ 引入 LoadBalancer
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2️⃣ 给 WebClient.Builder 加 @LoadBalanced
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
3️⃣ baseUrl 继续用服务名
.baseUrl("http://mb-ai")
此时调用链变成:
WebClient
→ LoadBalancer
→ Nacos
→ 真实 IP:PORT
UnknownHostException 到此彻底解决
七、最终依赖组合(最小可用)
<!-- WebClient / SSE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Nacos(项目里一般已有) -->
spring-cloud-starter-alibaba-nacos-discovery
⚠️ 不会把项目变成 WebFlux
只是“在 MVC 项目里用 WebClient”
八、架构上的最终形态(我现在的做法)
Feign
└── 普通同步接口(兼容老系统)
WebClient
└── SSE 流式接口(AI 问答)
接口层设计成:
POST /health_manager // 非流式
POST /health_manager/stream // SSE
前端可以按需选择。
九、一些实战踩坑总结
❌ Feign 强行做 SSE
→ 行不通
❌ WebClient 不加 LoadBalanced
→ 必炸 UnknownHostException
❌ 忘了 produces
→ 前端收不到流
❌ AI 实际没返回 text/event-stream
→ 你这边再对也没用
十、写在最后
这次改造最大的收获不是“把 SSE 跑通了”,而是更清楚地理解了:
-
Feign 和 WebClient 的边界
-
同步接口和流式接口在架构层面的本质差异
-
AI 场景对交互模型的倒逼
如果你现在也在做:
-
AI 问答
-
长文本生成
-
实时推送
那么,SSE 几乎是绕不开的一步。
更多推荐

所有评论(0)