在做Spring Boot 的 Web 项目时,在 Controller 或 Service 层经常会看到这样一行代码:
ini
// 在 Service 层直接获取当前登录用户的ID
Long userId = BaseContext.getCurrentId();
这就很神奇了:
- 没有传参 :Controller 调用 Service 时,并没有把
userId作为参数传进来 - 没有查库:这一行代码也没有去查询数据库
- 数据准确:它总是能精准地拿到当前发送请求的那个用户的 ID,张三发请求拿到张三,李四发请求拿到李四,互不干扰
它是怎么做到的?
有两个核心概念:ThreadLocal 和 Tomcat 的"一请求一线程"模型。
1.容器:ThreadLocal (线程局部变量)
BaseContext 只是一个包装类,它内部的核心是 JDK 提供的 ThreadLocal。
csharp
public class BaseContext {
// 核心:ThreadLocal 对象
public static 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(); // 清除
}
}
ThreadLocal 的作用:
当我们在 线程 A 中往 ThreadLocal 存入数据时,只有 线程 A 能取出来
线程 B 即使访问同一个变量,也完全摸不到 线程 A 的数据
这就是线程隔离(Thread Safety)
2.环境:Tomcat 的"一请求一线程"模型
Spring Boot 内置的 Web 服务器通常是 Tomcat。Tomcat 处理请求的机制是:One Request, One Thread (一个 HTTP 请求,由一个独立的线程全程负责)
当一个用户发起请求(比如"添加购物车")时:
Tomcat 分配 线程 X 来处理这个请求
拦截器 (Interceptor) 是 线程 X 执行的
Controller 是 线程 X 执行的
Service 还是 线程 X 执行的
Mapper 依然是 线程 X 执行的
结论: 只要我们没有手动开启新线程(new Thread),整个后端业务流程就像一场接力赛,但是是同一个运动员(线程 X) 从头跑到尾
流程
基于以上两个原理,我们可以还原 userId 是如何从请求头一步步流转到 Service 层的:
第一步:拦截器 (存入)
请求刚到达后端,拦截器(JwtTokenUserInterceptor)最先拦截

第二步:Controller
拦截器放行后,代码进入 Controller

第三步:Service (取出)
代码进入 Service 层

为什么要这么设计?
使用 ThreadLocal (BaseContext) 的方案,实现了数据在同一线程内的"隐式传递",让代码极其简洁优雅。
总结
- BaseContext 利用 ThreadLocal 实现了线程内部的数据隔离存储。
- Tomcat 保证了从拦截器到 Service 处于 同一个线程 中。
- 二者结合,让我们可以在 Service 层"隔空"获取 Controller 层(拦截器)解析的数据,极大简化了代码结构。