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 的取消 | 像"总部命令所有分厂停工" | 只要一个分厂出事,所有分厂必须停工 |

相关推荐
BD_Marathon21 小时前
【JavaWeb】Tomcat_WebAPP的标准结构
java·tomcat·web app
小雨下雨的雨21 小时前
第8篇:Redis缓存设计与缓存问题
java·redis·缓存
TT哇1 天前
【@NotBlank】@NotBlank与@NotEmpty与@NotNull区别
java·开发语言
mozhiyan21 天前
Spring Tool Suite4(STS)下载安装保姆级教程(附安装包)
java·spring·eclipse·sts4·sts4下载教程
用户0332126663671 天前
Java 读取或删除 Excel 文件文档属性:Spire.XLS for Java 实用指南
java
忘记9261 天前
GET 请求与 POST 请求的核心区别
java
没有bug.的程序员1 天前
JVM 与 Docker:资源限制的真相
java·jvm·后端·spring·docker·容器
lkbhua莱克瓦241 天前
IO流——打印流
java·开发语言·前端·学习方法
赵得C1 天前
软件设计师前沿考点精讲:新兴技术与性能优化实战
java·开发语言·分布式·算法·设计模式·性能优化
组合缺一1 天前
Solon AI 开发学习17 - generate - 使用复杂提示语
java·学习·ai·llm·solon·mcp