登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java CompletableFuture.exceptionallyCompose 如何串联异步补偿:异常恢复、线程切换与超时边界

来源:17golang原创

时间:2026-08-30 04:55:00 233浏览 收藏

订单报价服务通常先查主库存,再在主服务失败时切到备用报价源。备用调用本身也是异步的,这时 exceptionally 只能返回一个已经算好的值,而 exceptionallyCompose 可以把异常映射成新的 CompletionStage,继续等待备用调用完成。

需要“失败后再发起一次异步调用”时选 exceptionallyCompose;只需要把异常改成一个现成结果时才用 exceptionally。超时发生后仍要区分备用调用成功和备用调用也失败的两条路径。

实践要点:
  • 恢复函数接收原始异常,并返回同类型的异步阶段。
  • 需要明确线程池时使用 exceptionallyComposeAsync
  • 给主链设置 orTimeout,同时验收备用成功和备用失败。

报价查询的两个异步结果

下面的示例把主报价和备用报价都写成 CompletionStage。主调用用 failedFuture 模拟库存服务不可用,备用调用返回一个成功阶段,便于直接观察阶段如何衔接。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

static CompletionStage primaryQuote(String sku) {
    return CompletableFuture.failedFuture(
        new IllegalStateException("primary quote unavailable: " + sku));
}

static CompletionStage fallbackQuote(String sku) {
    return CompletableFuture.completedFuture("fallback-price-199");
}

这里的重点不是模拟网络,而是让类型保持一致:主阶段和备用阶段都产出字符串,恢复函数才能自然地返回新的 CompletionStage

异常发生后如何接上备用调用

把主阶段接到 exceptionallyCompose,函数参数就是主阶段携带的 Throwable。函数体先记录异常,再返回 fallbackQuote(sku);最终阶段会等待备用报价结束。

Java CompletableFuture 主报价异常后进入 exceptionallyCompose,再连接备用报价的调用链示意图

CompletionStage quote = primaryQuote("SKU-42")
    .exceptionallyCompose(error -> {
        System.out.println("primary failed: " + error.getMessage());
        return fallbackQuote("SKU-42");
    });

System.out.println(quote.toCompletableFuture().join());
// fallback-price-199

这条链上的真实节点是“主报价失败”“exceptionallyCompose”“备用报价”“fallback-price-199”。如果主阶段正常完成,恢复函数不会执行,结果仍来自主报价。

别把异步补偿写成普通值兜底

exceptionally 的函数返回的是 String,适合固定降级值;它不能直接表达“现在再调用备用服务”。硬把异步调用塞进普通值函数,会得到嵌套类型或过早阻塞。

CompletionStage fixed = primaryQuote("SKU-42")
    .exceptionally(error -> "local-cache-price");

CompletionStage asyncFallback = primaryQuote("SKU-42")
    .exceptionallyCompose(error -> fallbackQuote("SKU-42"));

前者直接结束为本地缓存字符串,后者还要等待备用阶段。恢复逻辑应该先问一句:备用结果是否需要网络、磁盘或其他异步资源?需要就保留阶段,不需要才返回常量。

超时之后的线程与失败边界

orTimeout 会让主阶段以 TimeoutException 异常完成,之后可以继续进入 exceptionallyComposeAsync。指定 Executor 能把备用调用放入报价专用线程池,避免把恢复工作混进默认执行器。

Java CompletableFuture 超时后以 TimeoutException 进入 exceptionallyComposeAsync,再分出备用成功与备用失败状态

ExecutorService quotePool = Executors.newFixedThreadPool(4);
CompletionStage result = primaryQuote("SKU-42")
    .toCompletableFuture()
    .orTimeout(300, TimeUnit.MILLISECONDS)
    .exceptionallyComposeAsync(error -> {
        System.out.println("fallback because: " + error.getClass().getSimpleName());
        return fallbackQuote("SKU-42");
    }, quotePool)
    .whenComplete((value, error) -> quotePool.shutdown());

检查时要看两件事:主服务慢到超过 300 毫秒时,日志应出现 TimeoutException;备用服务也失败时,最终阶段仍然异常完成,不能被误报成报价成功。

三个容易漏掉的验收点

恢复函数不要吞掉原始异常

日志至少保留异常类型和商品编号。只打印“降级成功”会让超时、连接失败和业务拒绝混为一谈。

不要在恢复函数里调用 join

恢复函数已经处在完成阶段里,再 join 另一个异步任务会把非阻塞链变成阻塞点,线程池较小时尤其容易拖慢后续任务。

验证备用失败仍能冒泡

测试主阶段失败、备用成功;主阶段超时、备用成功;主阶段失败、备用也失败三种组合。最后一种应由调用方统一记录失败,不应伪造默认价格。

什么时候该换成其他方法

如果只需把异常转换成固定字符串,用 exceptionally;如果既要观察成功值又要观察异常,用 handle;如果恢复调用必须在线程池中异步启动,用 exceptionallyComposeAsync。这些方法的差别在返回类型和调度时机,不在“哪个更高级”。

相关问题

exceptionallyCompose 会在主阶段成功时执行吗?

不会。主阶段正常完成时,恢复函数不执行,后续阶段直接沿用主结果。

备用阶段失败后还能再次补偿吗?

可以在备用阶段之后再接一层异常处理,但每一层都应明确最后的失败语义,避免无限重试。

为什么要优先使用同一个 CompletionStage 类型?

同类型可以让主调用、备用调用和最终结果保持一致,调用方无需处理嵌套阶段或额外的类型转换。

把“固定降级值”和“再次发起异步调用”分开,exceptionallyCompose 的边界就清楚了:它负责接续阶段,不负责替你决定重试次数、线程池容量或业务上的兜底价格。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>