Spring MVC结合WebClient时ExchangeFilterFunction缓存ThreadLocal值问题排查
故障原因
- 线程不匹配问题:WebClient是响应式异步组件,默认使用Reactor内部预初始化的线程池执行请求链路,
ExchangeFilterFunction的执行线程并不是处理用户请求的Tomcat线程。你使用的InheritableThreadLocal仅会在子线程创建时继承父线程的上下文值,而Reactor线程池的线程是提前创建好并长期复用的,第一次请求触发某条Reactor线程创建时继承了当时的JWT值,后续该线程复用的时候,上下文不会再更新,就会一直返回第一次的JWT。 - 初始化逻辑缺陷:
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官方提供的上下文传播能力解决:
- 引入
micrometer-context-propagation依赖(Spring Boot 3+自带,低版本需单独引入) - 改造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
相关产品推荐
相关产品推荐

