Spring AI 2.0:Flux

方法 作用 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。如果某个元素不需要保留,应该使用 filterhandle

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,用来做资源清理

信号互斥规则

  1. doOnCompletedoOnError 互斥:正常完成就不会进error;抛异常就不会进complete。
  2. doOnCancel 和 complete / error 互斥:手动取消流,既不会complete,也不会error。
  3. 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取消请求)。

关键点:

  1. chatClient.stream().content() 返回 Flux
  2. doOnCancel:前端断开 / 点停止按钮,后端 Flux 会触发 cancel,适合清理资源、计数
  3. doOnError:大模型调用异常、限流、鉴权失败触发
  4. doOnComplete:AI 完整把回答输出完毕,正常结束
  5. 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。
相关推荐
m0_547486661 小时前
《面向对象与Java程序设计》全套PPT课件2026
java·开发语言
小马过河R1 小时前
Graph Engineering 深度解析:模型越强,越需要给它画好“地图”
人工智能·langchain·graph·ai工程化·harness·驾驭工程
leisoo80971 小时前
涨停板次日表现因子怎么挖掘本地化Python全流程实战
大数据·人工智能·python
lucas_AI1 小时前
喂张白纸也能吐出证件号?文档 MLLM 的"关系级泄露"被测出来了
人工智能·算法·掘金技术征文
jason成都1 小时前
ignav-next|Java 层封装 GNSS/INS 组合导航框架,隔离 RTKLIB 与 INS 内核,双模块独立演进、最小侵入
java·开发语言
抓不住时间的沙1 小时前
butterfly主题美化,打造属于自己的个性博客
java·开发语言·前端·javascript·node.js·github
Old Uncle Tom1 小时前
手机银行用户画像设计
人工智能·智能手机
用户3126874877202 小时前
一个请求进来到底发生了什么?Spring MVC 请求处理全链路源码拆解
spring
Zane19942 小时前
从一个发短信的类到多态调用:封装、继承、多态到底是怎么长出来的
java·后端