BaseContext:如何在Service层“隔空取物”获取当前登录用户ID?

97 阅读2分钟

在做Spring Boot 的 Web 项目时,在 Controller 或 Service 层经常会看到这样一行代码:

// 在 Service 层直接获取当前登录用户的ID 
Long userId = BaseContext.getCurrentId();

这就很神奇了:

  1. 没有传参:Controller 调用 Service 时,并没有把 userId 作为参数传进来
  2. 没有查库:这一行代码也没有去查询数据库
  3. 数据准确:它总是能精准地拿到当前发送请求的那个用户的 ID,张三发请求拿到张三,李四发请求拿到李四,互不干扰

它是怎么做到的?

有两个核心概念:ThreadLocal 和 Tomcat 的“一请求一线程”模型。

1.容器:ThreadLocal (线程局部变量)

BaseContext 只是一个包装类,它内部的核心是 JDK 提供的 ThreadLocal。

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)最先拦截

image-20251128145149635

第二步:Controller

拦截器放行后,代码进入 Controller

image-20251128145336078

第三步:Service (取出)

代码进入 Service 层

image-20251128145520388

为什么要这么设计?

使用 ThreadLocal (BaseContext) 的方案,实现了数据在同一线程内的“隐式传递”,让代码极其简洁优雅。

总结

  1. BaseContext 利用 ThreadLocal 实现了线程内部的数据隔离存储。
  2. Tomcat 保证了从拦截器到 Service 处于 同一个线程 中。
  3. 二者结合,让我们可以在 Service 层“隔空”获取 Controller 层(拦截器)解析的数据,极大简化了代码结构。