深入解析Spring MVC中拦截器Interceptor的实现原理和应用场景

 更新时间:2025年12月11日 09:05:49   作者:李少兄  
在 Spring 生态中,拦截器(Interceptor) 是实现上述横切关注点(Cross-Cutting Concerns)的标准机制之一,它作为 Spring MVC 的核心组件,提供了对 Controller 层请求的精细化控制能力,下面小编就和大家深入介绍一下吧

前言

在构建企业级 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:继续执行后续拦截器或 Controller
  • false中断请求链,不再调用 Controller 和后续拦截器的 preHandle

注意事项

  • 即使返回 false,只要该方法被成功调用过,其对应的 afterCompletion 仍会执行(用于资源清理)
  • 此阶段可修改 request/response,如重定向、写入 JSON 错误

2.2postHandle:后置处理

执行时机:Controller 方法已执行完毕,但视图尚未渲染(ModelAndView 可修改)

限制条件

  • 若 Controller 抛出未被捕获的异常,此方法不会执行
  • 对于 @RestController 返回 ResponseEntity@ResponseBodymodelAndViewnull,但方法仍会调用

典型用途

  • 向所有页面统一添加公共数据(如用户信息、时间戳)
  • 修改视图名称或模型属性

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() → 返回 true
  • loggingInterceptor.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.servletjakarta.servlet
  • 路径匹配器:默认使用 PathPattern(性能优于 AntPathMatcher),但行为一致
  • Thymeleaf:需使用 spring-boot-starter-thymeleaf 3.x

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

八、最佳实践

拦截器使用原则

  • 职责单一:每个拦截器只做一件事(如认证、日志、限流)
  • 避免阻塞preHandle 中不要执行耗时 I/O 操作
  • 资源清理ThreadLocal 必须在 afterCompletionremove()
  • 路径明确:使用 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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅谈MyBatis循环Map(高级用法)

    浅谈MyBatis循环Map(高级用法)

    这篇文章主要介绍了浅谈MyBatis循环Map(高级用法),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • java.lang.Runtime.exec() Payload知识点详解

    java.lang.Runtime.exec() Payload知识点详解

    在本篇文章里小编给大家整理的是一篇关于java.lang.Runtime.exec() Payload知识点相关内容,有兴趣的朋友们学习下。
    2020-03-03
  • Java基础之颜色工具类(超详细注释)

    Java基础之颜色工具类(超详细注释)

    这篇文章主要介绍了Java基础之颜色工具类(超详细注释),文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • 新手小白入门必学JAVA面向对象之多态

    新手小白入门必学JAVA面向对象之多态

    说到多态,一定离不开其它两大特性:封装和继承,下面这篇文章主要给大家介绍了关于新手小白入门必学JAVA面向对象之多态的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-02-02
  • Java字符串技巧之删除标点或最后字符的方法

    Java字符串技巧之删除标点或最后字符的方法

    这篇文章主要介绍了Java字符串技巧之删除标点或最后字符的方法,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-11-11
  • Java多线程之线程状态详解

    Java多线程之线程状态详解

    这篇文章主要介绍了Java多线程 线程状态原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-09-09
  • Java servlet后端开发超详细教程

    Java servlet后端开发超详细教程

    Servlet指在服务器端执行的一段Java代码,可以接收用户的请求和返回给用户响应结果,下面这篇文章主要给大家介绍了关于Java.servlet生命周期的相关资料,需要的朋友可以参考下
    2023-02-02
  • 比较java中Future与FutureTask之间的关系

    比较java中Future与FutureTask之间的关系

    在本篇文章里我们给大家分享了java中Future与FutureTask之间的关系的内容,有需要的朋友们可以跟着学习下。
    2018-10-10
  • java基于UDP实现图片群发功能

    java基于UDP实现图片群发功能

    这篇文章主要为大家详细介绍了java基于UDP实现图片群发功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • Java中Collection集合常用API之 Collection存储自定义类型对象的示例代码

    Java中Collection集合常用API之 Collection存储自定义类型对象的示例代码

    Collection是单列集合的祖宗接口,因此它的功能是全部单列集合都可以继承使用的,这篇文章主要介绍了Java中Collection集合常用API - Collection存储自定义类型对象,需要的朋友可以参考下
    2022-12-12

最新评论