Project Reactor是Java响应式编程库,提供Mono和Flux核心类型,支持非阻塞、背压及异步数据流处理。它是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 规范定义了三种信号:
Publisher
├── onNext(T) → 数据信号(0..N 次)
├── onComplete() → 成功终止信号(最多 1 次)
└── onError(Throwable) → 错误终止信号(最多 1 次)
关键规则:
- onComplete 与 onError 互斥——一条流只能以其中一种方式终止
- 一旦发出 onComplete 或 onError,流的生命周期结束,不再有任何后续信号
- 错误信号会沿操作符链向下传播,直到被某个操作符拦截处理
1.2 错误传播的 Marble 图
正常流:
──1──2──3──|>
onComplete
错误流:
──1──2──X──|>
onError(e)
(3 不会被发射)
错误被捕获:
──1──2──X──fallback──|>
↑ ↑
onError 被拦截 onErrorResume 发射替代值
1.3 错误信号的不可恢复性(在流级别)
// 错误发生后,流终止。后续元素不再发射。
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 基本用法
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 无错误处理器的危险
// ⚠️ 危险:没有 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 是一个副作用操作符——它观察错误信号、执行副作用(如日志记录),但不改变错误信号本身。错误会继续向下游传播。
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 的类比
// 命令式等价物
try {
int result = 100 / i;
} catch (Exception e) {
log.error("Failed", e); // 只记录,不处理
throw e; // 继续抛出
}
3.3 关键特性
| 特性 | 说明 |
|---|---|
| 不消费错误 | 错误继续向下游传播 |
| 不改变流 | 流仍然以 onError 终止 |
| 纯副作用 | 日志、指标、告警 |
| 位置敏感 | 只能观察到其上游的错误 |
3.4 带条件的 doOnError
// 只记录特定类型的错误
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)。错误被"吞掉"。
Flux.just(1, 2, 0, 4)
.map(i -> 100 / i)
.onErrorReturn(-1) // 错误时返回 -1
.subscribe(System.out::println);
// 输出: 100, 50, -1
// 注意:4 仍然不会被处理(错误已经终止了上游流)
4.2 带条件的 onErrorReturn
// 只对特定异常类型返回默认值
flux.onErrorReturn(ArithmeticException.class, -1);
// 使用 Predicate
flux.onErrorReturn(e -> e instanceof ArithmeticException, -1);
4.3 适用场景
// 场景:查询用户,找不到时返回匿名用户
Mono<User> user = userRepository.findById(id)
.onErrorReturn(UserNotFoundException.class, User.ANONYMOUS);
// 场景:配置读取失败,使用默认配置
Mono<Config> config = configService.load()
.onErrorReturn(Config.DEFAULT);
4.4 命令式类比
// 等价于
try {
return compute(i);
} catch (ArithmeticException e) {
return -1;
}
五、onErrorResume:切换到备用流
5.1 语义
当上游发出错误信号时,切换到另一个 Publisher 继续发射数据。这是最灵活的错误恢复方式。
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
// 只对特定异常类型降级
flux.onErrorResume(TimeoutException.class, e -> fallbackService.getData());
// 使用 Predicate
flux.onErrorResume(e -> e.getCause() instanceof SQLException, e -> cacheService.getData());
5.3 经典降级模式
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 命令式类比
// 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 信号。不消费错误,只变换错误。
Mono<User> user = userRepository.findById(id)
.onErrorMap(
DataAccessException.class,
e -> new ServiceException("Failed to load user: " + id, e)
);
6.2 带条件的 onErrorMap
flux.onErrorMap(
e -> e instanceof SQLException,
e -> new RepositoryException("Database error", e)
);
6.3 工程中的典型用法:异常层次转换
// 将底层技术异常转换为业务异常
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 命令式类比
try {
return repository.save(entity);
} catch (DataAccessException e) {
throw new ServiceException("Save failed", e); // 包装后重新抛出
}
七、handle():在操作符中手动控制错误
7.1 语义
handle() 是 map() 的增强版——它允许你在转换过程中选择性地发射值、发出错误、或跳过元素。
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)
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 的对比
// 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):简单重试
// 最多重试 3 次(共执行 4 次)
Mono<Response> result = httpClient.get(url)
.retry(3);
⚠️ retry(n) 会重新订阅源 Publisher。对于 Cold Publisher,这意味着重新执行整个操作。
8.2 retry(Predicate):条件重试
// 只对特定异常重试
flux.retry(e -> e instanceof TransientException);
8.3 retryWhen(Retry):高级重试策略(推荐)
Reactor 3.3+ 引入了 reactor.util.retry.Retry 类,提供了声明式的重试策略:
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
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 退避策略时序
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 重试与资源清理
// ⚠️ 重试会重新订阅,确保资源正确释放
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 是一种特殊的错误处理模式——它不终止流,而是跳过导致错误的元素,继续处理后续元素。
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 的操作符
// ✅ 支持的操作符
map, filter, flatMap, handle, concatMap, delayElements, ...
// ❌ 不支持的操作符
onErrorResume, retry, switchOnFirst, ...
9.4 替代方案
// ✅ 更安全的替代:用 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)。
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 块
// 命令式等价物
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 资源清理模式
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——它管理一个资源的创建、使用、清理三个阶段。
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 示例:数据库连接管理
Flux<Row> results = Flux.using(
() -> database.acquireConnection(), // 创建
conn -> conn.executeQuery("SELECT * FROM users"), // 使用
conn -> conn.close() // 清理(保证执行)
);
11.3 与重试的配合
// 每次重试都会重新创建和清理资源
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 错误处理决策树
发生错误了,你想怎么做?
│
├── 只想记个日志,错误继续传播?
│ └── 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 完整的错误处理管道
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 中的错误处理
@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 中的错误处理
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 批量操作中的错误隔离
// ❌ 一个失败导致全部终止
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)
// 使用 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 异常
// ❌ 编译错误或异常被吞
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 中忽略内部错误
// ❌ 内部 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 ❌ 无条件重试导致雪崩
// ❌ 危险!无限重试可能压垮下游
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 ❌ 错误处理顺序错误
// ❌ 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 中抛出异常
// ❌ 危险
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 验证错误
@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
@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
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 错误信息中携带上下文
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;
});
十七、总结
┌──────────────────────────────────────────────────────────────────────────┐
│ 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 提供了一套完整、正交、可组合的错误处理操作符,让你能够以声明式的方式构建出从重试到降级、从日志到熔断的多层韧性体系。
掌握这套体系,你就能在分布式系统的不确定性中,构建出真正优雅降级、自我恢复的响应式应用。

1137

被折叠的 条评论
为什么被折叠?



