
在 Reactor 中,Mono 的空完成会被误判为“成功”,导致 doOnSuccess 等钩子无差别触发。本文介绍通过 onSuccess 在 flatMap 内部精准捕获真实执行时机的正确模式,并避免修改服务方法签名。
在 reactor 中,`mono
在响应式编程中,Mono<void></void> 常用于表示“仅执行副作用、无返回值”的异步操作(如保存、发送、更新状态)。但一个关键陷阱是:空的 Mono<void></void>(即未触发任何实际逻辑的空流)也会被视为“成功完成”,因此像 doOnSuccess、then() 或 doFinally 这类生命周期钩子无法区分“操作确实被执行并成功”和“因上游过滤而根本未执行”。
以原始代码为例:
.flatMap(msg -> service.loadSmth(msg)) .doOnSuccess(command -> serviceContinueWork.doSmthElse()) // ❌ 错误:即使 filter 拦截了所有元素,此处仍会触发
由于 filter 可能导致整个 Mono 为空(即 onComplete 而非 onNext + onComplete),而 Mono<void></void> 的成功完成语义等价于“完成且无错误”,doOnSuccess 便失去了业务意义上的“操作已调用”判定能力。
✅ 正确解法是将副作用逻辑内联到实际产生 Mono<void></void> 的源头内部,利用 onSuccess(Runnable) 在 service.loadSmth(msg) 真正发出完成信号时才触发后续动作:
public Mono<Void> execute(String resource) {
return Mono.just(resource)
.filter(Objects::nonNull)
.filter(msg -> msg.startsWith("http://"))
.flatMap(msg -> service.loadSmth(msg)
.onSuccess(() -> serviceContinueWork.doSmthElse())) // ✅ 仅当 loadSmth 实际执行并成功完成时触发
.doOnError(t -> log.error("service#loadSmth error: {}", ExceptionUtils.getStackTrace(t)))
.then();
}这里的关键在于:
-
onSuccess(Runnable)是Mono的实例方法,它仅在该Mono发出onComplete且此前未发出onError时执行; - 因为
service.loadSmth(msg)是在flatMap内部被订阅的,所以只有当msg通过了所有filter、flatMap创建了该Mono并完成时,onSuccess才会运行; - 若
filter导致上游无数据,flatMap就不会订阅service.loadSmth(msg),其内部的onSuccess自然不会执行——完美实现“按需触发”。
⚠️ 注意事项:
- 不要使用
doOnSuccess或doOnTerminate替代:它们作用于当前链式Mono,而非flatMap内部创建的Mono,语义层级不同; - 避免为适配响应式而强行修改服务接口(如改为
Mono<boolean></boolean>):这违背单一职责,且引入冗余状态; - 若
service.loadSmth可能返回Mono.empty()(而非因过滤未订阅),需额外确认其契约——理想情况下,loadSmth应只在执行成功后完成,不主动发empty();如有此需求,可结合switchIfEmpty显式处理。
总结:判断 Mono<void></void> 是否“真正执行”,本质是将观测点下沉至目标操作所在的 Mono 实例内部,而非停留在外层流上。onSuccess(Runnable) 提供了轻量、语义清晰、零侵入的解决方案,是 Reactor 中处理“副作用后置动作”的推荐实践。

















