try-finally是最稳妥的清理时机,因为JVM保障其无论异常、return或break均执行,可防止线程复用导致的上下文污染与内存泄漏。

在 Web 应用中,MDC(Mapped Diagnostic Context)和自定义 ThreadLocal 变量常用于透传请求 ID、用户信息等上下文,方便日志追踪。但若不及时清理,极易引发内存泄漏或跨请求污染——比如上一个请求的 traceId 出现在下一个请求的日志里。
为什么 try-finally 是最稳妥的清理时机
异步调用、过滤器链中断、未捕获异常、甚至 Spring 的 AOP 异常传播都可能导致 @After 或 finally 之外的“收尾逻辑”被跳过。而 try-finally 是 JVM 层保障执行的机制:只要进入 try 块,无论是否抛异常、是否 return、是否 break,finally 都会执行(除非 JVM 直接退出或线程被强制 stop)。
这意味着,把清理逻辑写在 finally 中,等于给上下文变量上了“最后防线”。
典型场景:Servlet 过滤器中的 MDC 清理
在基于 Filter 的请求入口(如 Logback + MDC),应在 doFilter 内部包裹业务逻辑,并在 finally 中清空 MDC:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
try {
// 设置请求 ID 到 MDC
String traceId = generateTraceId();
MDC.put("traceId", traceId);
// 其他上下文,如 user, tenant 等
MDC.put("user", getCurrentUser(req));
chain.doFilter(req, res); // 执行后续链(可能抛异常)
} finally {
// 必执行:清空整个 MDC,避免线程复用污染
MDC.clear();
}
}
自定义 ThreadLocal 的安全封装方式
直接使用 ThreadLocal.set() + remove() 容易遗漏。推荐封装为可自动清理的资源类,配合 try-with-resources 或显式 try-finally:
- 定义一个
ContextScope类,构造时设值,close()时调用remove() - 在业务代码中用
try (var scope = new ContextScope(...)) { ... },JVM 自动调用close() - 若无法用 try-with-resources(如老 JDK 或非标准生命周期),就退回到
try-finally显式调用scope.close()
关键点:永远不要只依赖“业务结束时手动 remove”,必须有 finally 保底。
Spring WebMvc 中的替代方案与局限
有人倾向用 @ControllerAdvice + @ExceptionHandler 或 HandlerInterceptor.afterCompletion() 清理。但这些机制有盲区:
-
afterCompletion不保证执行(例如异步请求、响应已提交后异常、容器提前终止) -
@ExceptionHandler只覆盖被捕获的异常,不覆盖未处理的Error或线程中断 - 它们都不是 JVM 级别的执行保障,可靠性低于
try-finally
所以,拦截器/切面可作为辅助日志记录,但不能替代 finally 中的清理动作。

















