Spring Boot 拦截器(Interceptor)与实战:原理、配置与最佳实践
- 发布时间:2025-12-29 20:56:26
- 本文热度:浏览 5 赞 0 评论 0
- 文章标签: SpringBoot Java Interceptor
- 全文共1字,阅读约需1分钟
🚀 Spring Boot 拦截器(Interceptor)深度解析与实战:从原理到应用
一、基础概念与核心思想:拦截器的诞生
在我们的日常Web应用开发中,常常需要在处理业务逻辑之前或之后执行一些横切关注点(Cross-cutting Concerns)操作,例如用户身份验证、权限校验、日志记录、性能监控等。如果我们将这些操作分散到每一个业务处理方法中,代码就会变得冗余、难以维护,并且极大地破坏了关注点分离(Separation of Concerns)的原则。
1. 什么是拦截器(Interceptor)?
拦截器,顾名思义,就是拦截请求和响应的组件。它工作在请求到达Controller之前以及Controller处理完请求之后,DispatcherServlet返回响应之前。
在 Spring Boot(本质上是 Spring Web MVC)中,拦截器是实现 HandlerInterceptor 接口的类,它允许我们在请求处理生命周期的关键节点植入自己的逻辑。
💡 类比理解:机场安检
我们可以把整个请求处理流程想象成旅客登机。
- 旅客(请求):带着目的地(URL)和行李(参数)来了。
- 值机柜台(Controller):处理核心业务,分配座位。
- 安检口(拦截器):在到达值机柜台(Controller)之前,检查你的身份(权限验证)、记录你的信息(日志),这就是拦截器的工作。只有通过安检,才能去值机。
2. 为什么需要拦截器?
拦截器解决了 Web 应用中的一系列通用性问题,它的核心价值在于:
- 集中处理横切关注点: 将分散在各个 Controller 方法中的通用逻辑(如权限检查)集中到一处管理和维护。
- 代码解耦与复用: 业务逻辑(Controller)只专注于业务,通用逻辑(Interceptor)只专注于通用处理,两者高度解耦,通用逻辑可以复用。
- 请求生命周期控制: 允许开发者在请求处理的多个阶段(前、后、完成时)介入,实现精细化的控制。
| 场景 | 拦截器应用 |
|---|---|
| 安全 | 身份认证(登录状态检查)、权限校验(基于角色的访问控制 RBAC) |
| 日志 | 记录请求的 URI、参数、处理时间、响应状态码等 |
| 性能 | 统计 API 的执行时间,进行性能监控 |
| 数据处理 | 请求参数预处理、统一字符编码设置 |
| 审计 | 记录敏感操作,用于追溯 |
二、工作原理:请求的完整生命周期
理解拦截器的工作原理,需要将其置于 Spring Web MVC 的整个请求处理流程中。拦截器是 Spring MVC DispatcherServlet 机制的一部分。
1. DispatcherServlet 与 HandlerInterceptor
当一个 HTTP 请求到达 Spring Boot 应用时,DispatcherServlet 是接收请求的核心前端控制器。它的基本流程如下:
- 接收请求。
- 通过
HandlerMapping查找能够处理该请求的 Handler(通常是 Controller 中的方法)。 - 如果找到了 Handler,
DispatcherServlet会查找所有适用的 HandlerInterceptor(拦截器链)。 - 请求依次经过拦截器链的
preHandle()方法。 - 如果所有
preHandle()都返回true,请求进入 Handler(Controller)执行。 - Handler 执行完毕后,请求返回,依次经过拦截器链的
postHandle()方法(注意:顺序与 preHandle 相反)。 - 视图渲染完毕或响应提交后,请求再次依次经过拦截器链的
afterCompletion()方法(顺序与 preHandle 相反)。
2. HandlerInterceptor 接口详解
Spring 框架中的 HandlerInterceptor 接口定义了三个核心方法,它们分别对应请求生命周期的三个关键节点:
-
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)- 作用: 在 Handler(Controller 方法)执行之前 执行。
- 返回值:
boolean。如果返回true,则请求继续执行(进入下一个拦截器或 Controller);如果返回false,则请求被中断,不再执行后续的拦截器和 Controller,必须手动编写响应(如返回 401 错误)。 - 场景: 身份验证、权限校验、日志预处理。
-
postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)- 作用: 在 Handler(Controller 方法)执行之后,但在视图渲染之前 执行。
- 注意: 只有
preHandle()返回true的拦截器才会执行postHandle()。 - 场景: 对 Model 和 View 进行操作,统一添加公共数据、修改响应头等。
-
afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)- 作用: 在 整个请求处理完成之后(视图渲染或响应已提交) 执行,无论 Controller 执行是否成功,都会执行(除非
preHandle直接返回false)。 - 参数
Exception ex: 如果 Controller 执行过程中抛出了异常,可以在此方法中获取并进行统一的异常处理、资源清理等。 - 场景: 资源清理、日志记录最终结果(如耗时统计)。
- 作用: 在 整个请求处理完成之后(视图渲染或响应已提交) 执行,无论 Controller 执行是否成功,都会执行(除非
3. 拦截器链的执行顺序
如果配置了多个拦截器(Interceptor A, Interceptor B),它们会形成一个拦截器链,执行顺序如下:
$$ \text{Request} \rightarrow \text{A}{\text{pre}} \rightarrow \text{B}{\text{pre}} \rightarrow \text{Controller} \rightarrow \text{B}{\text{post}} \rightarrow \text{A}{\text{post}} \rightarrow \text{B}{\text{after}} \rightarrow \text{A}{\text{after}} \rightarrow \text{Response} $$
总结:
preHandle按照配置顺序(A $\rightarrow$ B)执行。postHandle和afterCompletion按照配置的逆序(B $\rightarrow$ A)执行,体现了栈的特性。
⚠️ 关键点: 如果任何一个拦截器的
preHandle()返回false,整个请求链会被中断。此时,只有位于该拦截器之前的拦截器的afterCompletion()方法会被执行,用来保证资源的清理。
三、实战:自定义拦截器的实现与配置
下面我们将从零开始,创建一个 Spring Boot 项目,并实现一个完整的、具备权限校验和耗时统计功能的拦截器。
1. 拦截器类实现:AuthInterceptor
首先,我们创建一个自定义拦截器,实现 HandlerInterceptor 接口。
package com.example.interceptor.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 身份认证与请求耗时统计拦截器
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
// 线程变量,用于保存请求开始时间,确保线程安全
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
/**
* 1. preHandle:在Controller方法执行前执行
* @return true: 继续执行;false: 中断执行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 记录请求开始时间
START_TIME.set(System.currentTimeMillis());
String requestUri = request.getRequestURI();
String token = request.getHeader("Authorization");
log.info("【preHandle】开始请求:{},尝试获取Token: {}", requestUri, token);
// --- 权限校验逻辑示例 ---
if (requestUri.contains("/api/auth/login")) {
// 登录接口放行
return true;
}
if (token == null || !token.startsWith("Bearer ")) {
log.warn("【preHandle】请求被拦截:缺少或无效的Token");
// 设置响应状态码和返回信息
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
response.getWriter().write("{\"code\":401,\"message\":\"未授权或Token缺失\"}");
// 中断后续执行
return false;
}
// 模拟Token校验成功
log.info("【preHandle】Token校验成功,请求继续");
return true;
}
/**
* 2. postHandle:在Controller方法执行后,但在视图渲染前执行
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 可以在这里对返回的ModelAndView或响应头进行统一操作
response.setHeader("X-Processed-By", "AuthInterceptor");
log.info("【postHandle】Controller执行完成,URI: {}", request.getRequestURI());
}
/**
* 3. afterCompletion:在整个请求处理完成后执行(无论是否抛出异常)
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Long startTime = START_TIME.get();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
log.info("【afterCompletion】请求结束:{},耗时:{} ms,响应状态:{}",
request.getRequestURI(), costTime, response.getStatus());
// 清理ThreadLocal,防止内存泄漏
START_TIME.remove();
if (ex != null) {
log.error("【afterCompletion】请求处理中发生异常:", ex);
}
}
}
2. 拦截器注册与配置:WebMvcConfigurer
实现了拦截器后,我们还需要将它注册到 Spring MVC 的配置中,指定它对哪些路径生效,对哪些路径排除。这需要通过实现 WebMvcConfigurer 接口来完成。
package com.example.interceptor.config;
import com.example.interceptor.core.AuthInterceptor;
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 {
private final AuthInterceptor authInterceptor;
// 依赖注入我们创建的拦截器
@Autowired
public WebMvcConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
/**
* 注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器
registry.addInterceptor(authInterceptor)
// 配置拦截路径:拦截所有 /api/** 的请求
.addPathPatterns("/api/**")
// 配置排除路径:放行 swagger 文档相关的路径和特定的登录接口
.excludePathPatterns("/swagger-ui.html", "/webjars/**", "/v2/**", "/swagger-resources/**", "/api/auth/login");
// 如果有第二个拦截器,可以继续链式添加
// registry.addInterceptor(new AnotherInterceptor()).addPathPatterns("/api/v2/**");
}
}
3. 示例 Controller
为了测试拦截器,我们提供一个模拟的登录接口和一个需要授权的业务接口。
package com.example.interceptor.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class TestController {
/**
* 登录接口,在 WebMvcConfig 中配置为排除路径 (excludePathPatterns)
* 预计:不会被 AuthInterceptor 拦截
*/
@PostMapping("/auth/login")
public ResponseEntity<Map<String, Object>> login() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "登录成功");
// 模拟返回一个Token
result.put("data", "Bearer this-is-a-valid-token-for-test");
return ResponseEntity.ok(result);
}
/**
* 业务接口,在 WebMvcConfig 中配置为拦截路径 (addPathPatterns)
* 预计:会被 AuthInterceptor 拦截,需要有效的 Authorization Header
*/
@GetMapping("/data/secret")
public ResponseEntity<Map<String, Object>> getSecretData(@RequestHeader("Authorization") String token) {
// 这里的业务逻辑只有在拦截器放行后才能执行
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "获取敏感数据成功");
result.put("data", "Secret Data for User: " + token.substring(7));
return ResponseEntity.ok(result);
}
}
4. 运行与测试
启动 Spring Boot 应用,通过 cURL 或 Postman 进行测试:
测试案例一:访问登录接口 (排除路径)
- 请求:
POST /api/auth/login - 结果: 成功返回 200。
- 日志分析:
AuthInterceptor的preHandle不会被执行,但afterCompletion仍然会在请求链的末端被执行。- 作者经验: 排除路径的拦截器,其
preHandle()、postHandle()通常不会被调用。但如果排除路径是公共资源且你只关注所有请求的最终耗时,可以在更上层的 Filter 或 Servlet 过滤器中实现,或确认 Spring MVC 版本的具体行为。
- 作者经验: 排除路径的拦截器,其
测试案例二:访问受保护接口 (缺少 Token)
- 请求:
GET /api/data/secret(不带AuthorizationHeader) - 结果: 返回 401 Unauthorized,响应体为
{"code":401,"message":"未授权或Token缺失"}。 - 日志分析:
AuthInterceptor.preHandle执行,发现token为空,返回false。- Controller 方法未执行。
AuthInterceptor.afterCompletion被执行,记录耗时,清理资源。
测试案例三:访问受保护接口 (Token 有效)
- 请求:
GET /api/data/secret(Header:Authorization: Bearer my-test-token) - 结果: 成功返回 200。
- 日志分析:
AuthInterceptor.preHandle执行,Token 校验成功,返回true。- Controller 方法执行。
AuthInterceptor.postHandle执行,设置响应头X-Processed-By: AuthInterceptor。AuthInterceptor.afterCompletion执行,记录耗时。
四、高级应用与注意事项
1. 拦截器与过滤器(Filter)的区别与选择
初学者经常混淆 Spring MVC 拦截器(Interceptor)和 Servlet 规范中的过滤器(Filter)。它们在功能上相似,但工作层次和可操作性上存在本质区别。
| 特性 | 过滤器 (Filter) | 拦截器 (Interceptor) |
|---|---|---|
| 工作层次 | Servlet 规范,在 DispatcherServlet 之前 | Spring MVC 框架,在 DispatcherServlet 之后 |
| 依赖环境 | 依赖 Servlet API | 依赖 Spring MVC 框架 |
| 可操作对象 | 仅操作原始 HttpServletRequest 和 HttpServletResponse |
除了请求/响应,还可以操作 Handler (Controller 方法) 和 ModelAndView |
| 生命周期控制 | 只有一个 doFilter() 方法 |
三个关键点:preHandle、postHandle、afterCompletion |
| 适用场景 | 处理编码、跨域(CORS)、所有请求的预处理(如包装Request对象) | 身份验证、权限校验、日志、性能监控等与 Spring MVC 业务流程强相关的操作 |
选择原则:
- Filter: 如果需要对所有请求(包括静态资源、非 Spring MVC 处理的请求)进行通用处理,或者需要修改 Request/Response 的原始内容(如包装
Request获取多次读取的 Body),选择 Filter。- Interceptor: 如果逻辑与 Spring MVC 的 Handler、Model/View 相关,或者需要利用 Spring 容器中的 Bean(Filter 默认需要手动注入),选择 Interceptor。
2. 注意事项与常见错误排查
-
ThreadLocal 内存泄漏: 在拦截器中,如果使用
ThreadLocal存储请求数据(如上例中的START_TIME),务必在afterCompletion()中调用ThreadLocal.remove()方法。因为 Spring Boot 使用线程池处理请求,线程会被复用,不清理会导致数据混乱和内存泄漏。 -
postHandle与ResponseBody: 如果 Controller 方法使用了@ResponseBody或@RestController,Spring MVC 会使用HttpMessageConverter直接将返回值写入响应体,不再进行视图渲染。此时,ModelAndView参数将为null。如果你想在postHandle中修改响应体内容,这会变得困难,通常建议在afterCompletion中进行统计或在ControllerAdvice中进行统一响应包装。 -
拦截器未生效:
- 检查一: 确保拦截器类(如
AuthInterceptor)被 Spring 容器扫描到(例如添加了@Component)。 - 检查二: 确保拦截器通过
WebMvcConfigurer的addInterceptors方法正确注册。 - 检查三: 检查
addPathPatterns和excludePathPatterns的配置是否正确,路径匹配规则(Ant 风格)是否符合预期。例如,"/api/*"只匹配/api/user,不匹配/api/v1/user,而"/api/**"则会匹配所有。
- 检查一: 确保拦截器类(如
3. 最佳实践:结合 Spring Security
在实际项目中,涉及身份认证和权限校验的复杂场景,通常会推荐使用专业的安全框架,如 Spring Security。
- Spring Security Filter Chain: Spring Security 在 Filter 级别提供了强大的认证和授权机制。
- 拦截器的定位: 在引入 Spring Security 后,拦截器的职责应退化为非安全类的横切关注点,例如:
- 操作日志审计: 记录用户访问了哪些 Controller 方法。
- A/B 测试标记: 根据用户 ID 或请求头设置 A/B 测试分组的 ThreadLocal 变量。
- 请求上下文: 将从 Token 中解析出的用户 ID 等信息放入
ThreadLocal,供 Controller 和 Service 层使用。
五、作者经验总结
经过多年的分布式系统开发经验,我总结了关于 Spring Boot 拦截器的一些核心原则和实践心得:
-
分层思想不可废弃: 拦截器只应处理“请求到达 Controller 之前和 Controller 返回 之后”的通用性问题。复杂的业务逻辑或数据库操作不应该放在拦截器中。如果权限校验涉及多表查询,应该在 Service 层实现,拦截器只负责初步的 Token 格式校验和用户身份获取。
-
ThreadLocal 的生命周期管理是重中之重: 使用
ThreadLocal存储请求级上下文信息(如上文中的START_TIME、用户 ID、链路追踪 ID),必须保证在afterCompletion()中调用remove()。这是避免线程池环境下的内存泄漏和数据串线的铁律。 -
合理规划拦截路径:
addPathPatterns要尽量精确,避免拦截不必要的请求(如健康检查接口/actuator、静态资源等)。过度拦截会增加请求处理的负担。 -
异常处理链: 拦截器中的异常处理与 Spring Boot 的统一异常处理(
@ControllerAdvice)是两个层面的事。preHandle中抛出的异常,会直接被 Spring 的异常处理机制捕获。- Controller 中抛出的异常,可以在
afterCompletion中通过Exception ex参数获取,进行最后的日志记录和清理。 - 在
preHandle中手动设置响应(response.getWriter().write(...))并返回false是中断请求的标准做法,但它绕过了 Spring MVC 的正常异常流程,需要确保返回格式与应用其他接口保持一致。
-
拦截器的可读性: 拦截器代码要力求简洁,每个拦截器只做一件事(单一职责原则)。如果既要做权限,又要做日志,建议拆分成两个拦截器,通过
WebMvcConfig配置它们的顺序。
| 拦截器类型 | 核心职责 |
|---|---|
AuthInterceptor |
身份认证,Token 解析与校验 |
LogInterceptor |
请求日志记录,耗时统计 |
ContextInterceptor |
提取 Request Header 中的链路 ID 等,放入 ThreadLocal |
遵循这些原则,能确保你的 Spring Boot 应用的拦截器设计既高效又稳健。
附录:路径匹配规则
Spring Boot 拦截器使用的路径匹配规则是 Ant 风格(Ant-style path patterns),主要通配符如下:
| 符号 | 描述 | 示例 | 匹配示例 | 不匹配示例 |
|---|---|---|---|---|
? |
匹配一个字符 | /user/? |
/user/a |
/user/api |
* |
匹配零个或多个字符 | /api/* |
/api/user |
/api/v1/user |
** |
匹配零个或多个目录 | /api/** |
/api/user, /api/v1/user |
N/A |
{name} |
匹配 URI 路径段,并将其作为路径变量 | /users/{id} |
/users/123 |
N/A |
在使用 addPathPatterns 和 excludePathPatterns 时,要特别注意 /api/* 和 /api/** 的区别,这是初学者最常犯错的地方。