Spring MVC 拦截器 HandlerInterceptor 三大方法详解
Spring MVC 拦截器 HandlerInterceptor 三大方法详解
一句话概括
HandlerInterceptor 是 Spring MVC 提供的请求处理切面,它在 DispatcherServlet 处理请求的生命周期里插入 preHandle / postHandle / afterCompletion 三个回调,让你在不侵入 Controller 的前提下,统一处理鉴权、日志、上下文切换等横切关注点。
继承关系:HandlerInterceptor 与 AsyncHandlerInterceptor
先纠正一个常见误解:preHandle / postHandle / afterCompletion 三个方法定义在 HandlerInterceptor 接口里,而 AsyncHandlerInterceptor 只是继承它、额外增加了一个方法 afterConcurrentHandlingStarted。
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
public interface AsyncHandlerInterceptor extends HandlerInterceptor {
default void afterConcurrentHandlingStarted(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
}
}类图:
注意:Spring 5.3 起
HandlerInterceptorAdapter已被标记@Deprecated,官方推荐直接实现HandlerInterceptor或AsyncHandlerInterceptor(接口方法都是default,只重写需要的即可)。
三大方法逐一拆解
1️⃣ preHandle — 处理器执行前(守门员)
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;- 时机:定位到 Handler(Controller 方法)之后、真正调用之前。
- 返回值:
true继续执行链(后续拦截器 + Controller);false中断,后续拦截器与 Controller 都不执行,框架会对已经通过 preHandle 的前置拦截器依次回调afterCompletion。 - 典型用途:登录/鉴权、权限校验、JWT 校验、限流、参数预处理、写入 TraceId、把用户信息放进
ThreadLocal或ContextHolder。
2️⃣ postHandle — 处理器执行后、视图渲染前(中场修正)
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception;- 时机:Controller 正常返回之后、视图渲染之前。
- 作用:能拿到并修改
ModelAndView——统一追加公共 model 数据、动态改视图名。 - 两个不调用的情况:
- Controller 抛异常(异常走
HandlerExceptionResolver,不再渲染视图) @ResponseBody/ResponseEntity这类直接把结果写进响应体的场景,没有ModelAndView,一般用不上它。
- Controller 抛异常(异常走
- 典型用途:注入全局页面数据(当前用户、菜单、导航)、统一设置响应头。
3️⃣ afterCompletion — 请求彻底结束(收尾人)
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception;- 时机:视图渲染完成之后(或异常处理链走完之后),是整条链路的最后一步。
- 保证性:只要
preHandle返回了true,这个回调几乎一定会执行,ex参数能告诉你这次请求最终是否异常。 - 典型用途:释放资源、关闭连接、清理
ThreadLocal(防线程池复用导致内存泄漏与数据串读)、记录耗时/响应状态、埋点。
同步请求完整时序
异步请求:AsyncHandlerInterceptor 与 afterConcurrentHandlingStarted
这是 AsyncHandlerInterceptor 存在的意义。当 Controller 返回 Callable、DeferredResult、SseEmitter、StreamingResponseBody 等触发异步处理时,DispatcherServlet 的时序会明显变化。
核心结论:
preHandle会执行两次(两次 dispatch 各一次)postHandle只在第二次 dispatch 执行afterCompletion只在第二次 dispatch 执行afterConcurrentHandlingStarted只在异步刚开始时执行一次,代替第一次 dispatch 的postHandle+afterCompletion
为什么要单独设计这个回调?
官方注释很直白:
Called instead of
postHandleandafterCompletionwhen the handler is being executed concurrently. A typical use of this method would be to clean up thread-local variables.
第一次 dispatch 时,Controller 只返回了一个"未来结果"(Callable 等),真正的业务要在 taskExecutor 的另一个线程执行,请求线程此刻就要归还容器。因此:
- 结果(
ModelAndView)还没就绪,没法走postHandle; afterCompletion要等最终结果渲染完才算"完成",也不能现在调;- 于是给一个
afterConcurrentHandlingStarted,让你在释放请求线程前及时清理线程绑定数据(ThreadLocal)。
⚠️ 陷阱:如果你的拦截器在 afterCompletion 里依赖请求线程上下文(租户、登录态等),异步场景下它被延后到结果线程执行,可能导致上下文丢失或错位。
应用场景对照表
| 关注点 | 推荐方法 | 说明 |
|---|---|---|
| 登录/鉴权、权限校验 | preHandle | 不通过直接 return false 中断 |
| 限流、黑白名单 | preHandle | 请求入口拦截 |
| TraceId / 请求追踪 | preHandle | 写入 MDC / 请求属性 |
| 把用户/租户信息放入上下文 | preHandle | 供后续 Controller / Service 使用 |
| 统一注入页面公共数据 | postHandle | 修改 ModelAndView |
| 统一响应头 | postHandle | 视图渲染前设置 |
| 资源释放 / 连接关闭 | afterCompletion | 兜底清理 |
| ThreadLocal 清理 | afterCompletion | 必需,防内存泄漏 |
| 耗时统计 / 访问日志 | afterCompletion | 拿到最终状态与异常 |
| 异步请求提前清理 ThreadLocal | afterConcurrentHandlingStarted | 释放请求线程前 |
实战示例
1. 登录鉴权拦截器
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
User user = parseToken(request.getHeader("Authorization"));
if (user == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
return false; // 中断,不再进入 Controller
}
UserContextHolder.setUser(user); // 放入线程上下文
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
UserContextHolder.clear(); // 关键:清理 ThreadLocal,防止线程复用串数据
}
}2. 访问日志 + 耗时统计
public class AccessLogInterceptor implements HandlerInterceptor {
public static final String ATTR_START = "AccessLogInterceptor.start";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute(ATTR_START, System.currentTimeMillis());
log.info("[preHandle] 开始请求 {} {}", request.getMethod(), request.getRequestURI());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
long start = (long) request.getAttribute(ATTR_START);
long cost = System.currentTimeMillis() - start;
log.info("[afterCompletion] 完成请求 {} 耗时 {}ms ex={}", request.getRequestURI(), cost, ex);
}
}3. 租户上下文切换(成对的 preHandle + afterCompletion)
public class TenantContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Long visitTenantId = getVisitTenantId(request);
if (visitTenantId != null) {
TenantContextHolder.setTenantId(visitTenantId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
TenantContextHolder.clear(); // 还原,防止租户数据串读
}
}注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/login"); // 排除登录接口
registry.addInterceptor(new AccessLogInterceptor())
.addPathPatterns("/**");
}
}注意事项与最佳实践
preHandle与afterCompletion成对使用:在preHandle/业务里往ThreadLocal写的东西,务必在afterCompletion清掉。postHandle不保证执行:Controller 抛异常或@ResponseBody场景别依赖它做关键逻辑。afterCompletion用来兜底清理,而非业务逻辑——要幂等、要能容忍上下文已部分清理。- 异步接口要用
AsyncHandlerInterceptor:系统里有Callable/SseEmitter等异步接口、且拦截器持有线程绑定状态时,升级并实现afterConcurrentHandlingStarted。 - 别用已废弃的
HandlerInterceptorAdapter,直接实现接口。 - 拦截器是链式执行的:多个拦截器按注册顺序执行
preHandle,按逆序执行postHandle和afterCompletion。
小结
HandlerInterceptor三个方法构成了围绕 Controller 的前置 → 后置 → 收尾切面;- 日常同步接口,实现
HandlerInterceptor就够;异步接口记得用AsyncHandlerInterceptor处理afterConcurrentHandlingStarted; - 牢记异步时序差异:
preHandle跑两次,postHandle/afterCompletion延后到结果线程。