一、背景:为什么我要做 SSE?
在最近的一个项目中,我负责接入一个 AI 问答服务。
一开始的接口形态非常常规:
@PostMapping("/health_manager") public RespBean<HealthManagerQueryDataVO> sendQuery
记录了在 Spring Boot 项目中将同步接口改造为 SSE 流式接口的过程。针对 AI 问答服务响应慢的问题,发现 Feign 不支持流式消费,改用 WebClient 配合 SseEmitter 实现服务端推送。解决了 WebClient 调用内部服务时的 UnknownHostException 问题(通过引入 LoadBalancer)。最终实现了边生成边返回的流式交互,提升了用户体验。
在最近的一个项目中,我负责接入一个 AI 问答服务。
一开始的接口形态非常常规:
@PostMapping("/health_manager") public RespBean<HealthManagerQueryDataVO> sendQuery
客户端发请求,服务端等 AI 全部生成完内容,再一次性返回。
问题很快就暴露了:
于是,目标就很明确了:
把原有同步接口,改造成支持 SSE(Server-Sent Events)的流式接口
SSE(Server-Sent Events)是一种 服务器主动推送 的 HTTP 通信方式:
text/event-stream)返回的数据长这样:
data: 你好 data: 我是 data: AI
客户端可以一边接收,一边渲染。
| 技术 | 适配度 |
|---|---|
| HTTP 普通接口 | ❌ 等全部生成 |
| WebSocket | ❌ 太重 |
| SSE | ✅ 天生流式 |
AI 的输出特征是:
👉 SSE 几乎是最优解
项目里原本调用 AI 服务用的是 Feign:
@FeignClient("mb-ai") RespBean sendQuery(...)
一开始我尝试'硬改',但很快发现:
Feign 本质是一次性 HTTP 调用,它不支持流式消费响应体
哪怕 AI 服务是 SSE,Feign 也会:
结论很明确:
❌ Feign 不能用于 SSE
✅ SSE 必须用 WebClient / HttpClient
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-streamSseEmitterwebClient.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
);
这段代码的含义是:
emitter.send()真正实现了'边生成、边返回、边渲染'
代码写完,一跑,直接报错:
java.net.UnknownHostException: mb-ai
第一反应:
'不对啊,Feign 一直是能调用 mb-ai 的'
.baseUrl("http://mb-ai")
在 WebClient 看来:
mb-ai 就是一个普通域名
但 DNS 根本不认识它
<!-- Maven dependency -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
.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
前端可以按需选择。
→ 行不通
→ 必炸 UnknownHostException
→ 前端收不到流
→ 你这边再对也没用
这次改造最大的收获不是'把 SSE 跑通了',而是更清楚地理解了:
如果你现在也在做:
那么,SSE 几乎是绕不开的一步。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online