实战分享怎样实现Spring Boot 中基于 WebClient 的 SSE 流式接口操作

在实时交互需求越来越强的今天,"一次请求、持续返回"正在成为很多业务场景的标配:AI 对话逐字输出、日志实时推送、任务执行进度播报、行情订阅、设备状态上报等。相比传统的"请求-响应"模式,这类场景对后端提出了新的要求:连接要保持、数据要增量、延迟要低、资源要可控

在 Spring Boot 技术栈里,SSE(Server-Sent Events)是最轻量、最易落地的一种流式方案,而 WebClient 则是构建上游流式调用与下游透传的关键组件。本文将从原理、架构、代码、排障、优化、生产实践六个层面,完整讲透 **"基于 WebClient 的 SSE 流式接口"**怎么做、怎么稳、怎么上线。


一、为什么是 SSE:先把技术选型讲清楚

在做流式接口前,团队通常会在 SSE、WebSocket、长轮询之间做选择。

1. SSE 的特点

SSE 基于 HTTP,服务端以 text/event-stream 持续向客户端推送消息。浏览器原生支持 EventSource,实现简单,尤其适合"服务端单向推送"的场景。

优点:

  • 基于 HTTP,穿透代理和网关相对容易;
  • 前端接入成本低,浏览器原生支持;
  • 文本流天然适合逐段输出;
  • 自动重连机制友好(前端可控)。

限制:

  • 主要是单向推送(客户端到服务端不适合高频双向);
  • 对二进制数据不友好;
  • 连接管理、超时、代理缓冲需要额外关注。

2. WebSocket vs SSE

  • 需要双向实时交互(如协同编辑、IM)优先 WebSocket;
  • 需要"后端持续输出、前端展示"优先 SSE;
  • 若你在做 AI 生成式输出、任务进度流,SSE 往往是更低成本方案。

3. 为什么要配合 WebClient

很多企业场景不是"本地生成流",而是"调用上游流式接口,再转发给前端"。

例如:你的 Spring Boot 服务调用大模型接口(也是流式),再进行鉴权、审计、脱敏、埋点后,SSE 输出给前端。

此时 WebClient 的响应式流处理能力(Flux)就非常关键。


二、核心架构:上游流式输入 + 中间层治理 + 下游 SSE 输出

典型链路如下:

  1. 前端发起 /api/stream 请求;
  2. Spring Boot Controller 返回 SSE;
  3. Service 内部用 WebClient 调用上游流式接口;
  4. 将上游数据解析为 Flux<Event>;
  5. 做中间处理(过滤、重试、限速、审计);
  6. 以 SSE 事件持续写回前端;
  7. 完成后发送结束事件并关闭连接。

这个架构的核心价值是:
把"流"作为一等公民贯穿全链路,而不是把流拆成多次普通请求。


三、项目准备:依赖与基础配置

先确保你使用的是 Spring Boot WebFlux(不是传统 Spring MVC)。

1)Maven 依赖

xml

<dependencies> <!-- WebFlux + Reactor + WebClient --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 可选:lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 可选:测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

2)application.yml(关键超时)

yaml

server: port: 8080 spring: codec: max-in-memory-size: 10MB

生产中你还要配置 Reactor Netty 的连接超时、读写超时、连接池参数,后文会讲。


四、第一版可运行代码:从 0 到 1 打通 SSE

1)定义 WebClient Bean

java

@Configuration public class WebClientConfig { @Bean public WebClient webClient(WebClient.Builder builder) { return builder .baseUrl("https://example-upstream.com") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } }

2)Service:调用上游流并转为 Flux

假设上游返回的是逐行文本流(每行一段 JSON 或文本):

java

@Service @RequiredArgsConstructor public class StreamService { private final WebClient webClient; public Flux<String> upstreamStream(String prompt) { Map<String, Object> body = Map.of("prompt", prompt, "stream", true); return webClient.post() .uri("/v1/generate") .bodyValue(body) .retrieve() .bodyToFlux(String.class) // 视上游格式可换成 DataBuffer/自定���DTO .filter(line -> line != null && !line.isBlank()); } }

3)Controller:输出 SSE

java

@RestController @RequestMapping("/api") @RequiredArgsConstructor public class StreamController { private final StreamService streamService; @GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> sse(@RequestParam String prompt) { return streamService.upstreamStream(prompt) .map(chunk -> ServerSentEvent.<String>builder() .event("message") .data(chunk) .build()) .concatWith(Flux.just( ServerSentEvent.<String>builder() .event("done") .data("[DONE]") .build() )); } }

到这里,一个最小可用版本就完成了。浏览器访问后会持续收到事件。


五、前端对接:最小可用示例

html

<script> const es = new EventSource('/api/sse?prompt=你好'); es.onmessage = (e) => { console.log('message:', e.data); }; es.addEventListener('done', (e) => { console.log('done:', e.data); es.close(); }); es.onerror = (e) => { console.error('sse error', e); es.close(); }; </script>

注意:EventSource 默认是 GET 请求。如果你需要 POST 传复杂参数,常见做法是:

  • 先 POST 创建任务拿 streamId,再 GET /sse/{streamId} 订阅;
  • 或前端改用 fetch + ReadableStream(不走 EventSource)。

六、企业实战增强:错误处理、心跳、取消、重试

最小版能跑,但离生产可用还差很远。

1)错误处理(给前端可理解事件)

java

@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> sse(@RequestParam String prompt) { return streamService.upstreamStream(prompt) .map(data -> ServerSentEvent.builder(data).event("message").build()) .onErrorResume(ex -> Flux.just( ServerSentEvent.builder("stream failed: " + ex.getMessage()).event("error").build() )) .concatWith(Flux.just(ServerSentEvent.builder("[DONE]").event("done").build())); }

2)心跳保活(防止中间层断连)

某些网关/代理会在长时间无数据时断开连接。可定期发心跳:

java

Flux<ServerSentEvent<String>> heartbeat = Flux.interval(Duration.ofSeconds(15)) .map(i -> ServerSentEvent.<String>builder().event("ping").data("heartbeat").build()); return Flux.merge( businessFlux, // 业务流 heartbeat ).takeUntil(event -> "done".equals(event.event()));

3)客户端取消感知

当前端关闭连接,后端应尽快释放资源。可在 doOnCancel 中记录并清理:

java

return streamService.upstreamStream(prompt) .doOnCancel(() -> log.info("client cancelled, prompt={}", prompt)) .map(...) .doFinally(signal -> log.info("stream finished: {}", signal));

4)重试策略(谨慎)

流式接口重试要非常小心,避免重复输出。可只对"建立连接失败"重试:

java

.retryWhen(Retry.backoff(2, Duration.ofMillis(300)) .filter(ex -> ex instanceof WebClientRequestException));


七、性能与稳定性:生产环境必须关注的 8 件事

  1. 网关缓冲关闭:Nginx 要关闭代理缓冲(proxy_buffering off;),否则"流"会变"攒一坨再发"。
  2. 超时合理化:读取超时不能按普通接口设置得太短。
  3. 连接池上限:高并发 SSE 会占用连接,要调大连接池并限制单用户并发。
  4. 背压与限流:上游过快、下游过慢时要有节流策略。
  5. 消息大小控制:避免超大 chunk,建议分片输出。
  6. 日志采样:流式接口日志量大,避免全量打印 chunk。
  7. 安全审计:对输出内容做脱敏、敏感词过滤。
  8. 可观测性:埋点首包延迟、平均流时长、中断率、完成率。

八、常见问题排查清单(非常实用)

  • 前端收不到流,只在最后一次性收到?看网关是否缓冲、压缩是否干扰、响应头是否正确。
  • 浏览器频繁断开重连?看心跳间隔、代理空闲超时、服务端是否抛异常。
  • 中文乱码?确保 UTF-8,响应头与消息编码一致。
  • 连接数暴涨?检查前端是否重复创建 EventSource 未 close。
  • CPU 高?检查是否对每个 chunk 做重计算/重序列化。

九、一个更贴近 AI 场景的输出协议建议

建议统一事件类型,前端更好处理:

  • start:流开始(带 requestId)
  • delta:增量文本
  • meta:token 用量、阶段信息
  • error:错误信息
  • done:结束标记

这样前后端协议稳定,后续接入多模型/多上游也不容易乱。

基于 Spring Boot + WebClient 实现 SSE 流式接口,本质是把"实时返回"能力工程化:

不仅要"能流式输出",更要"可治理、可观测、可扩展"。

你可以把它理解为三步:

  1. 用 WebFlux 把流打通;
  2. 用策略把稳定性补齐;
  3. 用指标把生产闭环建立。

做到这三点,SSE 就不再是 demo 技术,而是可承载核心业务的正式能力。

相关推荐
青衫码上行2 小时前
【从零开始学习JVM】程序计数器
java·jvm·学习·面试
不吃香菜学java10 小时前
Redis的java客户端
java·开发语言·spring boot·redis·缓存
码事漫谈10 小时前
大模型输出的“隐性结构塌缩”问题及对策
前端·后端
captain37610 小时前
事务___
java·数据库·mysql
北漂Zachary10 小时前
四大编程语言终极对比
android·java·php·laravel
小江的记录本10 小时前
【网络安全】《网络安全常见攻击与防御》(附:《六大攻击核心特性横向对比表》)
java·网络·人工智能·后端·python·安全·web安全
努力的小雨11 小时前
龙虾量化实战法(QClaw)
后端
橙露11 小时前
SpringBoot 整合 MinIO:分布式文件存储上传下载
spring boot·分布式·后端
小眼哥12 小时前
SpringBoot整合Vue代码生成exe运行程序以及windows安装包
vue.js·windows·spring boot