从 Feign 到 WebClient 的一次真实踩坑记录
一、为什么要把同步改成流式?
在最近的一个项目中,我负责接入一个 AI 问答服务。一开始的接口形态非常常规:客户端发请求,服务端等 AI 全部生成完内容,再一次性返回。
问题很快就暴露了:
- AI 返回慢(10 秒甚至更久)
- 用户页面'卡死',体验极差
- 其实 AI 是'边生成边返回'的,但我们完全浪费了这个能力
于是,目标就很明确了:把原有同步接口,改造成支持 SSE(Server-Sent Events)的流式接口。
二、什么是 SSE?为什么适合 AI 场景?
SSE(Server-Sent Events)是一种 服务器主动推送 的 HTTP 通信方式。它基于 HTTP,单向(服务端 → 客户端),保持长连接,传输文本流(text/event-stream)。
返回的数据长这样:
data: 你好
data: 我是
data: 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) {
();
aiService.streamQuery(request, emitter);
emitter;
}

