Problems&Solutions
登录用户的id在拦截器里面被解析但是Services拿不到,所以要ThreadLocal来解释:
拦截器在线程里面放下empId=" "
Service里面打开这个ThreadLocal来获取到这个empId
项目里面有一个BaseContext类简单包装了ThreadLocal
java
package com.sky.context;
public class BaseContext {
// 创建一个 ThreadLocal,用来存当前登录用户的ID
private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();
// 放数据:当前线程存一个值
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
// 取数据:当前线程取出之前存的值
public static Long getCurrentId() {
return threadLocal.get();
}
// 清除数据:当前线程移除值(防止内存泄漏,后面讲)
public static void removeCurrentId() {
threadLocal.remove();
}
}
完整的传递流程:
第 1 步:拦截器里 set(放进去)
@Component
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 1. 从请求头取 token
String token = request.getHeader("token");
// 2. 解析 token,拿到 empId
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:{}", empId);
// 3. ★ 关键:把 empId 放进当前线程的 ThreadLocal
BaseContext.setCurrentId(empId);
return true; // 放行
}
}
执行完这一步,当前线程的储物柜里就有了 empId=1。
第 2 步:Service 里 get(取出来)
@Service
public class EmployeeServiceImpl implements EmployeeService {
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
employee.setStatus(StatusConstant.ENABLE);
employee.setPassword(DigestUtils.md5DigestAsHex(...));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
// ★ 关键:从当前线程的 ThreadLocal 里取出 empId
employee.setCreateUser(BaseContext.getCurrentId()); // 取出 1
employee.setUpdateUser(BaseContext.getCurrentId()); // 取出 1
employeeMapper.insert(employee);
}
}
因为还是同一个线程,所以 getCurrentId() 就能拿到拦截器里放的 1。
第 3 步:请求结束后 remove(清理)
// 拦截器的 afterCompletion 方法,请求结束后执行
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// ★ 清理当前线程的 ThreadLocal,防止内存泄漏
BaseContext.removeCurrentId();
}
五、为什么必须 remove?
Tomcat 使用线程池,线程不会销毁,会被重复使用:
请求1(admin)→ 线程A → set(1) → 处理完 → 线程A回到线程池
请求2(新来的)→ 线程A又被拿出来用 → 如果没remove,getCurrentId() 还能拿到 1!← 脏数据!
所以必须在请求结束后清理,否则下一个复用这个线程的请求可能拿到上一个用户的 ID,造成数据混乱。
六、完整流程图
前端请求(带token)
│
▼
【拦截器 preHandle】
│
├── 解析token → empId=1
├── BaseContext.setCurrentId(1) ← 放进线程A的储物柜
└── 放行
│
▼
【Controller.save()】
│
└── employeeService.save(dto)
│
▼
【Service.save()】
│
├── 组装 employee 对象
├── BaseContext.getCurrentId() ← 从线程A的储物柜取出 1
├── setCreateUser(1)
└── employeeMapper.insert(employee)
│
▼
【Mapper.insert()】→ 数据库
│
▼
【拦截器 afterCompletion】
│
└── BaseContext.removeCurrentId() ← 清理线程A的储物柜
│
▼
响应返回前端
七、关键要点总结
表格
| 问题 | 答案 |
|---|---|
| ThreadLocal 是什么 | 每个线程的私人储物柜,线程间数据隔离 |
| 为什么能用它传值 | 一次请求从头到尾在同一个线程执行 |
| 在哪放 | 拦截器 preHandle 里 setCurrentId(empId) |
| 在哪取 | Service 里 getCurrentId() |
| 为什么要清理 | 线程池会复用线程,不清理会有脏数据 |
| 在哪清理 | 拦截器 afterCompletion 里 removeCurrentId() |
| 需不需要改方法参数 | 不需要!这就是 ThreadLocal 的优势 |