You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 09:57:29