
本文详解如何在 spring webflux 中并行调用两个下游服务,并在任一调用失败时仍能组合部分成功结果,避免 mono.zip 的“全有或全无”语义,通过 optional 包装 + onerrorreturn 实现健壮的响应组装。
本文详解如何在 spring webflux 中并行调用两个下游服务,并在任一调用失败时仍能组合部分成功结果,避免 mono.zip 的“全有或全无”语义,通过 optional 包装 + onerrorreturn 实现健壮的响应组装。
在响应式编程中,Mono.zip() 是合并多个 Mono 的常用操作符。但其默认行为是:任一源发出错误(error)或空完成(empty),整个 zip 就立即终止,并传播该错误或直接完成——这与开发者期望的“尽力而为、部分可用”逻辑相悖。尤其在集成多个非关键下游服务时(如用户画像 + 推荐缓存),我们希望即使一个服务超时或返回 5xx,另一个的成功结果仍能被保留并纳入最终响应。
核心问题在于:Reactor 禁止 null 值,因此无法像 RxJava 1 中用 Single.just(null) 那样优雅地表示“缺失值”。直接使用 onErrorResume(e -> Mono.empty()) 后再 zip,会导致整个 zip 因 empty 而提前终止(Mono.zip 将 empty 视为完成信号,而非占位符)。
✅ 正确解法:用 Optional
✅ 推荐实现方案
首先,改造服务层方法,使其返回 Mono
class FirstService {
private final WebClient webClient;
Mono<Optional<FirstResponse>> get() {
return webClient.get()
.uri("https://api.example.com/first")
.retrieve()
.bodyToMono(FirstResponse.class)
.map(Optional::of) // 成功 → Optional.of(response)
.onErrorReturn(Optional.empty()); // 失败 → Optional.empty()
}
}
class SecondService {
private final WebClient webClient;
Mono<Optional<SecondResponse>> get() {
return webClient.get()
.uri("https://api.example.com/second")
.retrieve()
.bodyToMono(SecondResponse.class)
.map(Optional::of)
.onErrorReturn(Optional.empty());
}
}接着,在控制器中使用 Mono.zip 组合,并安全构造 CombinedResponse:
@RestController
@RequestMapping("/api")
class CombinationController {
private final FirstService firstService;
private final SecondService secondService;
@GetMapping("/combined")
Mono<CombinedResponse> getCombined() {
return Mono.zip(
firstService.get(),
secondService.get()
)
.map(tuple -> {
Optional<FirstResponse> firstOpt = tuple.getT1();
Optional<SecondResponse> secondOpt = tuple.getT2();
return new CombinedResponse(
firstOpt.orElse(null), // 允许 null 字段 —— CombinedResponse 设计上支持
secondOpt.orElse(null)
);
});
}
}对应的 DTO 应明确允许字段为 null(符合语义):
public class CombinedResponse {
private final FirstResponse first;
private final SecondResponse second;
public CombinedResponse(FirstResponse first, SecondResponse second) {
this.first = first;
this.second = second;
}
// getters...
}⚠️ 关键注意事项
- 不要滥用 Mono.empty():Mono.zip(a, b) 中若 a 发出 empty,zip 会立即完成且不等待 b,导致 b 被取消 —— 这正是原始问题的根源。
- Optional 是语义桥梁,不是性能负担:它轻量、不可变、线程安全,且明确表达了“值可能存在”的契约,比自定义空对象(如 FirstResponse.EMPTY)更清晰、更通用。
- 错误处理粒度可控:onErrorReturn(Optional.empty()) 可替换为 onErrorResume(e -> { log.warn("First service failed", e); return Mono.just(Optional.empty()); }),便于监控与告警。
- JSON 序列化兼容性:Jackson 默认序列化 null 字段为 null(需确保 CombinedResponse 字段未加 @JsonInclude(JsonInclude.Include.NON_NULL)),前端可据此判断数据可用性。
✅ 总结
当需要 WebFlux 中“容忍性合并”多个异步调用结果时,请放弃 onErrorResume(... -> Mono.empty()) + zip 的错误组合,转而采用:
-
服务层返回 Mono
> ,用 map(Optional::of) 和 onErrorReturn(Optional.empty()) 统一建模; - 编排层用 Mono.zip(a, b) + map 解包,安全构造部分填充的聚合对象;
- DTO 显式支持 null 字段,使 API 语义自解释。
这一模式兼顾了响应式流的严谨性与业务场景的容错需求,是迁移自 RxJava 并追求稳定性的团队的理想实践。









