ThreadLocal解决一个线程里面跨层传递问题

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 的优势
相关推荐
IT_陈寒3 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
Thneonl3 小时前
Celery 生产踩坑:1000 任务积压与 acks_late 双重执行
后端·python
卷福同学3 小时前
第一次当面试官有感
后端·面试
苏三说技术3 小时前
为什么越来越多人用 OnlyOffice?
后端
IT枫斗者枫哥3 小时前
MyBatis一对多分页:LIMIT 20,为什么凑不齐20个订单?
java·数据库
知守观3 小时前
@Transactional 事务失效排查,try-catch 吞异常导致回滚失败(附源码分析)
后端·spring
羑悻3 小时前
Codex + Seed-2.1-pro 实测:多模态理解 + Coding Agent 能扛住真实仓库吗?
后端
代码方舟3 小时前
Java数据工程:利用天远全网运营商三要素优化线上实名认证合规体验
java·人工智能
颜进强3 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
小小张说故事3 小时前
Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手
后端·python