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

使用CompletableFuture时Security Context为null的异常排查

问题分析与解决方案

问题根源

你的核心问题是SecurityContext没有正确传播到CompletableFuture的异步任务中,即便使用了DelegatingSecurityContextAsyncTaskExecutor,仍存在两个关键诱因:

  1. DelegatingSecurityContextAsyncTaskExecutor仅包装直接提交给它的任务,但thenCombineAsync执行时,当前请求的SecurityContext未被正确捕获并绑定到异步线程;
  2. 线程池的线程复用机制会导致:前一个任务执行后若未清理SecurityContext,后续任务可能拿到错误上下文;若上下文被清理,则会返回null。你遇到的"第6次调用异常"正好对应核心线程池(corePoolSize=6)用完开始复用线程的节点,验证了这一点。

解决方法

方法1:手动捕获并传递SecurityContext

在控制器中先捕获当前请求的SecurityContext,在异步任务中手动绑定,执行完毕后清理上下文避免污染线程池:

@RequestMapping(value = {"/randomizer"}, method = RequestMethod.POST)
public CompletableFuture<String> randomizer(){
    // 捕获当前请求的SecurityContext
    SecurityContext currentContext = SecurityContextHolder.getContext();

    CompletableFuture<String> someString = stringService
            .findRandomByInput("123")
            .thenCombine(stringService.findAnotherRandomByInput("321"), (result1, result2) -> {
                return applyRandom(result1, result2);
            });

    CompletableFuture<Void> computation = computingService.computeRandomByInput(RandomDto.empty(), "123");

    return someString.thenCombineAsync(computation, (result1, result2) -> {
        // 手动绑定上下文
        SecurityContextHolder.setContext(currentContext);
        try {
            combineIt(result1, result2, getCurrentApplicationUser());
            // 根据实际逻辑返回对应结果
            return "combined_result";
        } finally {
            // 执行完成后清理上下文,防止线程池复用引发问题
            SecurityContextHolder.clearContext();
        }
    }, taskExecutor);
}

方法2:使用Spring @Async注解(更规范)

调整服务层代码,用Spring的@Async注解实现异步逻辑,此时DelegatingSecurityContextAsyncTaskExecutor会自动帮你传播SecurityContext:

  1. 服务层方法添加@Async标注:
@Service
public class StringService {
    @Async("taskExecutor")
    public CompletableFuture<String> findRandomByInput(String input) {
        // 原有业务逻辑
        return CompletableFuture.completedFuture(yourResult);
    }

    @Async("taskExecutor")
    public CompletableFuture<String> findAnotherRandomByInput(String input) {
        // 原有业务逻辑
        return CompletableFuture.completedFuture(yourResult);
    }
}

@Service
public class ComputingService {
    @Async("taskExecutor")
    public CompletableFuture<Void> computeRandomByInput(RandomDto dto, String input) {
        // 原有业务逻辑
        return CompletableFuture.completedFuture(null);
    }
}
  1. 控制器方法简化:
@RequestMapping(value = {"/randomizer"}, method = RequestMethod.POST)
public CompletableFuture<String> randomizer(){
    CompletableFuture<String> someString = stringService
            .findRandomByInput("123")
            .thenCombine(stringService.findAnotherRandomByInput("321"), this::applyRandom);

    CompletableFuture<Void> computation = computingService.computeRandomByInput(RandomDto.empty(), "123");

    return someString.thenCombine(computation, (result1, result2) -> {
        // SecurityContext已自动传播,可直接调用getCurrentApplicationUser()
        combineIt(result1, result2, getCurrentApplicationUser());
        return "combined_result";
    });
}

额外注意事项

  • 确保getCurrentApplicationUser()方法每次都从SecurityContextHolder实时获取认证信息,不要缓存Authentication对象;
  • 异步任务中处理完用户相关逻辑后,务必及时清理SecurityContext,避免线程复用引发上下文污染;
  • 若使用线程池,建议监控线程复用情况,避免因长时间持有上下文导致的内存泄漏或数据错误。

内容的提问来源于stack exchange,提问作者Thomas Lang

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 18:15:16