ThreadLocal中存Token需在Filter的doFilter前set、finally中remove,避免线程复用污染;不可用static final直接声明含Spring依赖的ThreadLocal;异步调用须手动透传关键字段;UserContext应不可变或防御性拷贝。

Filter里怎么安全地把Token塞进ThreadLocal
ThreadLocal不是自动跨线程传递的,Filter中解析出的Token必须显式存进去,否则Service层拿不到。常见错误是只在doFilter里set却忘了remove,导致线程复用时污染上下文。
- 必须在
doFilter调用前set,且确保只在当前请求线程执行(不要在异步线程里操作) - 务必在
finally块中调用remove(),否则Tomcat线程池复用时会残留上一个请求的用户信息 - Token解析失败时也要
remove(),避免null或非法值被下游误用 - 示例:
try { String token = extractToken(request); if (token != null) { UserContext.setUserId(parseUserId(token)); } chain.doFilter(request, response); } finally { UserContext.remove(); // 关键! }
ThreadLocal变量为什么不能直接static final
看似合理,但static final ThreadLocal<UserContext>会导致所有线程共享同一个ThreadLocal实例——这本身没错,但问题出在泛型擦除和初始化时机上:如果UserContext含非静态字段或依赖Spring Bean,static final会导致类加载时提前初始化,破坏Spring代理和AOP。
- 正确写法是
private static final ThreadLocal<UserContext> contextHolder = ThreadLocal.withInitial(UserContext::new); - 避免在
withInitial里调用Spring容器方法(如ApplicationContext.getBean()),会引发EarlySingletonAccessException - 如果UserContext需要注入Bean,改用懒加载模式:
private static final ThreadLocal<UserContext> contextHolder = ThreadLocal.withInitial(() -> { return new UserContext(); // 不在此处获取Bean });,再在set时由业务代码注入依赖
异步调用时ThreadLocal值丢失怎么办
Spring的@Async、CompletableFuture、线程池提交任务都会创建新线程,ThreadLocal天然不继承。这不是Bug,是设计使然。
- 不要试图“复制”ThreadLocal,而应在提交任务前手动透传关键字段,比如:
String userId = UserContext.getUserId(); CompletableFuture.supplyAsync(() -> { UserContext.setUserId(userId); // 显式设置 return doSomething(); }).whenComplete((r, t) -> UserContext.remove()); - 若大量异步场景,可封装工具类
ThreadLocalCopier,仅拷贝必要字段(如userId、tenantId),避免深拷贝整个上下文对象 - 注意:CompletableFuture默认使用
ForkJoinPool.commonPool(),无法自动注入,必须显式指定自定义线程池并重写beforeExecute来复制上下文
为什么UserContext要设计成不可变或防御性拷贝
ThreadLocal里的对象一旦被下游修改(比如addRole()),会影响同一线程后续逻辑;更危险的是,如果UserContext被当成返回值暴露出去,外部修改会直接污染线程副本。
立即学习“Java免费学习笔记(深入)”;
- UserContext字段全部用
private final,构造时完成初始化 - 如需扩展属性,提供
withXXX()方法返回新实例,而非setXXX() - 对外暴露方法统一加
public UserContext copy(),内部用new UserContext(this)构造 - 特别警惕Map/List类型字段:即使对象本身final,其内容仍可变,必须用
Collections.unmodifiableMap()包装










