深入解析Spring MVC中拦截器Interceptor的实现原理和应用场景
前言
在构建企业级 Web 应用时,我们常常需要在请求到达业务逻辑层之前或之后执行一些通用逻辑,例如:
- 用户身份认证与权限校验
- 请求/响应日志记录
- 接口防刷与限流
- 多租户上下文设置
- 性能监控与耗时统计
在 Spring 生态中,拦截器(Interceptor) 是实现上述横切关注点(Cross-Cutting Concerns)的标准机制之一。它作为 Spring MVC 的核心组件,提供了对 Controller 层请求的精细化控制能力。
一、拦截器的本质与定位
1.1 什么是 HandlerInterceptor
HandlerInterceptor 是 Spring Framework 提供的一个接口,用于在 DispatcherServlet 处理请求的流程中插入自定义逻辑。其作用范围限定于 Spring MVC 的 Handler(即 @Controller 方法),不作用于静态资源、错误页面或非 Spring 管理的 Servlet 请求。
public interface HandlerInterceptor {
// 1. Controller 方法执行前调用
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return true; // 返回 true 继续执行;false 中断请求
}
// 2. Controller 方法执行后、视图渲染前调用
default void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 3. 整个请求完成(包括视图渲染)后调用
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex) throws Exception {
}
}
1.2 拦截器 vs 过滤器(Filter)——关键区别
| 维度 | 拦截器(Interceptor) | 过滤器(Filter) |
|---|---|---|
| 规范归属 | Spring MVC 框架 | Java Servlet 规范 |
| 容器依赖 | 依赖 Spring IoC 容器(可注入 Bean) | 不依赖 Spring(原生 Servlet) |
| 作用范围 | 仅 Controller 请求(经 DispatcherServlet 路由) | 所有 Web 请求(包括静态资源、JSP、错误页等) |
| 访问能力 | 可获取 HandlerMethod、方法注解、参数等 | 仅能访问原始 HttpServletRequest/Response |
| 执行时机 | 在 Filter 之后,在 Controller 之前 | 最先执行(在 Spring 上下文初始化前) |
| 异常处理 | afterCompletion 可捕获未处理异常 | 无法感知 Spring 层异常 |
选型建议:
- 需要访问 Spring Bean、Controller 方法元数据 → Interceptor
- 需要处理编码、安全头、全局 CORS、压缩等底层 HTTP 行为 → Filter
二、拦截器的生命周期详解
Spring MVC 请求处理流程中,拦截器的三个方法按以下顺序执行:
[Filter Chain]
→ [Interceptor1.preHandle]
→ [Interceptor2.preHandle]
→ [Controller Method]
→ [Interceptor2.postHandle]
→ [Interceptor1.postHandle]
→ [View Rendering]
→ [Interceptor2.afterCompletion]
→ [Interceptor1.afterCompletion]
2.1preHandle:前置处理
执行时机:DispatcherServlet 已确定目标 Handler(Controller 方法),但尚未调用。
返回值语义:
true:继续执行后续拦截器或 Controllerfalse:中断请求链,不再调用 Controller 和后续拦截器的preHandle
注意事项:
- 即使返回
false,只要该方法被成功调用过,其对应的afterCompletion仍会执行(用于资源清理) - 此阶段可修改
request/response,如重定向、写入 JSON 错误
2.2postHandle:后置处理
执行时机:Controller 方法已执行完毕,但视图尚未渲染(ModelAndView 可修改)
限制条件:
- 若 Controller 抛出未被捕获的异常,此方法不会执行
- 对于
@RestController返回ResponseEntity或@ResponseBody,modelAndView为null,但方法仍会调用
典型用途:
- 向所有页面统一添加公共数据(如用户信息、时间戳)
- 修改视图名称或模型属性
2.3afterCompletion:完成回调
执行时机:整个请求处理完成(包括视图渲染),无论成功或失败
关键参数:Exception ex —— 若请求过程中发生未处理异常,此处可捕获
强制要求:
- 必须在此方法中清理
ThreadLocal变量,防止内存泄漏 - 适合做最终日志记录、连接关闭、指标上报等收尾工作
重要原则:afterCompletion 的调用前提是对应的 preHandle 成功返回(无论 true/false),且未抛出异常。
三、编写自定义拦截器的完整步骤
步骤 1:实现 HandlerInterceptor 接口
// src/main/java/com/example/demo/interceptor/AuthLogInterceptor.java
package com.example.demo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.concurrent.TimeUnit;
/**
* 统一认证与请求日志拦截器
* <p>
* 功能:
* 1. 记录请求入口日志(含 Controller 类/方法名)
* 2. 校验用户登录状态(Session-based)
* 3. 统计请求耗时并记录出口日志
* 4. 清理 ThreadLocal 资源
*/
@Component
public class AuthLogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthLogInterceptor.class);
// 使用 ThreadLocal 存储请求开始时间(线程安全)
private final ThreadLocal<Long> requestStartTime = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
long startTime = System.currentTimeMillis();
requestStartTime.set(startTime);
String uri = request.getRequestURI();
String method = request.getMethod();
String clientIp = getClientIpAddress(request);
// 识别是否为 Controller 方法
if (handler instanceof HandlerMethod handlerMethod) {
String className = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
log.info(">>> [{}] {} from {} -> {}.{}", method, uri, clientIp, className, methodName);
} else {
log.info(">>> [{}] {} from {} (Non-controller handler)", method, uri, clientIp);
}
// === 登录状态校验 ===
Object userId = request.getSession().getAttribute("user_id");
if (userId == null) {
handleUnauthenticated(request, response);
return false; // 中断请求
}
log.debug("✅ Authenticated user [{}] accessing [{}]", userId, uri);
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
org.springframework.web.servlet.ModelAndView modelAndView) {
// 示例:向所有 Thymeleaf 页面添加服务器时间
if (modelAndView != null) {
modelAndView.addObject("serverTime", System.currentTimeMillis());
}
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
Long startTime = requestStartTime.get();
if (startTime == null) return;
long duration = System.currentTimeMillis() - startTime;
String uri = request.getRequestURI();
int status = response.getStatus();
if (ex != null) {
log.error("❌ Request [{}] failed in {}ms, status: {}, exception: {}",
uri, duration, status, ex.getMessage(), ex);
} else {
log.info("<<< Request [{}] completed in {}ms, status: {}", uri, duration, status);
}
} finally {
// ⚠️ 必须清理 ThreadLocal!
requestStartTime.remove();
}
}
/**
* 获取客户端真实 IP(考虑代理)
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
/**
* 处理未认证请求
*/
private void handleUnauthenticated(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (isAjaxRequest(request)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("""
{"code":401,"message":"Authentication required","timestamp":%d}
""".formatted(System.currentTimeMillis()));
} else {
response.sendRedirect("/login");
}
}
/**
* 判断是否为 AJAX 请求
*/
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")) ||
(request.getHeader("Accept") != null &&
request.getHeader("Accept").contains("application/json"));
}
}
代码亮点说明:
- 使用
ThreadLocal<Long>安全存储请求开始时间 - 通过
handler instanceof HandlerMethod区分 Controller 与静态资源 - 支持 AJAX 与普通请求的差异化未登录响应
- 在
finally块中remove()ThreadLocal,杜绝内存泄漏 - 日志包含 IP、URI、Controller 信息,便于追踪
步骤 2:注册拦截器(实现 WebMvcConfigurer)
// src/main/java/com/example/demo/config/WebMvcConfig.java
package com.example.demo.config;
import com.example.demo.interceptor.AuthLogInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC 全局配置类
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthLogInterceptor authLogInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authLogInterceptor)
// 拦截需要认证的路径
.addPathPatterns(
"/admin/**",
"/api/v1/**",
"/user/profile",
"/order/**"
)
// 排除公开路径
.excludePathPatterns(
"/",
"/login",
"/register",
"/public/**",
"/static/**",
"/webjars/**",
"/error",
// Swagger 文档(开发环境)
"/swagger-ui/**",
"/v3/api-docs/**",
// Actuator 健康检查(生产环境)
"/actuator/health"
)
// 设置拦截器优先级(数值越小,优先级越高)
.order(0);
}
}
路径匹配规则说明(Ant 风格):
| 模式 | 匹配示例 | 不匹配示例 |
|---|---|---|
| /api/** | /api/user, /api/user/123 | 无 |
| /admin/* | /admin/dashboard | /admin/user/profile |
| /public/*.html | /public/index.html | /public/css/style.css |
最佳实践:
- 明确列出需保护的路径,而非“拦截所有再排除”
- 将登录、注册、静态资源、健康检查等路径显式排除
- 使用
.order(n)控制多个拦截器的执行顺序
步骤 3:(可选)注入其他 Spring Bean
若拦截器需调用 Service 层逻辑(如查询用户权限),只需:
- 在拦截器类上添加
@Component - 使用
@Autowired注入所需 Bean
@Component
public class PermissionInterceptor implements HandlerInterceptor {
@Autowired
private PermissionService permissionService;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String uri = req.getRequestURI();
String userId = (String) req.getSession().getAttribute("user_id");
if (!permissionService.hasAccess(userId, uri)) {
res.sendError(HttpServletResponse.SC_FORBIDDEN, "Insufficient permissions");
return false;
}
return true;
}
}
注意:不要将拦截器定义为 @Bean,而应使用 @Component + @Autowired 注入到配置类中。
四、多拦截器的执行顺序与控制
当注册多个拦截器时,其执行顺序遵循 “栈”结构:
registry.addInterceptor(loggingInterceptor).order(10); // 后执行 preHandle,先执行 postHandle registry.addInterceptor(authInterceptor).order(0); // 先执行 preHandle,后执行 postHandle
执行流程:
authInterceptor.preHandle()→ 返回 trueloggingInterceptor.preHandle()→ 返回 true- Controller 执行
loggingInterceptor.postHandle()authInterceptor.postHandle()- 视图渲染
loggingInterceptor.afterCompletion()authInterceptor.afterCompletion()
记忆口诀:preHandle 正序,postHandle/afterCompletion 倒序
五、典型应用场景
场景 1:基于 Session 的登录校验(如上文示例)
场景 2:JWT Token 验证(无状态认证)
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String token = extractToken(request);
if (token == null || !jwtUtil.validate(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
// 将用户信息存入 ThreadLocal 或 SecurityContext
return true;
}
场景 3:接口防刷(Redis + 滑动窗口)
@Autowired
private RedisTemplate<String, Integer> redis;
@Override
public boolean preHandle(...) {
String key = "rate_limit:" + getClientIp(request) + ":" + uri;
Integer count = redis.opsForValue().increment(key);
if (count == 1) {
redis.expire(key, 60, TimeUnit.SECONDS); // 1分钟窗口
}
if (count > 10) { // 超过10次/分钟
response.sendError(429, "Too Many Requests");
return false;
}
return true;
}
场景 4:多租户上下文设置
@Override
public boolean preHandle(...) {
String tenantId = resolveTenantId(request); // 从 Header/域名解析
TenantContext.setCurrentTenant(tenantId); // 存入 ThreadLocal
return true;
}
@Override
public void afterCompletion(...) {
TenantContext.clear(); // 清理
}
六、常见问题与调试技巧
问题 1:拦截器未生效
排查清单:
- 配置类是否添加
@Configuration? - 是否实现了
WebMvcConfigurer? - 拦截路径是否被
excludePathPatterns覆盖? - 请求是否为静态资源(默认不走拦截器)?
- Spring Boot 是否启用了 Web MVC?(确保有
@SpringBootApplication)
问题 2:postHandle未执行
可能原因:
- Controller 抛出未被捕获的异常
- 使用了
@ControllerAdvice全局异常处理,但未 rethrow 异常 - 请求被 Filter 或 Security 拦截(如 Spring Security)
问题 3:如何获取 Controller 方法上的自定义注解
if (handler instanceof HandlerMethod hm) {
MyAnnotation anno = hm.getMethodAnnotation(MyAnnotation.class);
if (anno != null) {
// 处理注解逻辑
}
}
调试建议
- 在
preHandle中打印request.getRequestURI() - 使用 Postman 或 curl 测试 API
- 开启 DEBUG 日志:
logging.level.org.springframework.web=DEBUG
七、Spring Boot 3 兼容性说明
- 包名变更:
javax.servlet→jakarta.servlet - 路径匹配器:默认使用
PathPattern(性能优于AntPathMatcher),但行为一致 - Thymeleaf:需使用
spring-boot-starter-thymeleaf3.x
本文所有代码均兼容 Spring Boot 3.x(Jakarta EE 9+)
八、最佳实践
拦截器使用原则
- 职责单一:每个拦截器只做一件事(如认证、日志、限流)
- 避免阻塞:
preHandle中不要执行耗时 I/O 操作 - 资源清理:
ThreadLocal必须在afterCompletion中remove() - 路径明确:使用
addPathPatterns+excludePathPatterns精确控制范围 - 异常安全:
afterCompletion必须处理ex != null的情况
避免的反模式
- 在拦截器中直接操作数据库(应调用 Service)
- 忽略 AJAX 与普通请求的响应差异
- 忘记排除 Swagger、Actuator、静态资源路径
- 在
postHandle中假设modelAndView非空
附录:完整项目结构
src/
└── main/
├── java/
│ └── com.example.demo/
│ ├── DemoApplication.java
│ ├── config/
│ │ └── WebMvcConfig.java
│ ├── interceptor/
│ │ └── AuthLogInterceptor.java
│ ├── controller/
│ │ ├── LoginController.java
│ │ └── AdminController.java
│ └── service/
│ └── UserService.java
└── resources/
├── application.yml
└── static/
└── css/app.css
到此这篇关于深入解析Spring MVC中拦截器Interceptor的实现原理和应用场景的文章就介绍到这了,更多相关Spring MVC拦截器Interceptor内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
- Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析
- SpringBoot使用Mybatis-Plus中分页插件PaginationInterceptor详解
- Spring Boot拦截器Interceptor与过滤器Filter深度解析(区别、实现与实战指南)
- Spring Mvc中拦截器Interceptor用法解读
- Spring Boot拦截器Interceptor与过滤器Filter详细教程(示例详解)
- Spring拦截器之HandlerInterceptor使用方式
- Spring的拦截器HandlerInterceptor详解
- SpringMVC的处理器拦截器HandlerInterceptor详解
- spring中Interceptor的使用小结
相关文章
java.lang.Runtime.exec() Payload知识点详解
在本篇文章里小编给大家整理的是一篇关于java.lang.Runtime.exec() Payload知识点相关内容,有兴趣的朋友们学习下。2020-03-03
Java中Collection集合常用API之 Collection存储自定义类型对象的示例代码
Collection是单列集合的祖宗接口,因此它的功能是全部单列集合都可以继承使用的,这篇文章主要介绍了Java中Collection集合常用API - Collection存储自定义类型对象,需要的朋友可以参考下2022-12-12


最新评论