java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring MVC拦截器Interceptor

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

作者:李少兄

在 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 MVC 请求处理流程中,拦截器的三个方法按以下顺序执行:

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

2.1preHandle:前置处理

执行时机:DispatcherServlet 已确定目标 Handler(Controller 方法),但尚未调用。

返回值语义

注意事项

2.2postHandle:后置处理

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

限制条件

典型用途

2.3afterCompletion:完成回调

执行时机:整个请求处理完成(包括视图渲染),无论成功或失败

关键参数Exception ex —— 若请求过程中发生未处理异常,此处可捕获

强制要求

重要原则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"));
    }
}

代码亮点说明

步骤 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

最佳实践

步骤 3:(可选)注入其他 Spring Bean

若拦截器需调用 Service 层逻辑(如查询用户权限),只需:

@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

执行流程:

记忆口诀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:拦截器未生效

排查清单

问题 2:postHandle未执行

可能原因

问题 3:如何获取 Controller 方法上的自定义注解

if (handler instanceof HandlerMethod hm) {
    MyAnnotation anno = hm.getMethodAnnotation(MyAnnotation.class);
    if (anno != null) {
        // 处理注解逻辑
    }
}

调试建议

七、Spring Boot 3 兼容性说明

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

八、最佳实践

拦截器使用原则

避免的反模式

附录:完整项目结构

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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文