如何链式调用CompletableFuture并在首次成功时终止?
串行尝试直至成功:CompletableFuture链式实现方案
你提到的场景非常典型——基于串行物理线路的设备查询,必须逐一尝试直到成功,同时要避免CompletableFuture.anyOf那种“同时启动所有任务”的资源浪费。其实用CompletableFuture的链式API就能完美解决,核心思路是仅在前一次尝试明确失败后,才启动下一次尝试,完全串行执行,成功就立即终止。
核心实现思路
我们可以利用exceptionallyCompose(JDK 9+支持)或者结合thenCompose+异常处理,把各个尝试任务串联成一条“失败则继续”的链:
- 先执行第一个尝试任务
- 如果成功,直接返回结果,后续任务不会被触发
- 如果失败,自动启动下一个尝试任务
- 以此类推,直到某个任务成功,或者所有尝试都失败
代码示例
场景1:查询失败时抛出异常(推荐)
如果你的设备查询API在失败时会抛出异常(比如Modbus通信超时、响应错误),用exceptionallyCompose是最简洁的方式:
import java.util.Arrays; import java.util.List; import java.util.function.Supplier; import java.util.concurrent.CompletableFuture; public class DeviceQueryAttempts { // 模拟你的设备查询API,失败时抛出异常 private static CompletableFuture<String> queryDevice(String deviceType) { return CompletableFuture.supplyAsync(() -> { // 模拟尝试1失败,尝试2成功 if ("type1".equals(deviceType)) { throw new RuntimeException("Query type1 failed: timeout"); } return String.format("Successfully queried device type: %s", deviceType); }); } // 递归串联所有尝试 private static CompletableFuture<String> trySequentialAttempts(List<Supplier<CompletableFuture<String>>> attempts) { if (attempts.isEmpty()) { // 所有尝试都失败,返回失败的Future return CompletableFuture.failedFuture(new IllegalStateException("All device query attempts failed")); } // 取出第一个尝试 Supplier<CompletableFuture<String>> firstAttempt = attempts.get(0); List<Supplier<CompletableFuture<String>>> remainingAttempts = attempts.subList(1, attempts.size()); // 执行第一个尝试,失败则递归执行剩余尝试 return firstAttempt.get() .exceptionallyCompose(throwable -> trySequentialAttempts(remainingAttempts)); } public static void main(String[] args) { // 定义所有尝试顺序 List<Supplier<CompletableFuture<String>>> queryAttempts = Arrays.asList( () -> queryDevice("type1"), () -> queryDevice("type2"), () -> queryDevice("type3") ); // 执行串行尝试 trySequentialAttempts(queryAttempts) .thenAccept(result -> System.out.println("Final result: " + result)) .exceptionally(throwable -> { System.err.println("All attempts failed: " + throwable.getMessage()); return null; }); } }
场景2:查询失败返回特殊标记值
如果你的API用返回值(比如null、特定字符串)表示失败,那可以用thenCompose来判断结果并决定是否继续:
private static CompletableFuture<String> trySequentialAttemptsWithResultCheck(List<Supplier<CompletableFuture<String>>> attempts) { if (attempts.isEmpty()) { return CompletableFuture.failedFuture(new IllegalStateException("All attempts failed")); } Supplier<CompletableFuture<String>> firstAttempt = attempts.get(0); List<Supplier<CompletableFuture<String>>> remaining = attempts.subList(1, attempts.size()); return firstAttempt.get() .thenCompose(result -> { if (isQuerySuccess(result)) { // 自定义成功判断逻辑 return CompletableFuture.completedFuture(result); } else { return trySequentialAttemptsWithResultCheck(remaining); } }); } // 自定义成功判断方法 private static boolean isQuerySuccess(String result) { return result != null && !result.startsWith("FAILED"); }
为什么比anyOf更好?
- 完全串行执行:严格按照顺序执行,符合物理线路的串行通信要求,不会出现并行请求冲突
- 无资源浪费:只有前一次尝试失败后,才会启动下一次,成功后立即终止,不会让后续任务白跑
- 逻辑清晰:链式结构直观体现“失败则重试”的业务逻辑,比
anyOf+取消任务的方式更简洁
注意事项
- 如果使用JDK 8,没有
exceptionallyCompose,可以用exceptionally+thenCompose替代:return firstAttempt.get() .exceptionally(throwable -> { // 这里需要返回一个占位值,然后在thenCompose里处理 return null; }) .thenCompose(result -> { if (result != null) { return CompletableFuture.completedFuture(result); } else { return trySequentialAttempts(remainingAttempts); } }); - 记得处理所有尝试失败的情况,避免Future一直处于未完成状态
内容的提问来源于stack exchange,提问作者Garret Fick
相关产品推荐
相关产品推荐

