aftercompletion 是唯一可靠入口,因其在响应写出、异常捕获完毕后执行,确保线程复用前彻底清理 threadlocal,避免内存泄漏;其他时机如 prehandle、posthandle 或 finally 均存在清理遗漏风险。

必须在请求生命周期彻底结束、响应已写出之后调用 ThreadLocal.remove(),否则残留值会随线程复用持续累积,引发内存泄漏。Spring MVC 中最稳妥的位置是 HandlerInterceptor.afterCompletion()。
为什么 afterCompletion 是唯一可靠入口
Tomcat 等容器使用线程池复用工作线程,一个线程可能处理成百上千次请求。若不清理,上一次请求存入的 ThreadLocal 值(如用户上下文、TraceID)会一直强引用在该线程的 ThreadLocalMap 中——key 是弱引用会被 GC 回收,value 却无法释放,形成“脏 entry”。afterCompletion 在视图渲染完成、HTTP 响应已刷出、所有异常都已捕获后触发,且无论正常返回还是抛异常都会执行。
-
preHandle:请求刚进来,还没开始业务逻辑,此时清理会误删即将要用的值 -
postHandle:视图尚未渲染,后续仍可能抛异常导致方法提前退出,remove() 就被跳过 - @PreDestroy 或普通 finally:不覆盖异步请求、Filter 中断、@Async 场景,时机不可控
拦截器中 remove() 的标准写法
必须包裹在 try-finally 中,确保即使 remove() 自身抛出极少见异常(如 value 对象的 finalize 方法出错),也不会影响清理动作的执行。
- 每个自定义 ThreadLocal 都要单独调用
remove(),不能只清一个 - 静态 ThreadLocal 实例必须声明为
private static final,防止类卸载失败 - 避免在
afterCompletion里做耗时操作或调用外部服务,它只负责清理
示例:
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,<br> Object handler, Exception ex) {<br> try {<br> TraceIdContext.get().remove();<br> UserContext.get().remove();<br> LocaleContextHolder.resetLocaleContext(); // Spring 自带的也建议显式重置<br> } finally {<br> // 确保执行,不因上面语句异常而中断<br> }<br>}
配合 WebMvcConfigurer 注册拦截器的注意事项
仅注册拦截器还不够,必须确保它生效于全部请求路径。
- 注册时匹配模式应为
/**,而非/api/**或/v1/**,否则静态资源、错误页等路径下的 ThreadLocal 不会被清理 - 若项目含多个拦截器,注意执行顺序:清理拦截器应排在链尾,避免前置拦截器依赖的上下文被提前清掉
- 对 Filter 链中的 ThreadLocal(如鉴权信息),需在
Filter.doFilter()的finally块中清理,不能依赖拦截器
异步与子线程场景不能靠拦截器
afterCompletion 只作用于主线程的同步请求流程。一旦涉及 @Async、CompletableFuture、手动 new Thread() 或线程池提交任务,子线程不会触发该方法。
- 子线程需在自身任务结束前主动调用对应 ThreadLocal 的
remove() - 若需跨线程传递上下文,应显式拷贝必要字段,而非直接继承 ThreadLocal(InheritableThreadLocal 默认不安全,且子线程仍需自行清理)
- 可封装工具类,如
ThreadLocalUtils.runWithCleaned(() -> { /* 业务逻辑 */ });,内部自动 try-finally 清理










