文章目录
-
-
- 第一步:创建上下文持有者(UserContext)
- 第二步:在拦截器(Interceptor)中注入信息
- 第三步:在业务层(Service)丝滑使用
- 为什么这套方案是"最优解"?
- [⚠️ 两个必须避开的"巨坑"](#⚠️ 两个必须避开的“巨坑”)
-
在 Web 开发中,用 ThreadLocal 存储用户登录上下文(User Context)是一个非常经典且高效的设计模式。它能让你在业务层代码的任何地方,像呼吸一样自然地获取当前登录用户 ,而不需要在每个方法参数里都传一遍 userId。
以下是实现这一功能的完整三部曲:
第一步:创建上下文持有者(UserContext)
我们需要封装一个工具类来操作 ThreadLocal。泛型直接指定为你系统中的 User 对象或 UserInfo 实体类。
java
public class UserContext {
// 泛型定义为 User 对象,存储当前线程的登录用户信息
private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();
// 保存用户信息
public static void setUser(User user) {
USER_HOLDER.set(user);
}
// 获取当前登录用户
public static User getUser() {
return USER_HOLDER.get();
}
// 关键:请求结束必须清理,防止内存泄漏和线程复用污染
public static void remove() {
USER_HOLDER.remove();
}
}
第二步:在拦截器(Interceptor)中注入信息
Web 请求进来时,通常会经过一个拦截器。在这里校验 Token、查询用户信息,并塞入 ThreadLocal。
java
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从 Header 或 Cookie 中获取 Token
String token = request.getHeader("Authorization");
if (token != null) {
// 2. 模拟从 Redis 或数据库获取用户信息
User user = userService.getUserByToken(token);
// 3. 核心步骤:将用户信息存入当前线程的"口袋"
UserContext.setUser(user);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 4. 极其重要:请求处理完后,必须清理掉,否则在线程池环境下会产生数据错乱
UserContext.remove();
}
}
第三步:在业务层(Service)丝滑使用
现在,你可以在任何 Service、Mapper 甚至工具类里,随时随地拿到用户信息。
java
@Service
public class OrderService {
public void createOrder() {
// 无需从 Controller 传参数,直接从上下文中"摸"出当前用户
User currentUser = UserContext.getUser();
System.out.println("当前下单用户是:" + currentUser.getName());
// 执行后续业务逻辑...
}
}
为什么这套方案是"最优解"?
- 代码零侵入 :你的
Service方法签名不再需要(Long userId, String userName...)这种冗长的参数,代码非常干净。 - 横向解耦 :身份验证逻辑在拦截器里,业务逻辑在 Service 里,通过
ThreadLocal这个隐式通道连接,互不干扰。 - 性能极高:它是内存操作,没有数据库 I/O 损耗,且天然支持并发隔离。
⚠️ 两个必须避开的"巨坑"
- 内存泄漏(Memory Leak) :
由于 Tomcat 等服务器使用的是线程池 ,线程是不会被销毁的。如果你在afterCompletion里忘记调用.remove(),这个线程下次处理别的请求时,可能还会带着上一个人的用户信息。这不仅是内存溢出风险,更是严重的安全事故(A 看到 B 的数据)。 - 多线程环境失效 :
如果你在 Service 里开启了异步线程(比如CompletableFuture或@Async),子线程是拿不到 主线程ThreadLocal里的用户信息的。
- 解决办法 :使用
InheritableThreadLocal(支持父子线程传递)或者阿里巴巴开源的TransmittableThreadLocal。
一句话总结:
ThreadLocal 就是在整个请求生命周期内,为每个线程开辟的一个线程私有的"全局变量存储区",它让用户信息的传递变得透明且优雅。