| 方法 | 作用 | Spring AI 常见用途 |
|---|---|---|
map |
一对一转换 | 内容脱敏、包装 SSE、格式转换 |
filter |
过滤元素 | 忽略空片段或无效事件 |
doOnNext |
观察元素 | 日志、统计输出片段 |
doOnError |
观察异常 | 记录模型或网络异常 |
doFinally |
观察最终结束 | 统计耗时、识别完成/失败/取消 |
onErrorResume |
切换备用流 | 返回友好错误、服务降级 |
timeout |
无信号超时 | 防止模型长期无响应 |
retryWhen |
按策略重试 | 对连接阶段瞬时错误有限重试 |
take |
截取元素 | 调试、预览、主动提前结束 |
concatWith |
追加流 | 添加结束事件或尾部提示 |
collectList |
收集为列表 | 完整输出后批量处理 |
reduce |
聚合为一个值 | 拼接完整回答后存库 |
scan |
持续输出累计值 | 维护当前完整回答快照 |
flatMap |
并发异步转换 | 并发处理多个独立请求 |
concatMap |
顺序异步转换 | 按顺序处理问题或工具结果 |
bufferTimeout |
按数量或时间分批 | 合并片段、降低处理频率 |
把 Mono 类比为"未来可能得到一个结果",把 Flux 类比为"未来可能陆续得到多个结果"。
| 类型 | 元素数量 | 常见用途 |
|---|---|---|
Mono<T> |
0 或 1 个 | 查询单条数据、一次性 AI 回答、保存结果 |
Flux<T> |
0 到 N 个 | AI 流式回答、消息流、列表查询、SSE 推送 |
just:将少量元素封装成流
java
Flux<String> flux = Flux.just("hello", "world", "flux");
from
Flux.from(...) 接收 Reactive Streams 的 Publisher 或者 Flowable 对象。
java
Publisher<String> publisher = obtainPublisher();
Flux<String> flux = Flux.from(publisher);
Flowable<ApplicationResult> = new Application().streamCall(param);
map:同步的一对一转换
java
Flux<String> upperCase = Flux.just("java", "spring")
.map(String::toUpperCase);
Spring AI 中可以用 map 处理每个流式片段:
java
Flux<String> content = chatClient.prompt()
.user(message)
.stream()
.content()
.map(chunk -> chunk.replace("敏感词", "***"));
map 中不能返回 null。如果某个元素不需要保留,应该使用 filter 或 handle。
doOnXxx事件
doFirst /doOnError/doOnComplete /doOnCancel 这一组属于回调钩子(side‑effect 副作用方法),不修改数据流元素,只做事件监听。doOnXxx 只是监听,不会捕获异常,异常依旧向下传播。
- doFirst:订阅发生之前执行,流还没有开始发射数据;
- doOnComplete:流正常走完,全部元素发射完毕,正常完成;
- doOnError:流发生异常,抛出错误;
- doOnCancel:订阅被手动取消(主动终止流,没有走完也没有报错)。
场景1:正常走完,无异常,不取消
java
Flux<Integer> flux1 = Flux.just(1, 2, 3)
.doFirst(() -> System.out.println("✅ doFirst:准备订阅,还没发数据"))
.doOnComplete(() -> System.out.println("✅ doOnComplete:流正常结束,全部数据发射完成"))
.doOnError(e -> System.out.println("❌ doOnError:发生异常:" + e.getMessage()))
.doOnCancel(() -> System.out.println("⚠️ doOnCancel:流被手动取消"));
flux1.subscribe(System.out::println);
✅ doFirst:准备订阅,还没发数据
1
2
3
✅ doOnComplete:流正常结束,全部数据发射完成
场景2:中间抛出异常
java
Flux<Integer> flux2 = Flux.just(1, 2, 3)
.map(i -> {
if (i == 2) {
throw new RuntimeException("模拟业务报错");
}
return i;
})
.doFirst(() -> System.out.println("✅ doFirst:准备订阅"))
.doOnComplete(() -> System.out.println("✅ doOnComplete:正常完成(报错不会进这里)"))
.doOnError(e -> System.out.println("❌ doOnError:捕获异常:" + e.getMessage()))
.doOnCancel(() -> System.out.println("⚠️ doOnCancel:流被手动取消(报错不会进这里)"));
flux2.subscribe(
System.out::println,
error -> System.out.println("【subscribe收到异常】" + error.getMessage())
);
✅ doFirst:准备订阅
1
❌ doOnError:捕获异常:模拟业务报错
【subscribe收到异常】模拟业务报错
场景3:手动cancel取消流,没有报错、没有走完complete
java
Flux<Integer> flux3 = Flux.just(1, 2, 3,4,5)
.doFirst(() -> System.out.println("✅ doFirst:准备订阅"))
.doOnComplete(() -> System.out.println("✅ doOnComplete(取消不会进)"))
.doOnError(e -> System.out.println("❌ doOnError(取消不会进)"))
.doOnCancel(() -> System.out.println("⚠️ doOnCancel:流被手动取消!"));
// subscribe 返回 Disposable,可以手动取消订阅
var disposable = flux3.subscribe(System.out::println);
// 模拟业务:收到部分数据后,手动取消流
disposable.dispose();
✅ doFirst:准备订阅
1
⚠️ doOnCancel:流被手动取消!
场景4:区分 doFirst 位置,doFirst在链不同位置的效果
java
// doFirst是越靠下游,越先执行,从订阅点向上执行
// 链式语法调用是有顺序的,顺序不同效果不同。
Flux.just(10,20)
.doFirst(() -> System.out.println("A doFirst"))
.map(i->i*2)
.doFirst(() -> System.out.println("B doFirst"))
.subscribe(System.out::println);
B doFirst
A doFirst
20
40
Reactor Flux doFirst / doOnComplete / doOnError / doOnCancel
| 方法 | 触发时机 | 说明 |
|---|---|---|
doFirst(Runnable) |
订阅subscribe发生时,数据流还未发出任何元素 | 可以做日志、初始化;注意位置:越靠近下游,越优先执行;不生产数据,仅副作用 |
doOnComplete(Runnable) |
流正常完整结束,所有元素发射完毕,没有异常、没有取消 | 异常、cancel时不会执行;适合正常结束日志、统计 |
doOnError(Consumer<Throwable>) |
流发生异常,向上传播错误信号 | 只在异常场景触发;不会捕获异常,异常继续往下传递;打印异常日志 |
doOnCancel(Runnable) |
订阅被手动dispose()取消,流既没有正常complete,也没有报错 | SSE聊天场景:用户点停止输出,就会触发 doOnCancel,用来做资源清理 |
信号互斥规则
doOnComplete和doOnError互斥:正常完成就不会进error;抛异常就不会进complete。doOnCancel和 complete / error 互斥:手动取消流,既不会complete,也不会error。doFirst:只要发生订阅就执行,无论后续是complete/error/cancel。
实际业务场景(SSE流式聊天)
- doFirst:记录开始流式日志
- doOnComplete:大模型正常输出完毕,记录成功结束
- doOnError:大模型调用报错,打印异常
- doOnCancel:用户点击【停止输出】按钮,dispose取消Flux,触发doOnCancel,做资源清理。
业务场景类比(对应 Spring AI SSE)
- 用户请求进来,开始订阅流 → doFirst;
- AI 完整输出全部 token,服务端正常结束 → doOnComplete;
- 调用大模型接口抛异常 → doOnError;
- 用户前端点【停止回答】,AbortController,后端 Flux 被 cancel → doOnCancel。doOnCancel 非常适合做 SSE 聊天的资源释放,这个钩子只有主动取消才进,报错、正常完成不会进入。
takeWhile
takeWhile(Predicate):满足条件就继续接收元素;一旦条件不满足,直接终止流,发送onComplete信号。
只判断每一个下发出来的元素,条件返回 true 就下发,一旦返回 false:
当前这个不满足的元素直接丢弃
流直接结束,触发 doOnComplete
不会触发 doOnError、不会触发 doOnCancel
java
// 当4到来,4<4=false,条件不成立,流终止;4、5、6 全部不再下发。
// 导致终止的那一条元素不会向下游传递;
Flux.just(1,2,3,4,5,6)
.takeWhile(num -> num <4)
.doFirst(() -> System.out.println("doFirst"))
.doOnComplete(() -> System.out.println("doOnComplete 流结束"))
.doOnCancel(() -> System.out.println("doOnCancel"))
.subscribe(System.out::println);
doFirst
1
2
3
doOnComplete 流结束
在 Spring AI 流式聊天场景可以检测输出内容包含某个敏感词,takeWhile直接终止大模型输出。
c
// 一旦chunk包含敏感词,直接终止流
// ⚠️注意:此时是正常 complete,走doOnComplete,不是 cancel,不走 doOnCancel 钩子。
flux.takeWhile(chunk -> !chunk.contains("敏感词"))
concatWith
concatWith(Publisher<? extends T> other)
先执行当前流,当前流正常 onComplete 完成之后,再去订阅第二个流,把第二个流的数据接续发射出来。串行执行,不是并行;必须等第一个流全部结束,才跑第二个。
- 第一个流异常,第二个不会执行
- takeWhile 提前 complete,concatWith 会执行后面流
java
// 发射 1,2,3 → flux1 正常 complete
// 才去订阅 flux2,发射 10,20,30
Flux<Integer> flux1 = Flux.just(1,2,3)
.doOnComplete(() -> System.out.println("【flux1完成】"));
Flux<Integer> flux2 = Flux.just(10,20,30)
.doFirst(() -> System.out.println("===开始订阅flux2==="));
flux1.concatWith(flux2)
.subscribe(System.out::println);
1
2
3
【flux1完成】
===开始订阅flux2===
10
20
30
在 Spring AI 流式聊天中的业务举例:场景:先输出一段前置提示文本,再输出大模型流式回答
java
Flux<String> prefix = Flux.just("【知识库检索完成,开始回答】\n");
Flux<String> aiStream = chatClient.prompt()
.user(question)
.stream()
.content();
//先输出prefix,等prefix完成之后,再输出AI流式token
return prefix.concatWith(aiStream);
doOnXxx Spring AI 示例
业务场景:SSE 流式聊天接口,记录各个生命周期日志,doOnCancel对应前端点击停止输出(AbortController取消请求)。
关键点:
- chatClient.stream().content() 返回 Flux
- doOnCancel:前端断开 / 点停止按钮,后端 Flux 会触发 cancel,适合清理资源、计数
- doOnError:大模型调用异常、限流、鉴权失败触发
- doOnComplete:AI 完整把回答输出完毕,正常结束
- doFirst:订阅发生,开始推送 token 之前执行
java
@Slf4j
@RestController
@RequestMapping("/rag")
public class RagStreamController {
private final ChatClient chatClient;
public RagStreamController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Data
public static class ChatQueryDTO {
private String sid;
private String question;
}
/**
* SSE流式接口,produces = text/event‑stream
*/
@PostMapping(value = "/streamChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestBody ChatQueryDTO dto) {
String sid = dto.getSid();
String question = dto.getQuestion();
Flux<String> contentFlux = chatClient.prompt()
.user(question)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sid))
.stream()
.content();
// 挂上Reactor生命周期钩子
return contentFlux
.doFirst(() -> {
// 订阅发生,即将开始返回token
log.info("[doFirst] 会话sid={},开始流式问答,用户问题:{}", sid, question);
})
.doOnComplete(() -> {
// ✅ AI完整输出完毕,正常结束
log.info("[doOnComplete] 会话sid={},流式回答全部输出完成", sid);
})
.doOnError(throwable -> {
// ❌ 发生异常:大模型报错、限流、网络异常
log.error("[doOnError] 会话sid={},流式问答异常,error={}", sid, throwable.getMessage(), throwable);
})
.doOnCancel(() -> {
// ⚠️ 重点:前端主动断开连接 / 用户点击【停止输出】触发
// 既没有正常complete,也没有异常,属于人为取消流
log.warn("[doOnCancel] 会话sid={},流式回答被用户主动取消", sid);
});
}
}
各个钩子触发时机(结合前端 @microsoft/fetch‑event‑source):doOnComplete、doOnError、doOnCancel 三者互斥,只会进入其中一个,doFirst:只要订阅就会执行,不管后面结局是 complete /error/cancel
-
doFirst:前端发送请求,后端开始订阅Flux,还没有返回任何 token。用途:打印请求日志、埋点计数。
-
doOnComplete:AI 把全部 token 全部推送给前端,流正常结束。用途:统计成功会话、记录完成时间。
-
doOnError:大模型 API 调用失败、密钥错误、限流超时;代码内部抛出异常。⚠️注意:报错不会执行 doOnComplete /doOnCancel。
-
doOnCancel【SSE 最关键】,Flux 收到取消信号,进入doOnCancel。用途:释放临时资源、记录用户中途终止问答埋点。两种场景会触发:
- 用户点击前端停止按钮:调用 abortController.abort();
- 用户直接关闭浏览器标签页,网络连接断开;
ts
// @microsoft/fetch-event-source
let abortController: AbortController|null = null;
async function chat() {
abortController = new AbortController();
await fetchEventSource("/rag/streamChat",{
method:"POST",
signal: abortController.signal, // 这个signal abort会传递到后端Flux触发doOnCancel
// ...省略headers body
})
}
// 用户点击停止按钮
function stopAnswer(){
if(abortController){
abortController.abort(); // ← 后端进入 doOnCancel
}
}
注意:
- doOnCancel 不会捕获异常,只是监听信号;
- 如果后端直接返回 Flux.error(),只会进doOnError,不会进doOnCancel;
- Spring SSE 场景,只有客户端主动断开才会触发 doOnCancel;
- 服务端主动结束流触发doOnComplete。