目录
一、先给结论
二、完整登录请求全流程 + ThreadLocal 存取销毁时序
整体流程链路
1. 分步拆解 + ThreadLocal 操作时机
① 请求进来:preHandle 前置拦截(存入 ThreadLocal)
② 执行业务逻辑(Controller / Service / Mapper)
③ 请求正常响应 / 抛出异常 → 进入 afterCompletion(执行 remove 清空)
三、关键问题:登录接口执行完返回结果,立刻 remove 了吗?
1. 时间线顺序
2. 那登录过程中会不会被提前清空?
四、异常场景会不会漏掉 remove?
场景 1:接口主动抛异常
场景 2:请求被拦截器直接拦截拒绝
五、为什么一定要放在 afterCompletion 清理,不放在 Controller 最后清理?
六、线程池复用下整个流程演示(最核心)
七、错误用法对比(线上事故来源)
错误 1:只 set 不 remove
错误 2:在 Controller 方法末尾清理
正确唯一写法
八、精简面试口述版
一、先给结论
普通登录接口走完响应返回后,不会立刻执行 remove只有完整一次 HTTP 请求从头到尾走完(正常结束 / 异常结束),才会在afterCompletion里执行ThreadLocal.remove()清空上下文。
二、完整登录请求全流程 + ThreadLocal 存取销毁时序
整体流程链路
前端发起登录请求 → 到达网关 → 进入 SpringMVC 拦截器 → 登录 Controller → 登录 Service → 响应返回 → 后置清理 ThreadLocal
1. 分步拆解 + ThreadLocal 操作时机
① 请求进来:preHandle前置拦截(存入 ThreadLocal)
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. 从Header拿Token String token = request.getHeader("token"); // 2. 校验Token、解析出当前登录用户信息 LoginUser user = tokenCheckAndGetUser(token); // 3. 把用户信息存入 ThreadLocal UserContext.setUser(user); return true; // 放行进入控制器 }此时状态
- 当前请求线程的
ThreadLocalMap存入了当前登录用户 - 全局任意业务层、工具类都能
UserContext.getUser()获取
② 执行业务逻辑(Controller / Service / Mapper)
@RestController public class LoginController { @PostMapping("/login") public Result login(@RequestBody LoginDTO dto){ // 业务登录逻辑:账号密码校验、生成新token String newToken = loginService.doLogin(dto); // 业务中随时获取当前登录人 Long userId = UserContext.getUserId(); return Result.ok(newToken); } }全程ThreadLocal 数据一直存在,随时可取。
③ 请求正常响应 / 抛出异常 → 进入afterCompletion(执行 remove 清空)
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 不管接口成功、报错、抛异常,都会走到这里 UserContext.clear(); }// UserContext.clear() 底层 public static void clear(){ USER_THREAD_LOCAL.remove(); }这里才是真正删除 ThreadLocal 数据的时机
三、关键问题:登录接口执行完返回结果,立刻 remove 了吗?
1. 时间线顺序
- 接口代码执行完毕 → 组装返回值 →给前端返回响应
- 响应已经发给前端之后,才会执行
afterCompletion - 最后才调用
remove()清空 ThreadLocal
顺序总结接口业务结束 → 响应返回前端 → 后置拦截器清空 ThreadLocal
2. 那登录过程中会不会被提前清空?
绝对不会
preHandle:请求进来前置存数据afterCompletion:整个请求生命周期彻底结束才清- 中间 Controller、Service 所有代码执行期间,ThreadLocal 数据全程有效
四、异常场景会不会漏掉 remove?
场景 1:接口主动抛异常
public void biz(){ throw new RuntimeException("业务报错"); }Spring MVC 异常机制:
- 异常往上抛
- 全局异常处理器捕获
- 封装错误响应返回前端
- 依然一定会进入 afterCompletion→ 执行 remove
场景 2:请求被拦截器直接拦截拒绝
preHandle返回 false
- 不会进入 Controller
- 依然执行 afterCompletion,依旧清空
结论:只要进了 preHandle,就一定会走 afterCompletion,一定能清空
五、为什么一定要放在 afterCompletion 清理,不放在 Controller 最后清理?
- Controller 太多,每个人都容易忘写
remove - 一旦中间抛异常,Controller 末尾代码不走,直接内存泄漏
afterCompletion是 SpringMVC请求生命周期最终回调,兜底最强,100% 执行- 统一收口,全局只写一次清理代码,规范统一
六、线程池复用下整个流程演示(最核心)
- 线程池拿出线程
Thread-1 - 处理用户 A请求:preHandle 存入 A 用户信息
- 执行业务、返回结果
- afterCompletionremove 清空→ ThreadLocal 为空
- 线程
Thread-1归还线程池 - 下次复用处理用户 B请求
- 重新存入 B 用户信息,用完再次清空
完美闭环,无残留数据,彻底杜绝内存泄漏
七、错误用法对比(线上事故来源)
错误 1:只 set 不 remove
public void login(){ UserContext.setUser(user); // 业务逻辑 // 没有任何清理 }线程复用后,上一个用户数据残留到下一个请求→ 串号、权限错乱 + 内存泄漏
错误 2:在 Controller 方法末尾清理
public Result login(){ UserContext.setUser(user); biz(); UserContext.clear(); // 写在这里 }一旦biz()抛异常,最后一行清理代码不执行,直接泄漏。
正确唯一写法
前置存,后置拦截器统一兜底清。
八、精简面试口述版
- 用户发起登录请求,先经过 Spring 拦截器
preHandle,解析 Token 拿到用户信息存入 ThreadLocal 上下文; - 进入登录接口执行业务逻辑,全程可任意获取当前登录用户;
- 接口处理完成向前端返回响应;
- 请求生命周期结束后,触发拦截器
afterCompletion方法,统一调用 ThreadLocal 的 remove 方法清除数据; - 无论接口正常执行还是抛出异常,都会执行清空操作;
- 线程池复用场景下,每次请求用完必清空,避免用户数据串访与 ThreadLocal 内存泄漏。