为什么迁移
CompletableFuture 和 Mono 都解决了「异步编程」的问题,但设计目标不同:
| 特性 | CompletableFuture | Mono/Flux |
|---|---|---|
| 数据量 | 0 或 1 个结果 | 0、1 或 N 个元素 |
| 背压 | ❌ 无 | ✅ 内置 |
| 组合能力 | thenXxx 链 | 丰富的操作符 |
| 超时/重试 | 手动实现 | 内置 |
| 线程控制 | supplyAsync | Schedulers |
| 取消 | cancel | Disposable.dispose |
迁移的收益:统一背压控制、更丰富的错误恢复、管道化的数据流处理。但不是所有场景都需要迁移。
API 映射表
创建
| CompletableFuture | Mono | 说明 |
|---|---|---|
CompletableFuture.completedFuture(v) | Mono.just(v) | 已有值 |
CompletableFuture.supplyAsync(() -> v) | Mono.fromCallable(() -> v) | 延迟计算 |
new CompletableFuture<>() | Mono.create(sink -> ...) | 手动完成 |
CompletableFuture.failedFuture(e) | Mono.error(e) | 错误 |
CompletableFuture.allOf(cf1, cf2) | Mono.when(m1, m2) | 等待全部并丢弃结果;要保留结果用 Mono.zip(...) |
CompletableFuture.anyOf(cf1, cf2) | Mono.firstWithSignal(m1, m2) | 取最先发出信号的 Mono |
变换
| CompletableFuture | Mono | 说明 |
|---|---|---|
thenApply(f) | map(f) | 同步变换 |
thenCompose(f) | flatMap(f) | 异步展开 |
thenRun(r) | thenEmpty(Mono.fromRunnable(r)) | 副作用 |
错误处理
| CompletableFuture | Mono | 说明 |
|---|---|---|
exceptionally(f) | onErrorResume(e -> Mono.just(f.apply(e))) | f 是 Function<Throwable,T>;常量默认值才用 onErrorReturn(v) |
exceptionallyCompose(f) | onErrorResume(f) | 切换备用流 |
handle((v,e) -> ...) | map + onErrorResume,或 Mono.handle(...) | 值路径与错误路径要分开写 |
超时与重试
| CompletableFuture | Mono | 说明 |
|---|---|---|
orTimeout(duration) | timeout(duration) | 超时 |
completeOnTimeout(v, duration) | timeout(duration, Mono.just(v)) | 超时默认值 |
| 无内置重试 | retry(n) | 重试 N 次 |
| 无内置退避 | retryWhen(Retry.backoff(...)) | 退避重试 |
并发控制
| CompletableFuture | Mono | 说明 |
|---|---|---|
supplyAsync(fn, executor) | subscribeOn(Schedulers.xxx()) | 指定线程 |
| 无背压 | onBackpressureBuffer() | 背压缓冲 |
迁移示例
示例 1:链式调用
CompletableFuture:
CompletableFuture<User> userFuture = fetchUser(id)
.thenCompose(user -> fetchOrders(user.getAccountId()))
.thenApply(orders -> calculateTotal(orders))
.exceptionally(e -> {
log.error("Failed", e);
return User.empty();
});
Mono:
Mono<User> userMono = fetchUser(id)
.flatMap(user -> fetchOrders(user.getAccountId()))
.map(orders -> calculateTotal(orders))
.onErrorResume(e -> {
log.error("Failed", e);
return Mono.just(User.empty());
});
示例 2:多个并行调用
CompletableFuture:
CompletableFuture<User> f1 = fetchUser(1);
CompletableFuture<User> f2 = fetchUser(2);
CompletableFuture<User> f3 = fetchUser(3);
CompletableFuture<List<User>> usersFuture = CompletableFuture.allOf(f1, f2, f3)
.thenApply(v -> List.of(f1.join(), f2.join(), f3.join()));
旧写法在 allOf 里调用一次 fetchUser,又在 thenApply 里各调用一次,等于发出了两倍的请求;allOf 只负责等待,不会把结果带进 thenApply。
Mono:
Mono<List<User>> usersMono = Mono.zip(
fetchUser(1), fetchUser(2), fetchUser(3),
(u1, u2, u3) -> List.of(u1, u2, u3)
);
示例 3:超时处理
CompletableFuture:
fetchUser(id).orTimeout(Duration.ofSeconds(5));
Mono:
fetchUser(id).timeout(Duration.ofSeconds(5));
常见陷阱
陷阱 1:阻塞 get/join
// 错误:阻塞了响应式线程
User user = userMono.block(); // 等同于 future.get()!
// 正确:在订阅中处理
userMono.subscribe(u -> process(u));
// 测试中用 StepVerifier
StepVerifier.create(userMono)
.expectNext(expectedUser)
.verifyComplete();
陷阱 2:异常类型丢失
// CompletableFuture:异常可能被 CompletionException 包装
future.exceptionally(e -> {
Throwable cause = (e instanceof CompletionException && e.getCause() != null)
? e.getCause()
: e;
return fallback(cause);
});
// Mono:onErrorResume 收到的是操作符抛出的原始异常
mono.onErrorResume(e -> Mono.just(fallback(e)));
不要简单地说「CF 一定是 CompletionException,Mono 一定不是」。CompletableFuture.failedFuture(new IOException()) 传给 exceptionally 的就是 IOException;只有经过 thenApply/thenCompose 等依赖阶段后,异常才可能被包装成 CompletionException。判断时看 instanceof,不要无脑 getCause()。
陷阱 3:背压缺失
// CompletableFuture:没有背压概念
for (int i = 0; i < 1000000; i++) {
fetchUser(i); // 100 万个请求同时发出!
}
// Mono:用 flatMap 的 concurrency 控制
Flux.range(0, 1000000)
.flatMap(i -> fetchUser(i), 100) // 最多 100 个并发
.subscribe();
陷阱 4:线程池混淆
// CompletableFuture 默认用 ForkJoinPool
CompletableFuture.supplyAsync(() -> compute());
// Mono 默认在订阅线程执行,需要显式切换
Mono.fromCallable(() -> compute())
.subscribeOn(Schedulers.boundedElastic());
不应迁移的场景
| 场景 | 原因 | 建议 |
|---|---|---|
| 单次异步调用 | CompletableFuture 更简单 | 保持 CF |
| 已有成熟 CF 代码库 | 迁移成本高 | 在边界处转换 |
| 不需要背压 | CF 足够 | 保持 CF |
| 团队不熟悉响应式 | 学习成本高 | 培训后再迁移 |
边界转换
如果需要在 CF 和 Mono 之间转换:
// CompletableFuture → Mono
Mono<String> mono = Mono.fromFuture(completableFuture);
// Mono → CompletableFuture
CompletableFuture<String> future = mono.toFuture();
// Mono → Flux
Flux<String> flux = mono.flux();
// Flux → Mono(取第一个)
Mono<String> first = flux.next();
阅读导航
上一篇:附录 C 背压规则清单
混合使用 CF 和 Mono
项目中不需要全部迁移,可以在边界处转换:
在现有 CF 代码中引入 Mono
// 现有 CF 代码
public CompletableFuture<User> getUser(String id) {
return userClient.get(id);
}
// 新代码使用 Mono,在边界处转换
public Mono<Order> getOrderWithUser(String orderId) {
return orderRepository.findById(orderId)
.flatMap(order -> Mono.fromFuture(getUser(order.getUserId()))
.map(user -> {
order.setUser(user);
return order;
}));
}
在 Mono 链中调用 CF
public Mono<PaymentResult> processPayment(PaymentRequest request) {
return validate(request)
.flatMap(valid -> Mono.fromFuture(
legacyPaymentService.charge(request) // CF
))
.timeout(Duration.ofSeconds(10))
.onErrorResume(e -> Mono.just(PaymentResult.failure(e)));
}
Spring WebFlux 中的迁移
Spring WebFlux 默认返回 Mono/Flux,从 Spring MVC(同步)迁移时:
// Spring MVC(同步)
@GetMapping("/user/{id}")
public User getUser(@PathVariable String id) {
return userService.findById(id); // 阻塞!
}
// Spring WebFlux(异步)
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id); // 非阻塞
}
数据库层迁移(R2DBC):
// Spring Data JPA(同步)
public interface UserRepository extends JpaRepository<User, String> {
User findByEmail(String email);
}
// Spring Data R2DBC(异步)
public interface UserRepository extends ReactiveCrudRepository<User, String> {
Mono<User> findByEmail(String email);
}
迁移检查清单
| 检查项 | 通过标准 |
|---|---|
无 .block() 调用 | 除测试外全部移除 |
无 .get() / .join() 调用 | 除边界转换外全部移除 |
| 数据库访问使用 R2DBC 或 reactive driver | Mongo/MariaDB/Postgres 非阻塞驱动 |
| HTTP 客户端使用 WebClient | 替代 RestTemplate |
| 错误处理使用 onErrorResume | 替代 try-catch + 返回值 |
| 线程切换使用 subscribeOn/publishOn | 替代 supplyAsync |
超时使用 .timeout() | 替代 orTimeout |
重试使用 .retryWhen() | 替代手动循环 |
性能对比:先别急着比
旧版给过「单次调用 CF 675μs、Mono 680μs」的数字,但没有说明机器、JDK、预热、测量方式和调用内容。这类数字不能作为迁移依据:一次异步调用的耗时主要由 I/O 和线程池决定,Mono 与 CompletableFuture 的封装差异通常落在噪声里。
要比较,至少固定这些条件:
| 项 | 必须记录 |
|---|---|
| JDK 与 JVM flags | 版本、堆大小、GC |
| 线程池 | 核心数、队列、拒绝策略 |
| 调用内容 | 空操作、固定延迟、真实 I/O |
| 测量点 | 从订阅到终止,还是从提交到回调 |
| 预热与轮数 | 至少多轮,报告中位数和 p99 |
| 并发度 | 1、10、100 的结论可能相反 |
更有意义的比较不是「谁快几微秒」,而是「在同样并发下谁更稳」:CompletableFuture 没有背压,Flux.flatMap 可以把并发限制在连接池容量内;Mono 的超时、重试、取消传播是内置的,CF 需要自己拼。这些是结构性差异,不依赖某次微基准。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。