深入解析Spring MVC中攔截器Interceptor的實(shí)現(xiàn)原理和應(yīng)用場(chǎng)景
前言
在構(gòu)建企業(yè)級(jí) Web 應(yīng)用時(shí),我們常常需要在請(qǐng)求到達(dá)業(yè)務(wù)邏輯層之前或之后執(zhí)行一些通用邏輯,例如:
- 用戶身份認(rèn)證與權(quán)限校驗(yàn)
- 請(qǐng)求/響應(yīng)日志記錄
- 接口防刷與限流
- 多租戶上下文設(shè)置
- 性能監(jiān)控與耗時(shí)統(tǒng)計(jì)
在 Spring 生態(tài)中,攔截器(Interceptor) 是實(shí)現(xiàn)上述橫切關(guān)注點(diǎn)(Cross-Cutting Concerns)的標(biāo)準(zhǔn)機(jī)制之一。它作為 Spring MVC 的核心組件,提供了對(duì) Controller 層請(qǐng)求的精細(xì)化控制能力。
一、攔截器的本質(zhì)與定位
1.1 什么是 HandlerInterceptor
HandlerInterceptor 是 Spring Framework 提供的一個(gè)接口,用于在 DispatcherServlet 處理請(qǐng)求的流程中插入自定義邏輯。其作用范圍限定于 Spring MVC 的 Handler(即 @Controller 方法),不作用于靜態(tài)資源、錯(cuò)誤頁(yè)面或非 Spring 管理的 Servlet 請(qǐng)求。
public interface HandlerInterceptor {
// 1. Controller 方法執(zhí)行前調(diào)用
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return true; // 返回 true 繼續(xù)執(zhí)行;false 中斷請(qǐng)求
}
// 2. Controller 方法執(zhí)行后、視圖渲染前調(diào)用
default void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 3. 整個(gè)請(qǐng)求完成(包括視圖渲染)后調(diào)用
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex) throws Exception {
}
}
1.2 攔截器 vs 過濾器(Filter)——關(guān)鍵區(qū)別
| 維度 | 攔截器(Interceptor) | 過濾器(Filter) |
|---|---|---|
| 規(guī)范歸屬 | Spring MVC 框架 | Java Servlet 規(guī)范 |
| 容器依賴 | 依賴 Spring IoC 容器(可注入 Bean) | 不依賴 Spring(原生 Servlet) |
| 作用范圍 | 僅 Controller 請(qǐng)求(經(jīng) DispatcherServlet 路由) | 所有 Web 請(qǐng)求(包括靜態(tài)資源、JSP、錯(cuò)誤頁(yè)等) |
| 訪問能力 | 可獲取 HandlerMethod、方法注解、參數(shù)等 | 僅能訪問原始 HttpServletRequest/Response |
| 執(zhí)行時(shí)機(jī) | 在 Filter 之后,在 Controller 之前 | 最先執(zhí)行(在 Spring 上下文初始化前) |
| 異常處理 | afterCompletion 可捕獲未處理異常 | 無(wú)法感知 Spring 層異常 |
選型建議:
- 需要訪問 Spring Bean、Controller 方法元數(shù)據(jù) → Interceptor
- 需要處理編碼、安全頭、全局 CORS、壓縮等底層 HTTP 行為 → Filter
二、攔截器的生命周期詳解
Spring MVC 請(qǐng)求處理流程中,攔截器的三個(gè)方法按以下順序執(zhí)行:
[Filter Chain]
→ [Interceptor1.preHandle]
→ [Interceptor2.preHandle]
→ [Controller Method]
→ [Interceptor2.postHandle]
→ [Interceptor1.postHandle]
→ [View Rendering]
→ [Interceptor2.afterCompletion]
→ [Interceptor1.afterCompletion]
2.1preHandle:前置處理
執(zhí)行時(shí)機(jī):DispatcherServlet 已確定目標(biāo) Handler(Controller 方法),但尚未調(diào)用。
返回值語(yǔ)義:
true:繼續(xù)執(zhí)行后續(xù)攔截器或 Controllerfalse:中斷請(qǐng)求鏈,不再調(diào)用 Controller 和后續(xù)攔截器的preHandle
注意事項(xiàng):
- 即使返回
false,只要該方法被成功調(diào)用過,其對(duì)應(yīng)的afterCompletion仍會(huì)執(zhí)行(用于資源清理) - 此階段可修改
request/response,如重定向、寫入 JSON 錯(cuò)誤
2.2postHandle:后置處理
執(zhí)行時(shí)機(jī):Controller 方法已執(zhí)行完畢,但視圖尚未渲染(ModelAndView 可修改)
限制條件:
- 若 Controller 拋出未被捕獲的異常,此方法不會(huì)執(zhí)行
- 對(duì)于
@RestController返回ResponseEntity或@ResponseBody,modelAndView為null,但方法仍會(huì)調(diào)用
典型用途:
- 向所有頁(yè)面統(tǒng)一添加公共數(shù)據(jù)(如用戶信息、時(shí)間戳)
- 修改視圖名稱或模型屬性
2.3afterCompletion:完成回調(diào)
執(zhí)行時(shí)機(jī):整個(gè)請(qǐng)求處理完成(包括視圖渲染),無(wú)論成功或失敗
關(guān)鍵參數(shù):Exception ex —— 若請(qǐng)求過程中發(fā)生未處理異常,此處可捕獲
強(qiáng)制要求:
- 必須在此方法中清理
ThreadLocal變量,防止內(nèi)存泄漏 - 適合做最終日志記錄、連接關(guān)閉、指標(biāo)上報(bào)等收尾工作
重要原則:afterCompletion 的調(diào)用前提是對(duì)應(yīng)的 preHandle 成功返回(無(wú)論 true/false),且未拋出異常。
三、編寫自定義攔截器的完整步驟
步驟 1:實(shí)現(xiàn) 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;
/**
* 統(tǒng)一認(rèn)證與請(qǐng)求日志攔截器
* <p>
* 功能:
* 1. 記錄請(qǐng)求入口日志(含 Controller 類/方法名)
* 2. 校驗(yàn)用戶登錄狀態(tài)(Session-based)
* 3. 統(tǒng)計(jì)請(qǐng)求耗時(shí)并記錄出口日志
* 4. 清理 ThreadLocal 資源
*/
@Component
public class AuthLogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthLogInterceptor.class);
// 使用 ThreadLocal 存儲(chǔ)請(qǐng)求開始時(shí)間(線程安全)
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);
// 識(shí)別是否為 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);
}
// === 登錄狀態(tài)校驗(yàn) ===
Object userId = request.getSession().getAttribute("user_id");
if (userId == null) {
handleUnauthenticated(request, response);
return false; // 中斷請(qǐng)求
}
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 頁(yè)面添加服務(wù)器時(shí)間
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();
}
}
/**
* 獲取客戶端真實(shí) 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();
}
/**
* 處理未認(rèn)證請(qǐng)求
*/
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 請(qǐng)求
*/
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")) ||
(request.getHeader("Accept") != null &&
request.getHeader("Accept").contains("application/json"));
}
}
代碼亮點(diǎn)說(shuō)明:
- 使用
ThreadLocal<Long>安全存儲(chǔ)請(qǐng)求開始時(shí)間 - 通過
handler instanceof HandlerMethod區(qū)分 Controller 與靜態(tài)資源 - 支持 AJAX 與普通請(qǐng)求的差異化未登錄響應(yīng)
- 在
finally塊中remove()ThreadLocal,杜絕內(nèi)存泄漏 - 日志包含 IP、URI、Controller 信息,便于追蹤
步驟 2:注冊(cè)攔截器(實(shí)現(xiàn) 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)
// 攔截需要認(rèn)證的路徑
.addPathPatterns(
"/admin/**",
"/api/v1/**",
"/user/profile",
"/order/**"
)
// 排除公開路徑
.excludePathPatterns(
"/",
"/login",
"/register",
"/public/**",
"/static/**",
"/webjars/**",
"/error",
// Swagger 文檔(開發(fā)環(huán)境)
"/swagger-ui/**",
"/v3/api-docs/**",
// Actuator 健康檢查(生產(chǎn)環(huán)境)
"/actuator/health"
)
// 設(shè)置攔截器優(yōu)先級(jí)(數(shù)值越小,優(yōu)先級(jí)越高)
.order(0);
}
}
路徑匹配規(guī)則說(shuō)明(Ant 風(fēng)格):
| 模式 | 匹配示例 | 不匹配示例 |
|---|---|---|
| /api/** | /api/user, /api/user/123 | 無(wú) |
| /admin/* | /admin/dashboard | /admin/user/profile |
| /public/*.html | /public/index.html | /public/css/style.css |
最佳實(shí)踐:
- 明確列出需保護(hù)的路徑,而非“攔截所有再排除”
- 將登錄、注冊(cè)、靜態(tài)資源、健康檢查等路徑顯式排除
- 使用
.order(n)控制多個(gè)攔截器的執(zhí)行順序
步驟 3:(可選)注入其他 Spring Bean
若攔截器需調(diào)用 Service 層邏輯(如查詢用戶權(quán)限),只需:
- 在攔截器類上添加
@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,而應(yīng)使用 @Component + @Autowired 注入到配置類中。
四、多攔截器的執(zhí)行順序與控制
當(dāng)注冊(cè)多個(gè)攔截器時(shí),其執(zhí)行順序遵循 “棧”結(jié)構(gòu):
registry.addInterceptor(loggingInterceptor).order(10); // 后執(zhí)行 preHandle,先執(zhí)行 postHandle registry.addInterceptor(authInterceptor).order(0); // 先執(zhí)行 preHandle,后執(zhí)行 postHandle
執(zhí)行流程:
authInterceptor.preHandle()→ 返回 trueloggingInterceptor.preHandle()→ 返回 true- Controller 執(zhí)行
loggingInterceptor.postHandle()authInterceptor.postHandle()- 視圖渲染
loggingInterceptor.afterCompletion()authInterceptor.afterCompletion()
記憶口訣:preHandle 正序,postHandle/afterCompletion 倒序
五、典型應(yīng)用場(chǎng)景
場(chǎng)景 1:基于 Session 的登錄校驗(yàn)(如上文示例)
場(chǎng)景 2:JWT Token 驗(yàn)證(無(wú)狀態(tài)認(rèn)證)
@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;
}
場(chǎng)景 3:接口防刷(Redis + 滑動(dòng)窗口)
@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;
}
場(chǎng)景 4:多租戶上下文設(shè)置
@Override
public boolean preHandle(...) {
String tenantId = resolveTenantId(request); // 從 Header/域名解析
TenantContext.setCurrentTenant(tenantId); // 存入 ThreadLocal
return true;
}
@Override
public void afterCompletion(...) {
TenantContext.clear(); // 清理
}
六、常見問題與調(diào)試技巧
問題 1:攔截器未生效
排查清單:
- 配置類是否添加
@Configuration? - 是否實(shí)現(xiàn)了
WebMvcConfigurer? - 攔截路徑是否被
excludePathPatterns覆蓋? - 請(qǐng)求是否為靜態(tài)資源(默認(rèn)不走攔截器)?
- Spring Boot 是否啟用了 Web MVC?(確保有
@SpringBootApplication)
問題 2:postHandle未執(zhí)行
可能原因:
- Controller 拋出未被捕獲的異常
- 使用了
@ControllerAdvice全局異常處理,但未 rethrow 異常 - 請(qǐng)求被 Filter 或 Security 攔截(如 Spring Security)
問題 3:如何獲取 Controller 方法上的自定義注解
if (handler instanceof HandlerMethod hm) {
MyAnnotation anno = hm.getMethodAnnotation(MyAnnotation.class);
if (anno != null) {
// 處理注解邏輯
}
}
調(diào)試建議
- 在
preHandle中打印request.getRequestURI() - 使用 Postman 或 curl 測(cè)試 API
- 開啟 DEBUG 日志:
logging.level.org.springframework.web=DEBUG
七、Spring Boot 3 兼容性說(shuō)明
- 包名變更:
javax.servlet→jakarta.servlet - 路徑匹配器:默認(rèn)使用
PathPattern(性能優(yōu)于AntPathMatcher),但行為一致 - Thymeleaf:需使用
spring-boot-starter-thymeleaf3.x
本文所有代碼均兼容 Spring Boot 3.x(Jakarta EE 9+)
八、最佳實(shí)踐
攔截器使用原則
- 職責(zé)單一:每個(gè)攔截器只做一件事(如認(rèn)證、日志、限流)
- 避免阻塞:
preHandle中不要執(zhí)行耗時(shí) I/O 操作 - 資源清理:
ThreadLocal必須在afterCompletion中remove() - 路徑明確:使用
addPathPatterns+excludePathPatterns精確控制范圍 - 異常安全:
afterCompletion必須處理ex != null的情況
避免的反模式
- 在攔截器中直接操作數(shù)據(jù)庫(kù)(應(yīng)調(diào)用 Service)
- 忽略 AJAX 與普通請(qǐng)求的響應(yīng)差異
- 忘記排除 Swagger、Actuator、靜態(tài)資源路徑
- 在
postHandle中假設(shè)modelAndView非空
附錄:完整項(xiàng)目結(jié)構(gòu)
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
到此這篇關(guān)于深入解析Spring MVC中攔截器Interceptor的實(shí)現(xiàn)原理和應(yīng)用場(chǎng)景的文章就介紹到這了,更多相關(guān)Spring MVC攔截器Interceptor內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring?Boot?Interceptor的原理、配置、順序控制及與Filter的關(guān)鍵區(qū)別對(duì)比分析
- SpringBoot使用Mybatis-Plus中分頁(yè)插件PaginationInterceptor詳解
- Spring Boot攔截器Interceptor與過濾器Filter深度解析(區(qū)別、實(shí)現(xiàn)與實(shí)戰(zhàn)指南)
- Spring Mvc中攔截器Interceptor用法解讀
- Spring Boot攔截器Interceptor與過濾器Filter詳細(xì)教程(示例詳解)
- Spring攔截器之HandlerInterceptor使用方式
- Spring的攔截器HandlerInterceptor詳解
- SpringMVC的處理器攔截器HandlerInterceptor詳解
- spring中Interceptor的使用小結(jié)
相關(guān)文章
淺談MyBatis循環(huán)Map(高級(jí)用法)
這篇文章主要介紹了淺談MyBatis循環(huán)Map(高級(jí)用法),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
java.lang.Runtime.exec() Payload知識(shí)點(diǎn)詳解
在本篇文章里小編給大家整理的是一篇關(guān)于java.lang.Runtime.exec() Payload知識(shí)點(diǎn)相關(guān)內(nèi)容,有興趣的朋友們學(xué)習(xí)下。2020-03-03
新手小白入門必學(xué)JAVA面向?qū)ο笾鄳B(tài)
說(shuō)到多態(tài),一定離不開其它兩大特性:封裝和繼承,下面這篇文章主要給大家介紹了關(guān)于新手小白入門必學(xué)JAVA面向?qū)ο笾鄳B(tài)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02
Java字符串技巧之刪除標(biāo)點(diǎn)或最后字符的方法
這篇文章主要介紹了Java字符串技巧之刪除標(biāo)點(diǎn)或最后字符的方法,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-11-11
比較java中Future與FutureTask之間的關(guān)系
在本篇文章里我們給大家分享了java中Future與FutureTask之間的關(guān)系的內(nèi)容,有需要的朋友們可以跟著學(xué)習(xí)下。2018-10-10
java基于UDP實(shí)現(xiàn)圖片群發(fā)功能
這篇文章主要為大家詳細(xì)介紹了java基于UDP實(shí)現(xiàn)圖片群發(fā)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01
Java中Collection集合常用API之?Collection存儲(chǔ)自定義類型對(duì)象的示例代碼
Collection是單列集合的祖宗接口,因此它的功能是全部單列集合都可以繼承使用的,這篇文章主要介紹了Java中Collection集合常用API?-?Collection存儲(chǔ)自定義類型對(duì)象,需要的朋友可以參考下2022-12-12

