HandlerInterceptor
本章聚焦 Spring MVC 的请求拦截层。DispatcherServlet 把请求分发给 Controller 之前,以及 Controller 处理完毕之后,往往需要插入一些横切逻辑——如登录检查、权限校验、日志记录、性能监控等。HandlerInterceptor 正是为此而生的标准扩展接口。
本章与全局的关系:前面章节讲解了请求如何被映射到 Controller、参数如何被解析、返回值如何被序列化。本章讲解请求在 Controller 前后如何被拦截器处理。后续章节将讲解如何通过 WebMvcConfigurer 注册拦截器,以及跨域处理。
定义与作用
HandlerInterceptor 是 Spring MVC 提供的处理器拦截器接口,允许开发者在请求处理链路的三个关键节点插入自定义逻辑:
- preHandle:Controller 方法执行之前调用。常用于登录检查、权限校验、防重复提交等。返回
true放行,返回false中断请求。 - postHandle:Controller 方法执行之后、视图渲染之前调用。常用于修改模型数据、统一包装响应等。
- afterCompletion:整个请求处理完成之后(包括视图渲染)调用。常用于资源清理、异常日志记录、性能统计等。
生活类比:公司大楼的安保系统
想象飞翔科技的办公大楼:
- preHandle(门禁刷卡):你进大楼前,保安检查你的工牌。没工牌?直接拦在门外(返回 false)。有工牌?放行(返回 true)。
- postHandle(电梯间指引):你办完事后准备离开,电梯间广播提示"请带好随身物品"——此时你的事务已办完,但还没真正出门。
- afterCompletion(出门登记):你真正走出大楼后,门卫在登记簿上记录"某某于几点离开"——无论你在楼里有没有吵架(抛异常),出门登记一定会做。
关键认知:拦截器只拦截被 DispatcherServlet 处理的请求,不拦截静态资源(除非特别配置)。
核心原理
拦截器执行顺序时序图
当多个拦截器同时存在时,Spring MVC 按照注册顺序组织成拦截器链,执行顺序遵循"先进后出"原则:
执行顺序规律:
| 阶段 | 执行顺序 | 记忆口诀 |
|---|---|---|
| preHandle | I1 → I2 → Controller | 正序进入 |
| postHandle | Controller → I2 → I1 | 逆序离开 |
| afterCompletion | I2 → I1 | 逆序收尾 |
preHandle 返回 false 时的流程中断
当某个拦截器的 preHandle 返回 false 时,请求处理链立即中断:
重要细节:
- I2 的
preHandle返回 false 后,I2 的 postHandle 和 afterCompletion 不会执行 - I1 的 afterCompletion 仍然会执行(因为它已经放行了)
- 如果 I1 的 preHandle 就返回 false,没有任何 afterCompletion 会执行
适用位置与常用属性
HandlerInterceptor 是一个接口,开发者通过实现该接口来创建拦截器:
| 方法 | 触发时机 | 返回值 | 典型用途 |
|---|---|---|---|
preHandle(HttpServletRequest, HttpServletResponse, Object handler) | Controller 方法执行前 | boolean | 登录检查、权限校验、IP 黑名单 |
postHandle(HttpServletRequest, HttpServletResponse, Object handler, ModelAndView modelAndView) | Controller 方法执行后,视图渲染前 | void | 统一添加模型数据、修改视图名 |
afterCompletion(HttpServletRequest, HttpServletResponse, Object handler, Exception ex) | 请求完全结束后 | void | 日志记录、资源释放、性能统计 |
便捷基类:HandlerInterceptorAdapter
Spring 提供了 HandlerInterceptorAdapter 抽象类,实现了 HandlerInterceptor 接口的所有方法(默认空实现)。开发者只需重写需要的方法:
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 只关心 preHandle,其他方法继承空实现
return request.getSession().getAttribute("user") != null;
}
}
Spring 5.3+ 更新:
HandlerInterceptorAdapter已被标记为@Deprecated,推荐直接实现HandlerInterceptor接口(Java 8 默认方法支持)。
完整示例
场景
飞翔科技员工管理系统需要三层防护:
- 登录检查:未登录用户无法访问任何业务接口
- 权限校验:只有管理员(admin 角色)能访问
/admin/**路径 - 操作日志:记录所有请求的耗时和结果
项目结构
employee-web/
├── src/main/java/
│ └── com/feixiang/web/
│ ├── controller/
│ │ └── AdminController.java
│ └── interceptor/
│ ├── LoginInterceptor.java
│ ├── AdminAuthInterceptor.java
│ └── LogInterceptor.java
拦截器实现
// LoginInterceptor.java
package com.feixiang.web.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("user") == null) {
response.setStatus(401);
response.getWriter().write("{\"error\":\"未登录,请先登录\"}");
return false; // 拦截
}
return true; // 放行
}
}
// AdminAuthInterceptor.java
package com.feixiang.web.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AdminAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
String role = (String) session.getAttribute("role");
if (!"admin".equals(role)) {
response.setStatus(403);
response.getWriter().write("{\"error\":\"权限不足,需要管理员角色\"}");
return false;
}
return true;
}
}
// LogInterceptor.java
package com.feixiang.web.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LogInterceptor implements HandlerInterceptor {
private static final String START_TIME_KEY = "startTime";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute(START_TIME_KEY, System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 可在此修改模型数据
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
Long startTime = (Long) request.getAttribute(START_TIME_KEY);
long duration = System.currentTimeMillis() - startTime;
String uri = request.getRequestURI();
System.out.println("[日志] URI=" + uri + ", 耗时=" + duration + "ms, 异常=" + ex);
}
}
Controller
// AdminController.java
package com.feixiang.web.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/users")
public String listUsers() {
return "管理员用户列表";
}
}
HTTP 请求示例 1:未登录访问被拦截
$ curl -X GET http://localhost:8080/admin/users
响应:
{"error":"未登录,请先登录"}
状态码:401 Unauthorized
流程解析:
- 请求到达 DispatcherServlet
- LoginInterceptor.preHandle 检查 session,发现无 user 属性
- 返回 false,写入 401 响应体
- 请求中断,AdminAuthInterceptor 和 AdminController 都不执行
- LoginInterceptor.afterCompletion 执行(记录日志)
HTTP 请求示例 2:已登录但无权限
先模拟登录(设置 session):
$ curl -X POST http://localhost:8080/login \
-c cookies.txt \
-d "username=xiaocui&password=123456"
再用普通用户访问管理员接口:
$ curl -X GET http://localhost:8080/admin/users \
-b cookies.txt
响应:
{"error":"权限不足,需要管理员角色"}
状态码:403 Forbidden
流程解析:
- LoginInterceptor.preHandle 返回 true(已登录)
- AdminAuthInterceptor.preHandle 检查 role,发现不是 admin
- 返回 false,写入 403 响应体
- 请求中断,AdminController 不执行
- LoginInterceptor.afterCompletion 执行,LogInterceptor 的 afterCompletion 不执行(因为 LogInterceptor 的 preHandle 虽然返回 true,但后续拦截器返回 false 时,已执行 preHandle 的拦截器仍会执行 afterCompletion)
HTTP 请求示例 3:管理员正常访问
$ curl -X GET http://localhost:8080/admin/users \
-b cookies.txt
响应:
管理员用户列表
控制台输出:
[日志] URI=/admin/users, 耗时=23ms, 异常=null
易错场景与面试考点
误区一:拦截器可以拦截静态资源
错误认知:"我配置了 /** 拦截所有请求,所以 /static/logo.png 也会被拦截。"
纠正:默认情况下,Spring MVC 的拦截器不拦截静态资源。静态资源由 ResourceHttpRequestHandler 处理,不走 HandlerExecutionChain 的拦截器逻辑。如果需要拦截静态资源,需要特殊配置(如把静态资源也映射到 Controller,或使用 Filter)。
误区二:preHandle 返回 false 后什么都不执行
错误认知:"某个拦截器返回 false,所有后续逻辑都不执行。"
纠正:已执行过 preHandle 且返回 true 的拦截器,其 afterCompletion 仍然会执行。这是 Spring 设计的资源清理保障机制。只有当前返回 false 的那个拦截器,以及它之后的所有拦截器,不会执行任何后续方法。
误区三:postHandle 能修改 @ResponseBody 的返回值
错误认知:"我在 postHandle 里修改 modelAndView,就能改变 JSON 响应内容。"
纠正:对于 @ResponseBody / @RestController 的响应,消息转换器在 Controller 方法返回后立即写入响应流,postHandle 执行时响应已经提交。此时修改 ModelAndView 对 JSON 响应无效。如果需要统一包装 REST 响应,应使用 ResponseBodyAdvice(本教程不展开)。
面试高频:Filter 和 HandlerInterceptor 的区别
| 对比维度 | Filter(Servlet 规范) | HandlerInterceptor(Spring MVC) |
|---|---|---|
| 所属规范 | Servlet API | Spring MVC |
| 执行时机 | Servlet 容器层面,DispatcherServlet 之前 | Spring MVC 框架层面,DispatcherServlet 内部 |
| 能否获取 Controller 信息 | 不能 | 能(通过 handler 参数) |
| 能否获取 Spring 容器 | 不能直接获取 | 能(通过 ApplicationContext) |
| 配置方式 | web.xml / @WebFilter | WebMvcConfigurer |
| 拦截范围 | 所有请求(包括静态资源) | 仅 DispatcherServlet 处理的请求 |
标准回答:Filter 是 Servlet 容器层面的通用过滤器,HandlerInterceptor 是 Spring MVC 框架层面的专用拦截器。Filter 在 DispatcherServlet 之前执行,HandlerInterceptor 在 DispatcherServlet 内部执行,因此 HandlerInterceptor 能获取到 Spring 容器和 Controller 的上下文信息。
小结
HandlerInterceptor 是 Spring MVC 的请求拦截接口,通过 preHandle、postHandle、afterCompletion 三个回调,在 Controller 前后插入横切逻辑。preHandle 返回 true 放行、false 拦截,是登录检查和权限校验的核心机制。多个拦截器组成拦截器链,执行顺序遵循"正序进入、逆序离开"的规律。
本章与全局的关系:本章讲解了如何编写拦截器逻辑。下一章"WebMvcConfigurer"将讲解如何把拦截器注册到 Spring MVC 中,以及如何配置拦截路径和排除路径。