Reactor 中的 doOnError 与 doOnCancel

一、基本概念

  • 错误是流终止的主动原因:例如上游逻辑抛出异常。
  • 取消是流终止的被动原因:例如外部主动取消订阅。
  • 优先级规则:如果错误已经发生,Reactor 会优先处理错误,不会多余地触发取消。

Maven依赖:

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bluefoxyu</groupId>
    <artifactId>untitled</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/io.projectreactor/reactor-core -->
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>3.8.0</version>
        </dependency>
    </dependencies>

</project>

二、单个 Flux 示例

1. 触发 doOnError(不会触发 doOnCancel

java 复制代码
import reactor.core.publisher.Flux;
import java.time.Duration;

public class CancelErrorDemo1 {
    public static void main(String[] args) throws InterruptedException {

        Flux.interval(Duration.ofMillis(100)) // 每 100ms 产生一个数字 (0, 1, 2, ...)
        .doOnNext(i -> System.out.println("上游产生了: " + i))
        .map(i -> {
            if (i == 3) {
                throw new RuntimeException("在 3 处引爆!");
            }
            return "处理后: " + i;
        })
        .doOnCancel(() -> System.out.println("!!! 上游感知到 cancel 信号,停止生产 !!!"))
        .doOnError(e -> System.err.println("--- doOnError 感知到: " + e.getMessage() + " ---"))
        .subscribe(
            System.out::println,
            error -> System.err.println("--- 最终订阅者处理错误: " + error.getMessage() + " ---")
        );

        Thread.sleep(2000); // 保持主线程存活
    }
}

输出:

关键点:

  • 错误信号(onError)是主动终止原因:当 map 抛出异常时,错误信号直接传播到下游,触发 doOnError 和订阅者的错误回调。
  • 取消信号(cancel)不会被触发:错误终止是"计划内"的终止逻辑,无需额外触发取消。

2. 触发 doOnCancel(无错误,仅主动取消)

java 复制代码
import reactor.core.publisher.Flux;
import reactor.core.Disposable;
import java.time.Duration;

public class CancelDemo {
    public static void main(String[] args) throws InterruptedException {
        Disposable disposable = Flux.interval(Duration.ofMillis(100))
        .doOnNext(i -> System.out.println("上游产生了: " + i))
        .doOnCancel(() -> System.out.println("!!! 上游感知到 cancel 信号,停止生产 !!!"))
        .subscribe(i -> System.out.println("处理后: " + i));

        Thread.sleep(300); // 运行一会
        disposable.dispose(); // 主动取消订阅
        Thread.sleep(200); // 等待 cancel 信号传播
    }
}

输出:

关键点:

  • 取消信号是被动终止原因:通过 Disposable.dispose() 主动取消订阅时,上游会收到 cancel 信号,触发 doOnCancel
  • 无错误发生,因此 doOnError 不会被触发。

三、多个 Flux 在一条链上

示例 1:错误传播与取消副作用

java 复制代码
import reactor.core.publisher.Flux;
import java.time.Duration;

public class MergeCancelErrorDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Flux<Integer> flux1 = Flux.range(1, 5)
        .delayElements(Duration.ofMillis(100))
        .doOnNext(i -> System.out.println("[flux1 emit] " + i))
        .doOnCancel(() -> System.out.println(">>> flux1 cancel"))
        .doOnError(e -> System.err.println(">>> flux1 error: " + e.getMessage()));

        Flux<String> flux2 = flux1.map(i -> {
            if (i == 3) throw new RuntimeException("flux2 在 3 处爆了!");
            return "F2-" + i;
        })
        .doOnCancel(() -> System.out.println(">>> flux2 cancel"))
        .doOnError(e -> System.err.println(">>> flux2 error: " + e.getMessage()));

        Flux<String> flux3 = flux2.map(s -> "F3(" + s + ")")
        .doOnCancel(() -> System.out.println(">>> flux3 cancel"))
        .doOnError(e -> System.err.println(">>> flux3 error: " + e.getMessage()));

        flux3.subscribe(
            data -> System.out.println("下游收到: " + data),
            error -> System.err.println("=== 下游接收到错误: " + error.getMessage() + " ===")
        );

        Thread.sleep(2000);
    }
}

关键点:

  1. 错误信号向上游传播:
    1. flux2i=3 时抛出异常,触发自身的 doOnError
    2. 错误信号继续传播到 flux1,导致 flux1 被取消(触发 doOnCancel)。
  2. 取消是错误的副作用:
    1. 上游(flux1)的 doOnCancel 是因下游错误而间接触发的,而非主动取消。

示例 2:merge 合并多个 Flux

java 复制代码
import reactor.core.publisher.Flux;
import java.time.Duration;

public class MergeCancelErrorDemo {
    public static void main(String[] args) throws InterruptedException {

        // flux1:正常流,只打印 cancel 信号
        Flux<String> flux1 = Flux.interval(Duration.ofMillis(100))
        .map(i -> "F1-" + i)
        .doOnNext(s -> System.out.println("[flux1 emit] " + s))
        .doOnCancel(() -> System.out.println(">>> flux1 cancel"))
        .doOnError(e -> System.err.println(">>> flux1 error: " + e.getMessage()));

        // flux2:在 i==3 时抛出异常
        Flux<String> flux2 = Flux.interval(Duration.ofMillis(150))
        .map(i -> {
            if (i == 3) throw new RuntimeException("flux2 爆了!");
            return "F2-" + i;
        })
        .doOnNext(s -> System.out.println("[flux2 emit] " + s))
        .doOnCancel(() -> System.out.println(">>> flux2 cancel"))
        .doOnError(e -> System.err.println(">>> flux2 error: " + e.getMessage()));

        // merge 合并两个 flux
        Flux.merge(flux1, flux2)
        .subscribe(
            data -> System.out.println("下游收到: " + data),
            error -> System.err.println("=== 下游接收到错误: " + error.getMessage() + " ===")
        );

        Thread.sleep(1000);
    }
}

关键点:

  1. merge 的错误传播:
    1. flux2 抛出异常时,错误信号会终止整个合并后的流。
    2. 所有活跃的上游(flux1flux2)都会收到取消信号(即使 flux1 本身无错误)。
  2. 取消信号的广播:
    1. merge 在收到错误后,会向所有输入流发送取消信号,确保资源释放。

四、总结

1. 错误 vs 取消

|-----------------|-----|------------------------------------------|
| 信号类型 | 主动性 | 特点 |
| 错误(onError) | 主动 | 主动终止信号,可能触发上游的 cancel(但不会重复触发 doOnError) |
| 取消(cancel) | 被动 | 通常由外部主动取消或错误传播引起 |


2. 操作符链中的信号传播

  • 错误信号会优先向上游传播,可能导致上游触发 doOnCancel
  • merge 等组合操作符会广播取消信号到所有输入流。

五、通俗类比

|----------------|---------------|-----------------------------------|
| 概念 | 类比 | 含义 |
| doOnError | 像"工厂着火报警" | 立即停止生产,处理火灾(错误),无需再通知"停止生产"(取消) |
| doOnCancel | 像"接到上级通知停工" | 可能是外部原因(市场关闭),也可能是内部火灾(错误)引发的连锁反应 |
| merge 的取消 | 像"总部命令所有分厂停工" | 只要一个分厂出事,所有分厂必须停工 |

相关推荐
CoderYanger2 小时前
B.双指针——3194. 最小元素和最大元素的最小平均值
java·开发语言·数据结构·算法·leetcode·职场和发展·1024程序员节
程序猿20232 小时前
项目结构深度解析:理解Spring Boot项目的标准布局和约定
java·spring boot·后端
RainbowSea2 小时前
内网穿透配置和使用
java·后端
RainbowSea2 小时前
13. Spring AI 的观测性
java·spring·ai编程
心之伊始3 小时前
Java synchronized 锁升级全过程深度解析:从 Mark Word 到偏向锁、轻量级锁与重量级锁的 HotSpot 实现
java·开发语言·word
阿Y加油吧4 小时前
Java SE核心面试题总结——day 01
java
2021_fc4 小时前
Flink入门指南:使用Java构建第一个Flink应用
java·大数据·flink
Java开发追求者4 小时前
vscode导入springboot项目
java·ide·spring boot·vscode
麦烤楽鸡翅4 小时前
坚持60s (攻防世界)
java·网络安全·jar·ctf·misc·反编译·攻防世界