Spring Boot重复调用接口时CompletableFuture合并响应失效问题
问题根因
代码对CompletableFuture的异步时序逻辑处理错误,属于典型的Future提前完成导致的响应时序问题:
- 第一个HTTP请求返回后,代码直接调用
resultCallback.complete(firstResponse),此时返回给Spring MVC的Future已经进入完成状态,Spring会立刻将当前firstResponse对象序列化为HTTP响应返回给客户端,此时bodySecondResponse字段还未赋值,默认是null - 后续在
secondCallback中调用resultCallback.thenApply()追加的字段合并逻辑,只是在已经完成的Future上追加了一个无下游依赖的异步阶段:虽然代码确实给对象set了bodySecondResponse值,控制台也能打印出非null的结果,但这个修改动作发生在HTTP响应已经返回给客户端之后,用户无法收到 - 首次请求偶尔能返回完整值,完全是网络延迟带来的时序巧合:首次建连网络开销大,第二个请求返回、字段赋值的动作刚好赶在Spring序列化响应之前完成;后续请求连接复用后第一个请求返回速度变快,就必然出现
bodySecondResponse为null的问题。
修正思路
核心原则:返回给Controller的最终Future,必须等两个HTTP请求全部返回、字段合并完成后再标记为完成,具体调整点:
- 用两个独立的CompletableFuture分别承载第一个、第二个HTTP请求的返回结果,不要提前完成最终对外返回的Future
- 两个HTTP请求可以并行发起,不需要等第一个请求返回后再发第二个,减少接口整体耗时
- 等两个请求全部成功返回后,再执行字段合并逻辑,合并完成后再complete最终返回的Future
- 任意一个请求失败时,提前将最终Future标记为异常完成,返回错误信息
修正后代码
Controller层
原有Controller代码不需要调整,保持原样即可:
@RestController public class Controller { @Autowired Service service; @GetMapping("/comb") public CompletableFuture<CombinationBothResponses> combine() { CompletableFuture<CombinationBothResponses> resultCallback = new CompletableFuture<>(); service.sendTwoRequests(resultCallback); return resultCallback; } }
Service层
调整请求发起和合并逻辑,两个请求并行发送,等全部返回后再合并:
@org.springframework.stereotype.Service public class Service { private final Jsonb jsonb = JsonbBuilder.create(); private final OkHttpClient client = new OkHttpClient(); public void sendTwoRequests(CompletableFuture<CombinationBothResponses> resultCallback) { // 两个独立Future分别承载两个接口的返回结果 CompletableFuture<CombinationBothResponses> firstRespFuture = new CompletableFuture<>(); CompletableFuture<CombinationBothResponses> secondRespFuture = new CompletableFuture<>(); // 并行发起两个HTTP请求 Request firstRequest = new Request.Builder() .url("https://jsonplaceholder.typicode.com/posts/1") .build(); client.newCall(firstRequest).enqueue(new firstCallback(firstRespFuture)); Request secondRequest = new Request.Builder() .url("https://jsonplaceholder.typicode.com/posts/2") .build(); client.newCall(secondRequest).enqueue(new secondCallback(secondRespFuture)); // 两个请求全部成功后合并结果,再完成最终返回的Future firstRespFuture.thenAcceptBoth(secondRespFuture, (firstResp, secondResp) -> { firstResp.setBodySecondResponse(secondResp.getBody()); resultCallback.complete(firstResp); }).exceptionally(e -> { // 任意一个请求失败,直接向上抛出异常 resultCallback.completeExceptionally(e); return null; }); } }
第一个请求回调
仅负责完成第一个请求对应的Future,不触碰最终返回结果:
public class firstCallback implements Callback { private final Jsonb jsonb = JsonbBuilder.create(); private final CompletableFuture<CombinationBothResponses> firstRespFuture; public firstCallback(CompletableFuture<CombinationBothResponses> firstRespFuture) { this.firstRespFuture = firstRespFuture; } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { if (response.isSuccessful()) { CombinationBothResponses firstResponse = jsonb.fromJson(response.body().string(), CombinationBothResponses.class); firstRespFuture.complete(firstResponse); } else { firstRespFuture.completeExceptionally(new RuntimeException("First request failed, status: " + response.code())); } } @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { firstRespFuture.completeExceptionally(e); } }
第二个请求回调
仅负责完成第二个请求对应的Future,不直接修改最终结果:
public class secondCallback implements Callback { private final Jsonb jsonb = JsonbBuilder.create(); private final CompletableFuture<CombinationBothResponses> secondRespFuture; public secondCallback(CompletableFuture<CombinationBothResponses> secondRespFuture) { this.secondRespFuture = secondRespFuture; } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { if (response.isSuccessful()) { CombinationBothResponses secondResponse = jsonb.fromJson(response.body().string(), CombinationBothResponses.class); secondRespFuture.complete(secondResponse); } else { secondRespFuture.completeExceptionally(new RuntimeException("Second request failed, status: " + response.code())); } } @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { secondRespFuture.completeExceptionally(e); } }
实体类
原有CombinationBothResponses实体类不需要调整,保持原样即可。
优化说明:修正后两个HTTP请求从原来的串行执行改为并行执行,接口整体响应耗时会降低近一半,同时彻底解决了时序不确定导致的字段为null问题,每次请求都能稳定返回完整合并结果。
内容的提问来源于stack exchange,提问作者Anne Maier
相关产品推荐
相关产品推荐

