响应式流中的错误处理:Project Reactor 异常治理全体系

Project Reactor 是Java响应式编程库 ,提供MonoFlux 核心类型,支持非阻塞、背压及异步数据流处理 。它是Spring WebFlux 的基础,适用于构建高并发低延迟的微服务与事件驱动应用,遵循Reactive Streams规范。

1. 响应式编程入门:从阻塞困境到数据流之美

2. 深入 Project Reactor:从原理到工程实践的全面指南

3. Flux 与 Mono:Project Reactor 核心响应式类型深度解析

4. Mono:Project Reactor 中最精巧的响应式原语

5. 创建 Flux/Mono 并订阅:Project Reactor 响应式编程的第一步

6. 程序化创建响应式序列:Flux.generate、Flux.create 与 Flux.push 深度解析

7. 线程调度与 Schedulers:Project Reactor 并发模型的核心引擎

8. 响应式流中的错误处理:Project Reactor 异常治理全体系

9. Sinks API:Project Reactor 中程序化发射数据的现代方案

引言

在命令式编程中,异常处理遵循 try-catch-finally 的三层结构------简单、直观、线程绑定。但在响应式编程中,这套模型彻底失效了:

  • 数据在异步线程上流动,try-catch 无法跨越线程边界
  • 错误是信号(onError),而非抛出的异常
  • 一个错误信号会终止整条流,后续所有元素不再发射
  • 你无法用 throw 来"中断"流------因为流本来就在异步执行

Reactor 的设计哲学:错误不是意外,而是数据流的一部分。

错误像数据一样,可以被变换、过滤、重试、降级、传播。

Project Reactor 官方文档在 Core Features 章节以 "Error Handling" 为题,系统性地阐述了响应式流中错误处理的完整体系。本文将围绕该文档的核心内容,从错误信号的传播机制,到各类错误处理操作符的精确语义,再到工程实践中的错误治理策略,进行全方位深度解析。

一、错误在响应式流中的本质

1.1 Reactive Streams 规范中的错误信号

Reactive Streams 规范定义了三种信号:

java 复制代码
Publisher
  ├── onNext(T)        → 数据信号(0..N 次)
  ├── onComplete()     → 成功终止信号(最多 1 次)
  └── onError(Throwable) → 错误终止信号(最多 1 次)

关键规则:

  • onComplete 与 onError 互斥------一条流只能以其中一种方式终止
  • 一旦发出 onComplete 或 onError,流的生命周期结束,不再有任何后续信号
  • 错误信号会沿操作符链向下传播,直到被某个操作符拦截处理

1.2 错误传播的 Marble 图

java 复制代码
正常流:
──1──2──3──|>
             onComplete

错误流:
──1──2──X──|>
          onError(e)
          (3 不会被发射)

错误被捕获:
──1──2──X──fallback──|>
          ↑              ↑
     onError 被拦截   onErrorResume 发射替代值

1.3 错误信号的不可恢复性(在流级别)

java 复制代码
// 错误发生后,流终止。后续元素不再发射。
Flux.just(1, 2, 0, 4)
    .map(i -> 100 / i)
    .subscribe(
        System.out::println,
        error -> System.err.println("Error: " + error)
    );
// 输出: 100, 50, Error: java.lang.ArithmeticException: / by zero
// 注意:4 永远不会被处理

⚠️ 这是理解 Reactor 错误处理的前提:错误是终端事件。一旦错误信号到达某个操作符且未被捕获,该操作符之后的所有操作符都不会再收到 onNext。

二、subscribe() 中的错误处理:最后一道防线

2.1 基本用法

java 复制代码
Flux.just(1, 2, 0, 4)
    .map(i -> 100 / i)
    .subscribe(
        data  -> System.out.println("Data: " + data),       // onNext
        error -> System.err.println("Error: " + error),     // onError ← 错误处理
        ()    -> System.out.println("Done!")                // onComplete
    );

subscribe 的第二个参数 Consumer 是最终的错误兜底。如果链上没有任何操作符捕获错误,最终会到达这里。

2.2 无错误处理器的危险

java 复制代码
// ⚠️ 危险:没有 error consumer
Flux.error(new RuntimeException("boom"))
    .subscribe(System.out::println);
// 抛出 ErrorCallbackNotImplement 异常!

Reactor 强制要求:如果 subscribe 时没有提供 error handler,未处理的错误会以 ErrorCallbackNotImplement 的形式抛出。这是 Reactor 防止"静默吞掉异常"的保护机制。

2.3 适用场景

场景 是否适合在 subscribe 中处理错误
简单的终端消费 ✅ 适合
需要降级/恢复 ❌ 用 onErrorResume
需要重试 ❌ 用 retryWhen
需要映射错误类型 ❌ 用 onErrorMap
需要记录日志 ❌ 用 doOnError

最佳实践:subscribe 中的 error handler 只应作为最后的安全网,不应承载业务逻辑。

三、doOnError:记录错误的副作用

3.1 语义

doOnError 是一个副作用操作符------它观察错误信号、执行副作用(如日志记录),但不改变错误信号本身。错误会继续向下游传播。

java 复制代码
Flux.just(1, 2, 0)
    .map(i -> 100 / i)
    .doOnError(error -> {
        // 副作用:记录日志、发送告警
        log.error("Computation failed: {}", error.getMessage(), error);
        metricsService.recordError("division", error);
    })
    .subscribe(
        System.out::println,
        error -> System.err.println("Still got error: " + error)  // 错误继续传播到这里
    );

3.2 与 try-catch 中 log 的类比

java 复制代码
// 命令式等价物
try {
    int result = 100 / i;
} catch (Exception e) {
    log.error("Failed", e);  // 只记录,不处理
    throw e;                 // 继续抛出
}

3.3 关键特性

特性 说明
不消费错误 错误继续向下游传播
不改变流 流仍然以 onError 终止
纯副作用 日志、指标、告警
位置敏感 只能观察到其上游的错误

3.4 带条件的 doOnError

java 复制代码
// 只记录特定类型的错误
flux.doOnError(TimeoutException.class, e -> 
    log.warn("Timeout detected: {}", e.getMessage()));

// 使用 Predicate
flux.doOnError(e -> e instanceof TransientException, e -> 
    log.warn("Transient error, will retry: {}", e.getMessage()));

四、onErrorReturn:用默认值替代错误

4.1 语义

当上游发出错误信号时,发射一个预设的默认值,然后正常完成(onComplete)。错误被"吞掉"。

java 复制代码
Flux.just(1, 2, 0, 4)
    .map(i -> 100 / i)
    .onErrorReturn(-1)     // 错误时返回 -1
    .subscribe(System.out::println);
// 输出: 100, 50, -1
// 注意:4 仍然不会被处理(错误已经终止了上游流)

4.2 带条件的 onErrorReturn

java 复制代码
// 只对特定异常类型返回默认值
flux.onErrorReturn(ArithmeticException.class, -1);

// 使用 Predicate
flux.onErrorReturn(e -> e instanceof ArithmeticException, -1);

4.3 适用场景

java 复制代码
// 场景:查询用户,找不到时返回匿名用户
Mono<User> user = userRepository.findById(id)
    .onErrorReturn(UserNotFoundException.class, User.ANONYMOUS);

// 场景:配置读取失败,使用默认配置
Mono<Config> config = configService.load()
    .onErrorReturn(Config.DEFAULT);

4.4 命令式类比

java 复制代码
// 等价于
try {
    return compute(i);
} catch (ArithmeticException e) {
    return -1;
}

五、onErrorResume:切换到备用流

5.1 语义

当上游发出错误信号时,切换到另一个 Publisher 继续发射数据。这是最灵活的错误恢复方式。

java 复制代码
Flux.just(1, 2, 0, 4)
    .map(i -> 100 / i)
    .onErrorResume(error -> {
        // 切换到备用数据源
        return Flux.just(-1, -2);
    })
    .subscribe(System.out::println);
// 输出: 100, 50, -1, -2

5.2 带条件的 onErrorResume

java 复制代码
// 只对特定异常类型降级
flux.onErrorResume(TimeoutException.class, e -> fallbackService.getData());

// 使用 Predicate
flux.onErrorResume(e -> e.getCause() instanceof SQLException, e -> cacheService.getData());

5.3 经典降级模式

java 复制代码
public Mono<Product> getProduct(Long id) {
    return primaryService.getProduct(id)
        .timeout(Duration.ofSeconds(3))
        // 超时 → 尝试备用服务
        .onErrorResume(TimeoutException.class, e -> 
            backupService.getProduct(id))
        // 备用服务也失败 → 从缓存读取
        .onErrorResume(e -> 
            cacheService.get("product:" + id))
        // 缓存也没有 → 返回默认商品
        .onErrorReturn(Product.DEFAULT);
}

5.4 与 onErrorReturn 的区别

维度 onErrorReturn onErrorResume
返回值类型 单个值 T 一个 Publisher
后续行为 发射值后 onComplete 切换到新流,由新流决定终止方式
灵活度 低(固定值) 高(可以是另一个异步操作)
典型场景 简单默认值 备用服务、缓存、复杂降级逻辑

5.5 命令式类比

java 复制代码
// onErrorReturn 等价于
try {
    return primaryService.get();
} catch (Exception e) {
    return DEFAULT_VALUE;
}

// onErrorResume 等价于
try {
    return primaryService.get();
} catch (Exception e) {
    return backupService.get();  // 调用另一个方法
}

六、onErrorMap:变换异常类型

6.1 语义

将上游的异常映射/包装为另一种异常,然后继续传播 onError 信号。不消费错误,只变换错误。

java 复制代码
Mono<User> user = userRepository.findById(id)
    .onErrorMap(
        DataAccessException.class,
        e -> new ServiceException("Failed to load user: " + id, e)
    );

6.2 带条件的 onErrorMap

java 复制代码
flux.onErrorMap(
    e -> e instanceof SQLException,
    e -> new RepositoryException("Database error", e)
);

6.3 工程中的典型用法:异常层次转换

java 复制代码
// 将底层技术异常转换为业务异常
public Mono<Order> createOrder(OrderRequest request) {
    return orderRepository.save(new Order(request))
        .onErrorMap(
            DataIntegrityViolationException.class,
            e -> new DuplicateOrderException("Order already exists", e)
        )
        .onErrorMap(
            DataAccessException.class,
            e -> new OrderPersistenceException("Failed to save order", e)
        );
}

6.4 命令式类比

java 复制代码
try {
    return repository.save(entity);
} catch (DataAccessException e) {
    throw new ServiceException("Save failed", e);  // 包装后重新抛出
}

七、handle():在操作符中手动控制错误

7.1 语义

handle() 是 map() 的增强版------它允许你在转换过程中选择性地发射值、发出错误、或跳过元素。

java 复制代码
Flux.just(1, 2, 0, 4)
    .handle((i, sink) -> {
        if (i == 0) {
            sink.error(new IllegalArgumentException("Zero not allowed"));
        } else {
            sink.next(100 / i);
        }
    })
    .subscribe(System.out::println);

7.2 跳过元素(类似 filter + map)

java 复制代码
Flux.just("1", "abc", "3", "def", "5")
    .handle((s, sink) -> {
        try {
            sink.next(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            // 不调用 next,也不调用 error → 跳过该元素
            log.debug("Skipping non-numeric: {}", s);
        }
    })
    .subscribe(System.out::println);
// 输出: 1, 3, 5

7.3 与 filter + map 的对比

java 复制代码
// filter + map(两次遍历)
flux.filter(s -> s.matches("\\d+"))
    .map(Integer::parseInt);

// handle(一次遍历,更灵活)
flux.handle((s, sink) -> {
    if (s.matches("\\d+")) {
        sink.next(Integer.parseInt(s));
    }
    // 不匹配则静默跳过
});

八、retry 与 retryWhen:重试策略

8.1 retry(long n):简单重试

java 复制代码
// 最多重试 3 次(共执行 4 次)
Mono<Response> result = httpClient.get(url)
    .retry(3);

⚠️ retry(n) 会重新订阅源 Publisher。对于 Cold Publisher,这意味着重新执行整个操作。

8.2 retry(Predicate):条件重试

java 复制代码
// 只对特定异常重试
flux.retry(e -> e instanceof TransientException);

8.3 retryWhen(Retry):高级重试策略(推荐)

Reactor 3.3+ 引入了 reactor.util.retry.Retry 类,提供了声明式的重试策略:

java 复制代码
import reactor.util.retry.Retry;

Mono<Response> result = httpClient.get(url)
    .retryWhen(
        Retry.backoff(3, Duration.ofSeconds(1))     // 最多重试 3 次,初始退避 1 秒
            .maxBackoff(Duration.ofSeconds(30))      // 最大退避 30 秒
            .jitter(0.5)                             // 50% 随机抖动
            .filter(e -> e instanceof TransientException)  // 只重试瞬态异常
            .doBeforeRetry(signal -> 
                log.warn("Retry attempt {}: {}", 
                    signal.totalRetries() + 1, 
                    signal.failure().getMessage()))
            .onRetryExhaustedThrow((retry, signal) -> 
                new ServiceUnavailableException("All retries exhausted", signal.failure()))
    );

8.4 Retry 策略构建器 API

java 复制代码
Retry retrySpec = Retry
    // 固定次数退避
    .backoff(maxAttempts, firstBackoff)
    // 固定间隔(无退避)
    .fixedDelay(maxAttempts, delay)
    // 无限重试(谨慎!)
    .indefinitely()
    // 自定义
    .from(retrySignal -> ...);

// 配置选项
retrySpec
    .maxBackoff(Duration)           // 最大退避时间
    .jitter(double)                 // 抖动因子 [0, 1]
    .filter(Predicate<Throwable>)   // 只重试匹配的异常
    .transientErrors(boolean)       // 是否视为瞬态错误
    .doBeforeRetry(Consumer)        // 重试前回调
    .doAfterRetry(Consumer)         // 重试后回调
    .doBeforeRetryAsync(Function)   // 异步重试前回调
    .scheduler(Scheduler)           // 退避等待使用的调度器
    .onRetryExhaustedThrow(BiFunction)  // 重试耗尽时的异常
    .withThrowable(Function)        // 包装最终异常

8.5 退避策略时序

java 复制代码
Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10))

Attempt 1: 立即执行 → 失败
Wait:      ~1s (± jitter)
Attempt 2: 重试 → 失败
Wait:      ~2s (± jitter)
Attempt 3: 重试 → 失败
Wait:      ~4s (± jitter)
Attempt 4: 重试 → 失败
→ onRetryExhaustedThrow → 抛出最终异常

8.6 重试与资源清理

java 复制代码
// ⚠️ 重试会重新订阅,确保资源正确释放
Flux<Data> resilient = Flux.using(
    () -> openConnection(),                    // 资源创建
    conn -> Flux.fromIterable(conn.fetch()),   // 使用资源
    conn -> conn.close()                       // 资源清理(每次重试都会执行)
).retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));

九、onErrorContinue:跳过错误继续处理

9.1 语义

onErrorContinue 是一种特殊的错误处理模式------它不终止流,而是跳过导致错误的元素,继续处理后续元素。

java 复制代码
Flux.just(1, 2, 0, 4, 5)
    .map(i -> 100 / i)
    .onErrorContinue((error, element) -> {
        log.warn("Skipping element {} due to: {}", element, error.getMessage());
    })
    .subscribe(System.out::println);
// 输出: 100, 50, 25, 20
// 注意:0 被跳过,4 和 5 继续处理!

9.2 ⚠️ 重大警告

官方文档明确警告:

onErrorContinue 是一个不寻常的操作符。它不遵循标准的响应式流语义:

  • 它不是捕获 onError 信号,而是改变上游操作符的行为
  • 并非所有操作符都支持它(只有明确适配的操作符才会生效)
  • 它可能产生反直觉的行为
  • 在生产代码中应谨慎使用

9.3 支持 onErrorContinue 的操作符

java 复制代码
// ✅ 支持的操作符
map, filter, flatMap, handle, concatMap, delayElements, ...

// ❌ 不支持的操作符
onErrorResume, retry, switchOnFirst, ...

9.4 替代方案

java 复制代码
// ✅ 更安全的替代:用 flatMap + onErrorResume 逐个处理
Flux.just(1, 2, 0, 4, 5)
    .flatMap(i -> Mono.fromCallable(() -> 100 / i)
        .onErrorResume(e -> {
            log.warn("Skipping {}", i);
            return Mono.empty();  // 跳过该元素
        }))
    .subscribe(System.out::println);
// 输出: 100, 50, 25, 20

十、doFinally:无论成功或失败都执行清理

10.1 语义

doFinally 在流终止时执行副作用------无论是 onComplete、onError 还是 取消(cancel)。

java 复制代码
Flux.just(1, 2, 3)
    .map(i -> 100 / i)
    .doFinally(signalType -> {
        // signalType: ON_COMPLETE, ON_ERROR, 或 CANCEL
        log.info("Stream terminated with signal: {}", signalType);
        metricsService.recordCompletion(signalType);
    })
    .subscribe(System.out::println);

10.2 命令式类比:finally 块

java 复制代码
// 命令式等价物
Connection conn = null;
try {
    conn = openConnection();
    return conn.query();
} catch (Exception e) {
    throw e;
} finally {
    if (conn != null) conn.close();  // 无论如何都执行
}

10.3 与 doOnTerminate 的区别

操作符 触发条件 是否包含 cancel
doOnTerminate onComplete 或 onError ❌ 不包含
doFinally onComplete、onError、cancel ✅ 包含

10.4 资源清理模式

java 复制代码
Flux<Data> safe = Flux.using(
    () -> acquireResource(),
    resource -> resource.getDataStream(),
    resource -> resource.release()     // 类似 doFinally,但更精确
);

// 或者用 doFinally
Flux<Data> safe2 = acquireResource()
    .flatMapMany(res -> res.getDataStream())
    .doFinally(signal -> releaseResource());

十一、Flux.using() / Mono.using():响应式资源管理

11.1 语义

using() 是响应式版的 try-with-resources------它管理一个资源的创建、使用、清理三个阶段。

java 复制代码
public static <T, D> Flux<T> using(
    Callable<? extends D> resourceSupplier,     // 创建资源
    Function<? super D, ? extends Publisher<? extends T>> sourceSupplier,  // 使用资源
    Consumer<? super D> resourceCleanup         // 清理资源(无论成功/失败)
)

11.2 示例:数据库连接管理

java 复制代码
Flux<Row> results = Flux.using(
    () -> database.acquireConnection(),           // 创建
    conn -> conn.executeQuery("SELECT * FROM users"),  // 使用
    conn -> conn.close()                          // 清理(保证执行)
);

11.3 与重试的配合

java 复制代码
// 每次重试都会重新创建和清理资源
Flux<Row> resilient = Flux.using(
    () -> database.acquireConnection(),
    conn -> conn.executeQuery("SELECT * FROM events"),
    Connection::close
).retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));

十二、错误处理的完整操作符全景

12.1 分类速查表

类别 操作符 语义 是否终止流
观察 doOnError 记录日志/指标 ❌ 错误继续传播
恢复-值 onErrorReturn 返回默认值 ✅ 正常完成
恢复-流 onErrorResume 切换到备用 Publisher ✅ 由新流决定
变换 onErrorMap 包装/映射异常类型 ❌ 错误继续传播
跳过 onErrorContinue 跳过错误元素 ❌ 流继续
重试 retry / retryWhen 重新订阅源 ❌ 重新执行
清理 doFinally 终止时执行副作用 ---
资源 Flux.using try-with-resources ---
手动 handle 手动控制 next/error/skip 取决于实现

12.2 错误处理决策树

java 复制代码
发生错误了,你想怎么做?
│
├── 只想记个日志,错误继续传播?
│   └── doOnError()
│
├── 想用默认值替代?
│   └── onErrorReturn(defaultValue)
│
├── 想切换到备用数据源?
│   └── onErrorResume(e -> fallbackPublisher)
│
├── 想包装/转换异常类型?
│   └── onErrorMap(e -> new BusinessException(e))
│
├── 想重试?
│   ├── 简单重试 → retry(n)
│   └── 退避重试 → retryWhen(Retry.backoff(...))
│
├── 想跳过错误元素继续处理?
│   ├── 上游操作符支持 → onErrorContinue()(谨慎)
│   └── 更安全 → flatMap + onErrorResume(Mono.empty())
│
├── 想无论成功失败都清理资源?
│   └── doFinally(signal -> cleanup())
│
└── 想管理资源生命周期?
    └── Flux.using(create, use, cleanup)

十三、工程实战:构建多层错误治理体系

13.1 完整的错误处理管道

java 复制代码
public Mono<OrderDetail> getOrderDetail(Long orderId) {
    return orderRepository.findById(orderId)
        // 第 1 层:业务校验
        .switchIfEmpty(Mono.error(new OrderNotFoundException(orderId)))
        
        // 第 2 层:技术异常 → 业务异常
        .onErrorMap(DataAccessException.class, 
            e -> new OrderServiceException("DB error for order " + orderId, e))
        
        // 第 3 层:重试瞬态错误
        .retryWhen(Retry.backoff(2, Duration.ofMillis(500))
            .filter(e -> e instanceof TransientException)
            .doBeforeRetry(signal -> 
                log.warn("Retrying order fetch, attempt {}", signal.totalRetries() + 1)))
        
        // 第 4 层:超时保护
        .timeout(Duration.ofSeconds(5))
        
        // 第 5 层:降级到缓存
        .onErrorResume(TimeoutException.class, e -> 
            cacheService.getOrder(orderId))
        
        // 第 6 层:最终兜底
        .onErrorReturn(OrderDetail.EMPTY)
        
        // 第 7 层:审计日志
        .doOnError(e -> auditService.logFailure("getOrderDetail", orderId, e))
        
        // 第 8 层:指标记录
        .doFinally(signal -> 
            metricsService.record("order.detail", signal));
}

13.2 WebFlux Controller 中的错误处理

java 复制代码
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Order>> getOrder(@PathVariable Long id) {
        return orderService.getOrder(id)
            .map(ResponseEntity::ok)
            .onErrorReturn(OrderNotFoundException.class, 
                ResponseEntity.notFound().build())
            .onErrorReturn(OrderServiceException.class, 
                ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build());
    }
}

// 全局异常处理
@ControllerAdvice
public class GlobalErrorHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Mono<ErrorResponse> handleNotFound(OrderNotFoundException e) {
        return Mono.just(new ErrorResponse("NOT_FOUND", e.getMessage()));
    }

    @ExceptionHandler(ServiceException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Mono<ErrorResponse> handleServiceError(ServiceException e) {
        log.error("Service error", e);
        return Mono.just(new ErrorResponse("INTERNAL_ERROR", "Something went wrong"));
    }
}

13.3 WebClient 中的错误处理

java 复制代码
public Mono<ExternalData> callExternalApi(String id) {
    return webClient.get()
        .uri("/api/data/{id}", id)
        .retrieve()
        // HTTP 状态码 → 异常
        .onStatus(HttpStatusCode::is4xxClientError, response ->
            Mono.error(new ClientErrorException("Client error: " + response.statusCode())))
        .onStatus(HttpStatusCode::is5xxServerError, response ->
            Mono.error(new ServerErrorException("Server error: " + response.statusCode())))
        .bodyToMono(ExternalData.class)
        // 超时
        .timeout(Duration.ofSeconds(3))
        // 重试
        .retryWhen(Retry.backoff(2, Duration.ofMillis(200))
            .filter(e -> e instanceof ServerErrorException || e instanceof TimeoutException))
        // 降级
        .onErrorResume(e -> {
            log.warn("External API failed, using fallback", e);
            return Mono.just(ExternalData.FALLBACK);
        });
}

13.4 批量操作中的错误隔离

java 复制代码
// ❌ 一个失败导致全部终止
Flux.fromIterable(ids)
    .flatMap(id -> processItem(id))  // 任何一个 onError 都会终止整个 Flux
    .collectList();

// ✅ 错误隔离:单个失败不影响其他
Flux.fromIterable(ids)
    .flatMap(id -> processItem(id)
        .onErrorResume(e -> {
            log.warn("Failed to process item {}", id, e);
            return Mono.empty();  // 跳过失败的元素
        })
    )
    .collectList();

// ✅ 收集成功和失败的结果
Flux.fromIterable(ids)
    .flatMap(id -> processItem(id)
        .map(result -> Either.right(result))
        .onErrorReturn(e -> Either.left(e))
    )
    .collectList()
    .map(results -> {
        List<Item> successes = results.stream()
            .filter(Either::isRight).map(Either::getRight).toList();
        List<Throwable> failures = results.stream()
            .filter(Either::isLeft).map(Either::getLeft).toList();
        return new BatchResult(successes, failures);
    });

13.5 断路器模式(Circuit Breaker)

java 复制代码
// 使用 Resilience4j 与 Reactor 集成
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("externalService");

public Mono<Data> resilientCall() {
    return Mono.defer(() -> externalService.fetch())
        .transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
        .timeout(Duration.ofSeconds(3))
        .onErrorResume(CallNotPermittedException.class, e -> 
            Mono.just(Data.CIRCUIT_OPEN_FALLBACK))
        .onErrorResume(TimeoutException.class, e -> 
            Mono.just(Data.TIMEOUT_FALLBACK));
}

十四、常见陷阱与最佳实践

14.1 ❌ 在 map/flatMap 中抛出 checked 异常

java 复制代码
// ❌ 编译错误或异常被吞
flux.map(item -> {
    return objectMapper.readValue(item, MyObject.class);  // throws IOException
});

// ✅ 正确:用 try-catch 包装为 Mono.error
flux.flatMap(item -> {
    try {
        return Mono.just(objectMapper.readValue(item, MyObject.class));
    } catch (IOException e) {
        return Mono.error(new ParseException("Failed to parse", e));
    }
});

// ✅ 或者用 Mono.fromCallable
flux.flatMap(item -> Mono.fromCallable(() -> 
    objectMapper.readValue(item, MyObject.class)));

14.2 ❌ 在 flatMap 中忽略内部错误

java 复制代码
// ❌ 内部 Mono 的错误会终止外部流
flux.flatMap(item -> saveToDb(item));  // 如果 saveToDb 失败,整个 flux 终止

// ✅ 正确:隔离错误
flux.flatMap(item -> saveToDb(item)
    .onErrorResume(e -> {
        log.error("Failed to save item {}", item.getId(), e);
        return Mono.empty();
    }));

14.3 ❌ 无条件重试导致雪崩

java 复制代码
// ❌ 危险!无限重试可能压垮下游
flux.retryWhen(Retry.indefinitely());

// ✅ 正确:有限重试 + 退避 + 条件过滤
flux.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
    .maxBackoff(Duration.ofSeconds(30))
    .jitter(0.5)
    .filter(e -> e instanceof TransientException));

14.4 ❌ 错误处理顺序错误

java 复制代码
// ❌ doOnError 在 onErrorResume 之后 → 永远不会触发
flux.onErrorResume(e -> fallback())
    .doOnError(e -> log.error("Error!", e));  // 错误已被 resume 消费

// ✅ 正确:doOnError 在 onErrorResume 之前
flux.doOnError(e -> log.error("Error!", e))
    .onErrorResume(e -> fallback());

14.5 ❌ 在 subscribe 的 error handler 中抛出异常

java 复制代码
// ❌ 危险
flux.subscribe(
    data -> process(data),
    error -> { throw new RuntimeException(error); }  // 会被 Reactor 吞掉
);

// ✅ 正确:在链中处理,或记录日志
flux.doOnError(e -> log.error("Fatal", e))
    .subscribe();

14.6 ✅ 最佳实践清单

# 实践 说明
1 尽早处理错误 错误处理操作符靠近错误源
2 分层处理 技术异常 → 业务异常 → 用户友好响应
3 不要吞掉异常 至少记录日志(doOnError)
4 重试必须有上限 永远不要无限重试
5 重试必须加退避 避免瞬间重试风暴
6 超时保护每个外部调用 .timeout(Duration)
7 批量操作隔离错误 flatMap + onErrorResume(Mono.empty())
8 慎用 onErrorContinue 优先用 flatMap 方案
9 doFinally 确保资源清理 或使用 Flux.using()
10 全局异常处理兜底 @ControllerAdvice + @ExceptionHandler

十五、测试错误处理

15.1 StepVerifier 验证错误

java 复制代码
@Test
void shouldReturnDefaultOnError() {
    StepVerifier.create(
        Flux.just(1, 2, 0)
            .map(i -> 100 / i)
            .onErrorReturn(-1)
    )
    .expectNext(100)
    .expectNext(50)
    .expectNext(-1)
    .verifyComplete();
}

@Test
void shouldResumeWithFallback() {
    StepVerifier.create(
        Flux.error(new RuntimeException("primary failed"))
            .onErrorResume(e -> Flux.just("fallback-1", "fallback-2"))
    )
    .expectNext("fallback-1")
    .expectNext("fallback-2")
    .verifyComplete();
}

@Test
void shouldMapErrorType() {
    StepVerifier.create(
        Mono.error(new SQLException("db error"))
            .onErrorMap(e -> new ServiceException("wrapped", e))
    )
    .expectErrorMatches(e -> 
        e instanceof ServiceException && 
        e.getCause() instanceof SQLException)
    .verify();
}

@Test
void shouldRetryAndSucceed() {
    AtomicInteger attempts = new AtomicInteger(0);
    
    StepVerifier.create(
        Mono.fromCallable(() -> {
            if (attempts.incrementAndGet() < 3) {
                throw new TransientException("temporary failure");
            }
            return "success";
        })
        .retryWhen(Retry.fixedDelay(3, Duration.ofMillis(10)))
    )
    .expectNext("success")
    .verifyComplete();
    
    assertEquals(3, attempts.get());
}

@Test
void shouldExhaustRetries() {
    StepVerifier.create(
        Mono.error(new RuntimeException("always fails"))
            .retryWhen(Retry.backoff(2, Duration.ofMillis(10)))
    )
    .expectErrorMatches(e -> e instanceof RuntimeException)
    .verify();
}

15.2 测试 doFinally

java 复制代码
@Test
void shouldExecuteFinallyOnError() {
    AtomicBoolean finallyExecuted = new AtomicBoolean(false);
    
    StepVerifier.create(
        Flux.error(new RuntimeException("boom"))
            .doFinally(signal -> finallyExecuted.set(true))
    )
    .expectError(RuntimeException.class)
    .verify();
    
    assertTrue(finallyExecuted.get());
}

十六、错误处理与 Reactor Context

16.1 在错误处理中访问 Context

java 复制代码
Mono<User> user = userRepository.findById(id)
    .onErrorResume(e -> Mono.deferContextual(ctx -> {
        String traceId = ctx.getOrDefault("traceId", "unknown");
        log.error("[{}] Failed to load user {}", traceId, id, e);
        return Mono.just(User.FALLBACK);
    }))
    .contextWrite(Context.of("traceId", UUID.randomUUID().toString()));

16.2 错误信息中携带上下文

java 复制代码
Mono<Data> withContext = fetchData()
    .onErrorMap(e -> {
        // 包装异常,添加上下文信息
        EnrichedException enriched = new EnrichedException(
            "Operation failed at " + Instant.now(), e);
        enriched.set("userId", currentUserId);
        enriched.set("requestId", currentRequestId);
        return enriched;
    });

十七、总结

java 复制代码
┌──────────────────────────────────────────────────────────────────────────┐
│              Project Reactor 错误处理 --- 完整知识图谱                     │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  错误信号本质:                                                            │
│  • onError 是终端信号,与 onComplete 互斥                                   │
│  • 错误沿链向下传播,直到被拦截                                              │
│  • 未被处理的错误 → ErrorCallbackNotImplement                              │
│                                                                          │
│  观察类:                                                                 │
│  • doOnError(Consumer)         → 记日志,不改变流                          │
│  • doFinally(Consumer)         → 终止时清理(含 cancel)                   │
│                                                                         │
│  恢复类:                                                                │
│  • onErrorReturn(value)        → 默认值替代                               │
│  • onErrorResume(Function)     → 切换到备用 Publisher                     │
│  • onErrorContinue(BiConsumer) → 跳过错误元素(谨慎!)                     │
│                                                                         │
│  变换类:                                                                │
│  • onErrorMap(Function)        → 包装/映射异常类型                        │
│  • handle(BiConsumer)          → 手动控制 next/error/skip                │
│                                                                         │
│  重试类:                                                                │
│  • retry(n)                    → 简单重试 n 次                           │
│  • retryWhen(Retry)            → 声明式退避重试(推荐)                    │
│                                                                        │
│  资源管理类:                                                            │
│  • Flux.using(create, use, cleanup) → try-with-resources               │
│  • doFinally(signal -> cleanup)     → finally 块                       │
│                                                                        │
│  黄金法则:                                                              │
│  • 错误是数据,不是意外                                                    │
│  • 分层处理:技术异常 → 业务异常 → 用户响应                                   │
│  • 重试有上限,退避有抖动                                                   │
│  • 超时保护每个外部调用                                                     │
│  • 批量操作隔离错误                                                        │
│  • 永远不要静默吞掉异常                                                     │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

在响应式编程中,错误处理不再是事后的"补救措施",而是流处理逻辑的有机组成部分 。Reactor 提供了一套完整、正交、可组合的错误处理操作符,让你能够以声明式的方式构建出从重试到降级、从日志到熔断的多层韧性体系。

掌握这套体系,你就能在分布式系统的不确定性中,构建出真正优雅降级、自我恢复的响应式应用。

相关推荐
我命由我123451 小时前
Android 开发问题:TopAppBar 和 topAppBarColors API is experimental...
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
AC赳赳老秦1 小时前
语义采集进阶实战:利用 OpenClaw AI 语义识别自动提取网页核心信息,无需手动编写选择器
java·运维·服务器·python·信息可视化·deepseek·openclaw
AI人工智能+电脑小能手1 小时前
大白话说Java设计模式-23-桥接模式(源码剖析篇)
java·设计模式·jdbc·桥接模式·源码分析·awt·java logging
李高钢1 小时前
C# WPF Prism 进阶(二):区域(Region)与模块化(Module)
java·前端·数据库
long3161 小时前
枚举(Enums)
java·开发语言·数据库
代码方舟1 小时前
企业级对公银行 KYC 架构:基于天远人脸身份证比对A构建自动化客户尽调网关
运维·人工智能·架构·自动化
墨雨晨曦882 小时前
2026/08/15 spring AI学习总结
java·tomcat
wno7042 小时前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
聊浮游2 小时前
JAVA2026最新全套学习资料、学习路线
java·开发语言·jvm·mysql·spring·maven·idea