最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入解析Spring MVC中攔截器Interceptor的實現(xiàn)原理和應(yīng)用場景

 更新時間:2025年12月11日 09:05:49   作者:李少兄  
在 Spring 生態(tài)中,攔截器(Interceptor) 是實現(xiàn)上述橫切關(guān)注點(Cross-Cutting Concerns)的標(biāo)準(zhǔn)機(jī)制之一,它作為 Spring MVC 的核心組件,提供了對 Controller 層請求的精細(xì)化控制能力,下面小編就和大家深入介紹一下吧

前言

在構(gòu)建企業(yè)級 Web 應(yīng)用時,我們常常需要在請求到達(dá)業(yè)務(wù)邏輯層之前或之后執(zhí)行一些通用邏輯,例如:

  • 用戶身份認(rèn)證與權(quán)限校驗
  • 請求/響應(yīng)日志記錄
  • 接口防刷與限流
  • 多租戶上下文設(shè)置
  • 性能監(jiān)控與耗時統(tǒng)計

在 Spring 生態(tài)中,攔截器(Interceptor) 是實現(xiàn)上述橫切關(guān)注點(Cross-Cutting Concerns)的標(biāo)準(zhǔn)機(jī)制之一。它作為 Spring MVC 的核心組件,提供了對 Controller 層請求的精細(xì)化控制能力。

一、攔截器的本質(zhì)與定位

1.1 什么是 HandlerInterceptor

HandlerInterceptor 是 Spring Framework 提供的一個接口,用于在 DispatcherServlet 處理請求的流程中插入自定義邏輯。其作用范圍限定于 Spring MVC 的 Handler(即 @Controller 方法),不作用于靜態(tài)資源、錯誤頁面或非 Spring 管理的 Servlet 請求。

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 中斷請求
    }

    // 2. Controller 方法執(zhí)行后、視圖渲染前調(diào)用
    default void postHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            @Nullable ModelAndView modelAndView) throws Exception {
    }

    // 3. 整個請求完成(包括視圖渲染)后調(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 請求(經(jīng) DispatcherServlet 路由)所有 Web 請求(包括靜態(tài)資源、JSP、錯誤頁等)
訪問能力可獲取 HandlerMethod、方法注解、參數(shù)等僅能訪問原始 HttpServletRequest/Response
執(zhí)行時機(jī)在 Filter 之后,在 Controller 之前最先執(zhí)行(在 Spring 上下文初始化前)
異常處理afterCompletion 可捕獲未處理異常無法感知 Spring 層異常

選型建議

  • 需要訪問 Spring Bean、Controller 方法元數(shù)據(jù) → Interceptor
  • 需要處理編碼、安全頭、全局 CORS、壓縮等底層 HTTP 行為 → Filter

二、攔截器的生命周期詳解

Spring MVC 請求處理流程中,攔截器的三個方法按以下順序執(zhí)行:

[Filter Chain] 
    → [Interceptor1.preHandle] 
    → [Interceptor2.preHandle] 
    → [Controller Method] 
    → [Interceptor2.postHandle] 
    → [Interceptor1.postHandle] 
    → [View Rendering] 
    → [Interceptor2.afterCompletion] 
    → [Interceptor1.afterCompletion]

2.1preHandle:前置處理

執(zhí)行時機(jī):DispatcherServlet 已確定目標(biāo) Handler(Controller 方法),但尚未調(diào)用。

返回值語義

  • true:繼續(xù)執(zhí)行后續(xù)攔截器或 Controller
  • false中斷請求鏈,不再調(diào)用 Controller 和后續(xù)攔截器的 preHandle

注意事項

  • 即使返回 false,只要該方法被成功調(diào)用過,其對應(yīng)的 afterCompletion 仍會執(zhí)行(用于資源清理)
  • 此階段可修改 request/response,如重定向、寫入 JSON 錯誤

2.2postHandle:后置處理

執(zhí)行時機(jī):Controller 方法已執(zhí)行完畢,但視圖尚未渲染(ModelAndView 可修改)

限制條件

  • 若 Controller 拋出未被捕獲的異常,此方法不會執(zhí)行
  • 對于 @RestController 返回 ResponseEntity@ResponseBodymodelAndViewnull,但方法仍會調(diào)用

典型用途

  • 向所有頁面統(tǒng)一添加公共數(shù)據(jù)(如用戶信息、時間戳)
  • 修改視圖名稱或模型屬性

2.3afterCompletion:完成回調(diào)

執(zhí)行時機(jī):整個請求處理完成(包括視圖渲染),無論成功或失敗

關(guān)鍵參數(shù)Exception ex —— 若請求過程中發(fā)生未處理異常,此處可捕獲

強制要求

  • 必須在此方法中清理 ThreadLocal 變量,防止內(nèi)存泄漏
  • 適合做最終日志記錄、連接關(guān)閉、指標(biāo)上報等收尾工作

重要原則afterCompletion 的調(diào)用前提是對應(yīng)的 preHandle 成功返回(無論 true/false),且未拋出異常。

三、編寫自定義攔截器的完整步驟

步驟 1:實現(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)證與請求日志攔截器
 * <p>
 * 功能:
 * 1. 記錄請求入口日志(含 Controller 類/方法名)
 * 2. 校驗用戶登錄狀態(tài)(Session-based)
 * 3. 統(tǒng)計請求耗時并記錄出口日志
 * 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);
        }

        // === 登錄狀態(tài)校驗 ===
        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 頁面添加服務(wù)器時間
        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();
    }

    /**
     * 處理未認(rèn)證請求
     */
    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 區(qū)分 Controller 與靜態(tài)資源
  • 支持 AJAX 與普通請求的差異化未登錄響應(yīng)
  • finally 塊中 remove() ThreadLocal,杜絕內(nèi)存泄漏
  • 日志包含 IP、URI、Controller 信息,便于追蹤

步驟 2:注冊攔截器(實現(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)先級(數(shù)值越小,優(yōu)先級越高)
                .order(0);
    }
}

路徑匹配規(guī)則說明(Ant 風(fēng)格):

模式匹配示例不匹配示例
/api/**/api/user, /api/user/123
/admin/*/admin/dashboard/admin/user/profile
/public/*.html/public/index.html/public/css/style.css

最佳實踐

  • 明確列出需保護(hù)的路徑,而非“攔截所有再排除”
  • 將登錄、注冊、靜態(tài)資源、健康檢查等路徑顯式排除
  • 使用 .order(n) 控制多個攔截器的執(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)注冊多個攔截器時,其執(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() → 返回 true
  • loggingInterceptor.preHandle() → 返回 true
  • Controller 執(zhí)行
  • loggingInterceptor.postHandle()
  • authInterceptor.postHandle()
  • 視圖渲染
  • loggingInterceptor.afterCompletion()
  • authInterceptor.afterCompletion()

記憶口訣preHandle 正序,postHandle/afterCompletion 倒序

五、典型應(yīng)用場景

場景 1:基于 Session 的登錄校驗(如上文示例)

場景 2:JWT Token 驗證(無狀態(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;
}

場景 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:多租戶上下文設(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?
  • 是否實現(xiàn)了 WebMvcConfigurer
  • 攔截路徑是否被 excludePathPatterns 覆蓋?
  • 請求是否為靜態(tài)資源(默認(rèn)不走攔截器)?
  • Spring Boot 是否啟用了 Web MVC?(確保有 @SpringBootApplication

問題 2:postHandle未執(zhí)行

可能原因

  • Controller 拋出未被捕獲的異常
  • 使用了 @ControllerAdvice 全局異常處理,但未 rethrow 異常
  • 請求被 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 測試 API
  • 開啟 DEBUG 日志:logging.level.org.springframework.web=DEBUG

七、Spring Boot 3 兼容性說明

  • 包名變更javax.servletjakarta.servlet
  • 路徑匹配器:默認(rèn)使用 PathPattern(性能優(yōu)于 AntPathMatcher),但行為一致
  • Thymeleaf:需使用 spring-boot-starter-thymeleaf 3.x

本文所有代碼均兼容 Spring Boot 3.x(Jakarta EE 9+)

八、最佳實踐

攔截器使用原則

  • 職責(zé)單一:每個攔截器只做一件事(如認(rèn)證、日志、限流)
  • 避免阻塞preHandle 中不要執(zhí)行耗時 I/O 操作
  • 資源清理ThreadLocal 必須在 afterCompletionremove()
  • 路徑明確:使用 addPathPatterns + excludePathPatterns 精確控制范圍
  • 異常安全afterCompletion 必須處理 ex != null 的情況

避免的反模式

  • 在攔截器中直接操作數(shù)據(jù)庫(應(yīng)調(diào)用 Service)
  • 忽略 AJAX 與普通請求的響應(yīng)差異
  • 忘記排除 Swagger、Actuator、靜態(tài)資源路徑
  • postHandle 中假設(shè) modelAndView 非空

附錄:完整項目結(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的實現(xiàn)原理和應(yīng)用場景的文章就介紹到這了,更多相關(guān)Spring MVC攔截器Interceptor內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • springboot項目整合mybatis并配置mybatis中間件的實現(xiàn)

    springboot項目整合mybatis并配置mybatis中間件的實現(xiàn)

    這篇文章主要介紹了springboot項目整合mybatis并配置mybatis中間件的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • SpringMVC 異常處理機(jī)制與自定義異常處理方式

    SpringMVC 異常處理機(jī)制與自定義異常處理方式

    這篇文章主要介紹了SpringMVC 異常處理機(jī)制與自定義異常處理方式,具有很好的開車價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • Spring Cloud下實現(xiàn)用戶鑒權(quán)的方案

    Spring Cloud下實現(xiàn)用戶鑒權(quán)的方案

    Java下常用的安全框架主要有Spring Security和shiro,都可提供非常強大的功能,但學(xué)習(xí)成本較高。但在微服務(wù)下鑒權(quán)又會對服務(wù)有一定的入侵性。 因此,本文將介紹Spring Cloud下實現(xiàn)用戶鑒權(quán)的方案,感興趣的同學(xué)可以關(guān)注一下
    2021-11-11
  • Spring Boot產(chǎn)生環(huán)形注入的解決方案

    Spring Boot產(chǎn)生環(huán)形注入的解決方案

    這篇文章主要介紹了Spring Boot產(chǎn)生環(huán)形注入的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • SpringBoot啟用GZIP壓縮的代碼工程

    SpringBoot啟用GZIP壓縮的代碼工程

    經(jīng)常我們都會與服務(wù)端進(jìn)行大數(shù)據(jù)量的文本傳輸,例如?JSON?就是常見的一種格式,通過?REST?API?接口進(jìn)行?GET?和?POST?請求,可能會有大量的文本格式數(shù)據(jù)提交、返回,壓縮和解壓在提升網(wǎng)絡(luò)帶寬的同時,會帶來?CPU?資源的損耗,本文介紹了SpringBoot啟用GZIP壓縮的代碼工程
    2024-08-08
  • Java中的內(nèi)部類你了解嗎

    Java中的內(nèi)部類你了解嗎

    這篇文章主要為大家介紹了Java的內(nèi)部類,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-01-01
  • mac系統(tǒng)如何下載和安裝jdk的三種方式

    mac系統(tǒng)如何下載和安裝jdk的三種方式

    這篇文章主要介紹了mac系統(tǒng)下載和安裝jdk的三種方式,每種方法都有其特點,如安裝來源及可靠性、安裝過程及便捷性、版本管理及切換、環(huán)境配置、jdk默認(rèn)安裝路徑以及占用空間及資源管理等,需要的朋友可以參考下
    2025-01-01
  • Java實現(xiàn)從Word文檔中提取文本和圖像

    Java實現(xiàn)從Word文檔中提取文本和圖像

    在 Java 開發(fā)項目中,處理 Microsoft Word 文檔是常見場景,例如,內(nèi)容管理系統(tǒng)需要批量解析文檔文本用于搜索索引,數(shù)據(jù)遷移工具需分離圖像以便獨立存儲,本文介紹如何借助 Spire.Doc for Java 庫實現(xiàn)這一功能,需要的朋友可以參考下
    2026-03-03
  • 23種設(shè)計模式(18)java備忘錄模式

    23種設(shè)計模式(18)java備忘錄模式

    這篇文章主要為大家詳細(xì)介紹了23種設(shè)計模式之java備忘錄模式,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • Springboot集成Minio實現(xiàn)文件上傳基本步驟

    Springboot集成Minio實現(xiàn)文件上傳基本步驟

    這篇文章主要介紹了Springboot集成Minio實現(xiàn)文件上傳基本步驟,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2025-06-06

最新評論

吴江市| 连云港市| 平山县| 启东市| 锡林浩特市| 滨海县| 寻乌县| 襄垣县| 长白| 都江堰市| 龙山县| 福清市| 天气| 广灵县| 八宿县| 沾化县| 通城县| 青田县| 三原县| 光泽县| 两当县| 龙口市| 定日县| 大英县| 长沙县| 新营市| 高青县| 大姚县| 南漳县| 南投市| 肇州县| 观塘区| 陈巴尔虎旗| 沈阳市| 西华县| 彝良县| 上杭县| 溆浦县| 绥江县| 墨竹工卡县| 古丈县|