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

Spring MVC结合WebClient时ExchangeFilterFunction缓存ThreadLocal值问题排查

故障原因
  1. 线程不匹配问题:WebClient是响应式异步组件,默认使用Reactor内部预初始化的线程池执行请求链路,ExchangeFilterFunction的执行线程并不是处理用户请求的Tomcat线程。你使用的InheritableThreadLocal仅会在子线程创建时继承父线程的上下文值,而Reactor线程池的线程是提前创建好并长期复用的,第一次请求触发某条Reactor线程创建时继承了当时的JWT值,后续该线程复用的时候,上下文不会再更新,就会一直返回第一次的JWT。
  2. 初始化逻辑缺陷:RequestContextService的init方法做了非空判断才写入上下文,当线程复用导致ThreadLocal里已有旧值时,新的请求上下文直接被丢弃,进一步加剧了旧值复用的问题。
解决方案

第一步:修复上下文初始化逻辑

首先去掉RequestContextService.init方法的非空判断,每次请求直接覆盖上下文,避免旧值阻塞新上下文写入:

public void init(RequestContext requestContext) {
    CONTEXT.set(requestContext);
}

如果担心重复初始化的问题,可以把判断逻辑调整为打警告日志后直接覆盖,不要直接丢弃新的上下文。

第二步:解决异步线程上下文透传问题

根据你的项目技术栈可选以下两种方案:

方案一:绕开Filter读取ThreadLocal(最稳妥,适配MVC场景)

封装WebClient调用工具类,在Tomcat请求线程中提前读取ThreadLocal中的JWT,构造请求时直接传入Header,不需要依赖ExchangeFilterFunction读取上下文,从根源避免线程切换导致的上下文丢失/脏读问题。

// 工具类示例
public class WebClientUtils {
    private final WebClient webClient;
    private final RequestContextService requestContextService;
    
    public <T> T get(String url, Class<T> respType) {
        String jwt = requestContextService.get().getJwt();
        if (StringUtils.isBlank(jwt)) {
            throw new RuntimeException("未授权");
        }
        return webClient.get()
                .uri(url)
                .header(SAML_HEADER_NAME, jwt)
                .retrieve()
                .bodyToMono(respType)
                // MVC场景同步等待结果
                .block();
    }
}

方案二:改造ExchangeFilterFunction适配异步上下文传播

如果必须保留ExchangeFilterFunction的统一处理逻辑,可使用Spring官方提供的上下文传播能力解决:

  1. 引入micrometer-context-propagation依赖(Spring Boot 3+自带,低版本需单独引入)
  2. 改造ExchangeFilterFunction,使用Mono.defer结合ContextSnapshot捕获并透传ThreadLocal上下文:
@Override
public @NotNull Mono<ClientResponse> filter(@NotNull ClientRequest request, @NotNull ExchangeFunction next) {
    return Mono.defer(() -> {
        var jwt = requestContextService.get().getJwt();
        if (StringUtils.isNoneBlank(jwt)) {
            var clientRequest = ClientRequest.from(request)
                    .headers(httpHeaders -> httpHeaders.set(SAML_HEADER_NAME, jwt))
                    .build();
            return next.exchange(clientRequest);
        }
        return Mono.just(UNAUTHORIZED_CLIENT_RESPONSE);
    }).contextWrite(context -> ContextSnapshot.captureAll().updateContext(context));
}

可选优化:替换ThreadLocal实现

如果不需要子线程继承上下文,可直接把InheritableThreadLocal替换为普通ThreadLocal,避免异步线程池的线程继承到旧的上下文值,减少脏数据风险。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 00:09:03