如何在CompletableFuture调用链中统计总耗时及各API单独耗时?
CompletableFuture链式调用+多维度耗时统计实现方案
针对你的需求——构建带短路逻辑的异步API调用链,同时统计每个API的单独耗时和总耗时,我们可以通过封装耗时统计逻辑+保留链式短路判断来实现,具体代码和说明如下:
核心思路
- 用自定义载体类统一存储API的名称、返回结果(含空值处理)和耗时,方便统计数据管理
- 封装通用的API调用包装方法,自动处理异步执行和耗时计算,避免重复代码
- 在链式调用中收集每个执行过的API的统计数据,同时保留"前一个结果非空则跳过后续API"的短路逻辑
- 在调用链收尾阶段统计总耗时并输出所有统计结果
完整实现代码
import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Supplier; // 存储API调用结果与耗时的载体类 class ResultWithTime { private final String apiName; private final Optional<String> result; private final long elapsedMillis; public ResultWithTime(String apiName, Optional<String> result, long elapsedMillis) { this.apiName = apiName; this.result = result; this.elapsedMillis = elapsedMillis; } // Getter方法 public String getApiName() { return apiName; } public Optional<String> getResult() { return result; } public long getElapsedMillis() { return elapsedMillis; } } public class CompletableFutureApiCallsExample { public static void main(String[] args) throws ExecutionException, InterruptedException { // 收集所有执行过的API的耗时统计 List<ResultWithTime> apiTimeStats = new ArrayList<>(); // 记录调用链总开始时间 long totalStartTime = System.currentTimeMillis(); var callChain = trackApiCall("API调用1", CompletableFutureApiCallsExample::apiCall1) .thenCompose(result1 -> { apiTimeStats.add(result1); // 前一个结果为空则执行下一个API,否则返回当前结果 if (result1.getResult().isEmpty()) { return trackApiCall("API调用2", CompletableFutureApiCallsExample::apiCall2); } else { return CompletableFuture.completedFuture(result1); } }) .thenCompose(result2 -> { // 避免重复添加同一结果(短路时result2等于上一个result) if (!apiTimeStats.contains(result2)) { apiTimeStats.add(result2); } if (result2.getResult().isEmpty()) { return trackApiCall("API调用3", CompletableFutureApiCallsExample::apiCall3); } else { return CompletableFuture.completedFuture(result2); } }) .thenApply(finalResult -> { // 补充最后一个未被添加的结果 if (!apiTimeStats.contains(finalResult)) { apiTimeStats.add(finalResult); } return finalMapping(finalResult.getResult().orElse(null)); }) .whenComplete((finalOutput, throwable) -> { // 计算并输出总耗时 long totalEndTime = System.currentTimeMillis(); long totalElapsed = totalEndTime - totalStartTime; System.out.println("\n===== 耗时统计 ====="); apiTimeStats.forEach(stat -> System.out.printf("%s: %d 毫秒%n", stat.getApiName(), stat.getElapsedMillis()) ); System.out.printf("调用链总耗时: %d 毫秒%n", totalElapsed); }); System.out.println("最终输出结果: " + callChain.get()); } // 通用API调用包装器:处理异步执行+耗时统计 private static CompletableFuture<ResultWithTime> trackApiCall(String apiName, Supplier<String> apiSupplier) { return CompletableFuture.supplyAsync(() -> { long startTime = System.currentTimeMillis(); String apiResult = apiSupplier.get(); long endTime = System.currentTimeMillis(); // 用Optional统一处理空结果,避免空指针 return new ResultWithTime(apiName, Optional.ofNullable(apiResult), endTime - startTime); }); } // 原API业务方法(仅保留业务逻辑,异步和统计交给包装器) private static String apiCall1() { // 模拟接口耗时 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "API调用1返回结果"; // 改为return null可触发后续API调用 } private static String apiCall2() { try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "API调用2返回结果"; } private static String apiCall3() { try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "API调用3返回结果"; } private static String finalMapping(String result) { return "最终处理结果: " + result; } }
关键细节说明
- ResultWithTime类:用Optional统一处理空结果,避免空指针异常,同时绑定API名称和耗时,让统计数据更直观。
- trackApiCall方法:复用性极强,所有API都通过这个方法执行,无需在每个API方法中重复编写异步和耗时统计逻辑。
- 短路逻辑保持:每个
thenCompose阶段都会判断当前结果是否为空,为空才会触发下一个API调用,完全符合需求。 - 耗时收集去重:因为短路时会直接返回上一个结果,所以需要判断是否已添加到统计列表,避免重复统计同一个API的耗时。
- 总耗时统计:在调用链启动前记录总开始时间,在
whenComplete回调中计算总耗时,即使调用链出现异常也能正常输出总耗时。
测试场景验证
- 如果
apiCall1返回非空值:只会执行apiCall1,统计列表中只有apiCall1的耗时,总耗时约等于apiCall1的耗时。 - 如果
apiCall1返回null,apiCall2返回非空值:会依次执行apiCall1、apiCall2,统计这两个的耗时,总耗时约为两者之和。
内容的提问来源于stack exchange,提问作者Stranger95
相关产品推荐
相关产品推荐

