这是基于工程实际用法与 Reactor 核心逻辑整合的响应式流(Reactor)最佳实践指南。
核心原则:数据在流动,算子在加工
在响应式编程中,不要把它看作方法的调用,而要看作数据流经一条流水线,每个算子都是流水线上的一个加工站。
1. 核心转换算子:处理"数据本身"
| 算子 | 最佳实践场景 | 关键点 |
|---|---|---|
map |
同步对象转换、字段提取、简单计算。 | 必须是纯内存操作,严禁 IO 或阻塞。 |
flatMap |
异步操作串联(查库、HTTP、调用其他 Mono)。 | "拆包"操作,将嵌套的 Mono<Mono<T>> 压平为 Mono<T>。 |
cast |
类型强转。 | 比 map 强转更安全,类型不符时自动抛出异常中断。 |
defaultIfEmpty |
流为空时提供默认值。 | 类似 SQL 的 NVL,用于处理可选配置缺失。 |
2. 组合与并行:处理"多路数据汇合"
| 算子 | 最佳实践场景 | 关键点 |
|---|---|---|
zipWith / zipWhen |
并行获取多个异步结果并组合。 | 避免嵌套 flatMap,常用于同时查主表和关联表。 |
collectList |
将 Flux<T> 聚合成 Mono<List<T>>。 |
需要一次性拿到所有结果(如批量校验、列表返回)时使用。 |
3. 副作用与观察:处理"非数据逻辑"
| 算子 | 最佳实践场景 | 关键点 |
|---|---|---|
doOnNext |
打印日志、调试、给可变对象设值(Fill)。 | 不改变流中的数据,只"偷看"一眼。 |
doOnError |
记录异常日志、触发告警。 | 异常依然会向下传递,除非配合 onErrorResume。 |
doOnSuccess |
操作成功后的通知。 | 常用于 Mono<Void> 场景,如保存成功后发通知。 |
4. 流程控制:处理"分支与过滤"
| 算子 | 最佳实践场景 | 关键点 |
|---|---|---|
filter |
根据条件丢弃数据。 | 条件不满足时,流会变成"空流"(Empty)。 |
switchIfEmpty |
兜底逻辑。上游无数据时切换到另一个 Mono。 | 常用于"查不到就抛异常"或"查不到就返回默认对象"。 |
take(1) / next() |
只要第一个结果。 | 类似 SQL 的 LIMIT 1,减少不必要的计算和传输。 |
5. 错误处理:响应式的"Try-Catch"
onErrorResume(e -> ...): 捕获并恢复。比如查库失败,返回一个空对象或降级方案,让流程继续。onErrorMap(e -> ...): 异常转换 。把底层技术异常(如SQLException)转换成业务异常(BusinessException)。retry(n): 重试。网络抖动或临时性故障时使用。
6. 终结算子:处理"结束与返回"
then(): 忽略前面的数据,等前面执行完后返回Mono<Void>。常用于保存操作后直接结束。thenReturn(value): 忽略前面的数据,等前面执行完后返回一个指定值。常用于保存后返回 ID。thenMany(Flux): 将一个 Mono 转换为 Flux,常用于"先执行初始化,再返回流"的场景。
💡 避坑指南与高频疑问
- 什么时候用
just?- 当你手里有一个普通对象,想把它塞进响应式流开始时,用
Mono.just(obj)。
- 当你手里有一个普通对象,想把它塞进响应式流开始时,用
- 为什么有时候要用
defer?- 如果你希望每次订阅时才执行创建逻辑(如获取当前时间、生成随机数),用
Mono.defer(() -> Mono.just(...))。
- 如果你希望每次订阅时才执行创建逻辑(如获取当前时间、生成随机数),用
- 遇到
Mono<Void>怎么办?Void表示"只关心完成,不关心结果"。如果你想接着干别的事,用.then(Mono.newTask())衔接。
- 调试技巧:
- 在链式调用中插入
.log(),它会把流的生命周期(订阅、请求、onNext、onComplete)全部打印到控制台,是排查问题的神器。
- 在链式调用中插入
🚀 终极口诀(建议背诵)
同步转换用
map,异步交互flatMap。
并行组合zipWith,类型强转用cast。
偷看设值doOnNext,判空兜底switchIfEmpty。
可选缺省defaultIfEmpty,异常降级onErrorResume。
只要结果用then,收集列表collectList。
调试跟踪加个log,遇到Void用then接。
🛠️ 典型业务组合拳示例
- 查不到就报错 :
.findById(id).switchIfEmpty(Mono.error(new BusinessException("不存在"))) - 保存后返回 ID :
.save(entity).thenReturn(entity.getId()) - 并行查两个表并合并 :
.zipWhen(user -> roleService.findByUserId(user.getId())) - 补全默认值并保存 :
.doOnNext(req -> { if(req.getStatus()==null) req.setStatus(0); }).flatMap(this::save)