Spring Boot 拦截器(Interceptor)与实战:原理、配置与最佳实践

🚀 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 是接收请求的核心前端控制器。它的基本流程如下:

  1. 接收请求。
  2. 通过 HandlerMapping 查找能够处理该请求的 Handler(通常是 Controller 中的方法)。
  3. 如果找到了 Handler,DispatcherServlet 会查找所有适用的 HandlerInterceptor(拦截器链)。
  4. 请求依次经过拦截器链的 preHandle() 方法。
  5. 如果所有 preHandle() 都返回 true,请求进入 Handler(Controller)执行。
  6. Handler 执行完毕后,请求返回,依次经过拦截器链的 postHandle() 方法(注意:顺序与 preHandle 相反)。
  7. 视图渲染完毕或响应提交后,请求再次依次经过拦截器链的 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 执行过程中抛出了异常,可以在此方法中获取并进行统一的异常处理、资源清理等。
    • 场景: 资源清理、日志记录最终结果(如耗时统计)。

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)执行。
  • postHandleafterCompletion 按照配置的逆序(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。
  • 日志分析: AuthInterceptorpreHandle 不会被执行,但 afterCompletion 仍然会在请求链的末端被执行。
    • 作者经验: 排除路径的拦截器,其 preHandle()postHandle() 通常不会被调用。但如果排除路径是公共资源且你只关注所有请求的最终耗时,可以在更上层的 Filter 或 Servlet 过滤器中实现,或确认 Spring MVC 版本的具体行为。

测试案例二:访问受保护接口 (缺少 Token)

  • 请求: GET /api/data/secret (不带 Authorization Header)
  • 结果: 返回 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 框架
可操作对象 仅操作原始 HttpServletRequestHttpServletResponse 除了请求/响应,还可以操作 Handler (Controller 方法) 和 ModelAndView
生命周期控制 只有一个 doFilter() 方法 三个关键点:preHandlepostHandleafterCompletion
适用场景 处理编码、跨域(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 使用线程池处理请求,线程会被复用,不清理会导致数据混乱和内存泄漏。

  • postHandleResponseBody 如果 Controller 方法使用了 @ResponseBody@RestController,Spring MVC 会使用 HttpMessageConverter 直接将返回值写入响应体,不再进行视图渲染。此时,ModelAndView 参数将为 null。如果你想在 postHandle 中修改响应体内容,这会变得困难,通常建议在 afterCompletion 中进行统计或在 ControllerAdvice 中进行统一响应包装。

  • 拦截器未生效:

    • 检查一: 确保拦截器类(如 AuthInterceptor)被 Spring 容器扫描到(例如添加了 @Component)。
    • 检查二: 确保拦截器通过 WebMvcConfigureraddInterceptors 方法正确注册。
    • 检查三: 检查 addPathPatternsexcludePathPatterns 的配置是否正确,路径匹配规则(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 拦截器的一些核心原则和实践心得:

  1. 分层思想不可废弃: 拦截器只应处理“请求到达 Controller 之前和 Controller 返回 之后”的通用性问题。复杂的业务逻辑或数据库操作不应该放在拦截器中。如果权限校验涉及多表查询,应该在 Service 层实现,拦截器只负责初步的 Token 格式校验和用户身份获取。

  2. ThreadLocal 的生命周期管理是重中之重: 使用 ThreadLocal 存储请求级上下文信息(如上文中的 START_TIME、用户 ID、链路追踪 ID),必须保证在 afterCompletion() 中调用 remove()。这是避免线程池环境下的内存泄漏和数据串线的铁律。

  3. 合理规划拦截路径: addPathPatterns 要尽量精确,避免拦截不必要的请求(如健康检查接口 /actuator、静态资源等)。过度拦截会增加请求处理的负担。

  4. 异常处理链: 拦截器中的异常处理与 Spring Boot 的统一异常处理(@ControllerAdvice)是两个层面的事。

    • preHandle 中抛出的异常,会直接被 Spring 的异常处理机制捕获。
    • Controller 中抛出的异常,可以在 afterCompletion 中通过 Exception ex 参数获取,进行最后的日志记录和清理。
    • preHandle 中手动设置响应(response.getWriter().write(...))并返回 false 是中断请求的标准做法,但它绕过了 Spring MVC 的正常异常流程,需要确保返回格式与应用其他接口保持一致。
  5. 拦截器的可读性: 拦截器代码要力求简洁,每个拦截器只做一件事(单一职责原则)。如果既要做权限,又要做日志,建议拆分成两个拦截器,通过 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

在使用 addPathPatternsexcludePathPatterns 时,要特别注意 /api/*/api/** 的区别,这是初学者最常犯错的地方。


正文到此结束
评论插件初始化中...
Loading...
本文目录