Spring Boot 中基于 WebClient 的 SSE 流式接口实战

7 阅读3分钟

本文由 本人CSDN博客 转码, 原文地址 Spring Boot 中基于 WebClient 的 SSE 流式接口实战-CSDN博客

—— 从 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")


此时调用链变成:

WebClientLoadBalancerNacos
 → 真实 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 几乎是绕不开的一步