CompletableFuture异步调用未等待所有URL请求完成问题求助
根因说明
你代码偶发缺失请求结果的核心原因是使用了非线程安全的ArrayList作为结果收集容器,多个异步回调同时执行llll::add时会出现元素覆盖、丢失的问题,并非CompletableFuture.allOf没有等待请求完成。
实现方案
我们针对你的三个需求做了对应改造:
- 保留
allOf.join()逻辑保证所有请求完成后才走后续流程 - 不使用共享容器存储中间结果,避免并发写冲突,最终统一收集所有结果到List
- 支持通过键值对为请求绑定自定义标签,结果中保留状态码、响应体、异常信息等校验所需字段
完整可运行代码
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; // 自定义请求结果实体,存储所有校验需要的字段 class RequestResult { // 请求标签 private final String tag; // 响应状态码,请求异常时为-1 private final int statusCode; // 响应体,请求异常时为null private final String body; // 异常信息,请求成功时为null private final String errorMsg; public RequestResult(String tag, int statusCode, String body, String errorMsg) { this.tag = tag; this.statusCode = statusCode; this.body = body; this.errorMsg = errorMsg; } // Getter方法 public String getTag() { return tag; } public int getStatusCode() { return statusCode; } public String getBody() { return body; } public String getErrorMsg() { return errorMsg; } } class HelloWorld { // 入参改为标签-URL的键值对,支持打分类标签 private static List<RequestResult> concurrentCalls(Map<String, String> tagToUrl) { var client = HttpClient.newHttpClient(); // 每个请求对应一个返回RequestResult的CompletableFuture List<CompletableFuture<RequestResult>> futureList = tagToUrl.entrySet().stream() .map(entry -> { String tag = entry.getKey(); String url = entry.getValue(); HttpRequest request = HttpRequest.newBuilder(URI.create(url)) .GET() .timeout(Duration.ofSeconds(20)) .build(); // 异步发送请求,封装结果为RequestResult return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(response -> new RequestResult( tag, response.statusCode(), response.body(), null )) .exceptionally(e -> new RequestResult( tag, -1, null, "请求异常: " + e.getMessage() )); }) .collect(Collectors.toList()); // 等待所有请求完成(无论成功失败) CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join(); // 所有请求完成后统一收集结果到List List<RequestResult> resultList = new ArrayList<>(); for (CompletableFuture<RequestResult> future : futureList) { try { resultList.add(future.get()); } catch (Exception e) { // 异常已经在exceptionally处理过,理论上不会走到这里 resultList.add(new RequestResult("unknown", -1, null, "未知异常: " + e.getMessage())); } } return resultList; } public static void main(String[] args) { // 传入标签和URL的映射,按你的需求自定义标签即可 Map<String, String> tagToUrl = Map.of( "正常get请求", "https://postman-echo.com/get?foo1=bar1&foo2=bar2", "自定义响应头请求", "https://postman-echo.com/response-headers?foo1=bar1&foo2=bar2", "参数非法请求测试", "https://postman-echo.com/status/400" ); List<RequestResult> results = concurrentCalls(tagToUrl); // 遍历结果做断言校验示例 for (RequestResult result : results) { System.out.println("标签:" + result.getTag()); System.out.println("状态码:" + result.getStatusCode()); if (result.getErrorMsg() != null) { System.out.println("异常信息:" + result.getErrorMsg()); } else { System.out.println("响应体:" + result.getBody().substring(0, 100) + "..."); } System.out.println("---------------------------"); } } }
方案说明
- 等待逻辑:
CompletableFuture.allOf会聚合所有异步任务,调用join()会阻塞当前线程直到所有任务执行完成,保证不会提前进入结果处理流程 - 结果收集:所有任务完成后才统一遍历
futureList提取结果,完全避免了多线程并发写容器的问题,不会出现结果丢失 - 标签能力:入参使用
Map结构绑定标签和请求地址,结果中会携带对应标签,可直接通过标签区分不同类型的请求 - 校验支持:结果实体包含了标签、状态码、响应体、异常信息所有你需要的字段,后续直接调用对应Getter方法即可做断言校验
内容的提问来源于stack exchange,提问作者Ionutz Asaftei
相关产品推荐
相关产品推荐

