从 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) {
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 几乎是绕不开的一步。

