Spring MVC 拦截器

Spring MVC 拦截器

拦截器是 Spring MVC 中强大的控件,它可以在进入处理器之前做一些操作,或者在处理器完成后进行操作,甚至是在渲染视图后进行操作。Spring MVC 会在启动期间就通过 @RequestMapping 的注解解析 URI 和处理器的对应关系,在运行的时候通过请求找到对应的 HandlerMapping,然后构建 HandlerExecutionChain 对象,它是一个执行的责任链对象。

对于拦截器所需要关注的有两点:

  1. 它有哪些方法,方法的含义是什么;
  2. 各个方法在流程中执行的顺序是如何。

拦截器的定义

Spring 要求处理器的拦截器都要实现 org.springframework.web.servlet.HandlerInterceptor 接口,这个接口定义了三个方法:

public interface HandlerInterceptor {  

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  
            throws Exception {  

        return true;  
    }  

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,  
            @Nullable ModelAndView modelAndView) throws Exception {  
    }  

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,  
            @Nullable Exception ex) throws Exception {  
    }  

}

三个方法的意义

  1. preHandler 方法:在处理器之前执行的前置方法,Spring MVC 可在进入处理器前通过该方法处理一些逻辑。它返回一个 boolean 值,会影响后续流程(返回 true 则继续执行,返回 false 则终止后续流程)。
  2. postHandler 方法:在处理器之后执行的后置方法,处理器的核心逻辑完成后运行。
  3. afterCompletion 方法:无论是否产生异常,都会在渲染视图后执行的方法,常用于资源清理等操作。

说明:在新版本中,这三个方法被定义为默认方法。因为实际开发中可能只需要实现其中某一个回调方法,老版本中为了简化这种场景,Spring 提供了 HandlerInterceptorAdapter 适配器(适配器设计模式的实现),供开发者选择性继承。

拦截器的执行流程

拦截器可能配置多个,先介绍单个拦截器的执行流程,后续再说明多个拦截器的场景:

graph TD A[请求进入] --> B[执行 preHandle 方法] B --> C{preHandle 返回 true?} C -- 是 --> D[执行处理器(Handler)核心逻辑] D --> E[执行 postHandle 方法] E --> F[视图解析和渲染] F --> G[执行 afterCompletion 方法] G --> H[流程结束] C -- 否 --> H

关键说明:

  • 可在处理器执行前后、视图渲染后插入自定义逻辑;
  • 当前置方法(preHandle)返回 false 时,后续的处理器逻辑、后置方法(postHandle)、afterCompletion 方法都不会执行。

开发拦截器

下面开发一个简单的角色拦截器(测试用),可通过实现 HandlerInterceptor 接口。

角色拦截器代码

package com.intercreptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class RoleInterceptor implements HandlerInterceptor {

    /**
     * 前置方法
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 处理器对象
     * @return true 表示放行,false 表示不放行
     * @throws Exception 可能抛出的异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("RoleInterceptor 前置方法, handler:" + ((HandlerMethod) handler).getMethod().toString());
        return true; // 返回 true 放行,返回 false 终止后续流程
    }

    /**
     * 后置方法
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 处理器对象
     * @param modelAndView 模型和视图对象(可修改视图数据)
     * @throws Exception 可能抛出的异常
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        System.out.println("RoleInterceptor 后置方法, handler:" + ((HandlerMethod) handler).getMethod().toString());
        // 遍历模型中的数据
        modelAndView.getModel().forEach((k, v) -> System.out.println("key:" + k + ";value:" + v));
        // 获取逻辑视图名称
        System.out.println("获取逻辑视图名称:" + modelAndView.getViewName());
    }

    /**
     * 渲染完成方法(无论是否异常都会执行)
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 处理器对象
     * @param ex 执行过程中抛出的异常(无异常则为 null)
     * @throws Exception 可能抛出的异常
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        System.out.println("RoleInterceptor 渲染完成方法(无论异常与否), handler:" + ((HandlerMethod) handler).getMethod().toString());
        System.out.println("异常:" + ex);
    }
}

配置拦截器(Java 配置)

通过实现 WebMvcConfigurer 接口的 addInterceptors 方法配置拦截器,同时可配置静态资源、跨域、视图解析器等相关参数:

package com.config;

import com.converter.String2Position;
import com.intercreptor.RoleInterceptor;
import org.hibernate.validator.HibernateValidator;
import org.jspecify.annotations.Nullable;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.format.FormatterRegistry;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * Spring 为了方便 Spring MVC 相关配置,提供了 WebMvcConfigurer 接口
 */
@Configuration
@ComponentScan(basePackages = "com.controller") // 扫描控制器所在包
@EnableWebMvc // 以注解方式驱动 Spring MVC(包含注解映射的 HandlerMapping、HandlerAdapter 等组件)
public class ServletConfig implements WebMvcConfigurer {
    private final MessageSource messageSource;

    public ServletConfig(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * 配置静态资源不被拦截(css、js、图片、视频等)
     * @param registry 资源处理器注册器
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**") // 静态资源的访问路径(根路径下所有请求)
                .addResourceLocations("/public/"); // 静态资源实际存放目录(访问时无需拼接 public 路径)
    }

    /**
     * 配置全局跨域(局部跨域可使用 @CrossOrigin 注解)
     * @param registry 跨域注册器
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 允许所有请求跨域
                .allowedOrigins("http://localhost:5173") // 允许的跨域来源(需携带 Cookie 时,不可设为 *)
                .allowedHeaders("*") // 允许所有请求头
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD") // 允许的请求方式
                .allowCredentials(true) // 允许携带 Cookie
                .maxAge(3600); // 跨域缓存有效时间(1小时)
    }

    /**
     * 配置视图解析器
     * @param registry 视图解析器注册器
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(
                new InternalResourceViewResolver("/WEB-INF/pages/", ".jsp") // 视图前缀和后缀
        );
    }

    /**
     * 配置拦截器
     * @param registry 拦截器注册器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RoleInterceptor()) // 注册自定义拦截器
                .addPathPatterns("/staff/**"); // 指定拦截的请求路径(/staff/ 下所有请求,默认拦截所有)
    }
}

多个拦截器的执行顺序

当配置多个拦截器时,执行顺序遵循「责任链模式」,具体分为两种情况:所有前置方法返回 true、某一个前置方法返回 false。

准备多个拦截器

创建三个简单的拦截器,仅打印执行日志:

拦截器 1

public class RoleInterceptor1 implements HandlerInterceptor {  

    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        System.out.println("preHandle1");  
        return true;  
    }  

    @Override  
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {  
        System.out.println("postHandle1");  
    }  

    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
        System.out.println("afterCompletion1");  
    }  
}  

拦截器 2

public class RoleInterceptor2 implements HandlerInterceptor {  

    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        System.out.println("preHandle2");  
        return true;  
    }  

    @Override  
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {  
        System.out.println("postHandle2");  
    }  

    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
        System.out.println("afterCompletion2");  
    }  
}  

拦截器 3

public class RoleInterceptor3 implements HandlerInterceptor {  

    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        System.out.println("preHandle3");  
        return true;  
    }  

    @Override  
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {  
        System.out.println("postHandle3");  
    }  

    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
        System.out.println("afterCompletion3");  
    }  
}  

配置多个拦截器

ServletConfigaddInterceptors 方法中按顺序注册三个拦截器:

@Override  
public void addInterceptors(InterceptorRegistry registry) {  
    registry.addInterceptor(new RoleInterceptor1()).addPathPatterns("/control/**");  
    registry.addInterceptor(new RoleInterceptor2()).addPathPatterns("/control/**");  
    registry.addInterceptor(new RoleInterceptor3()).addPathPatterns("/control/**");  
}  

所有 preHandle 都返回 true 的执行结果

执行流程及日志输出如下:

preHandle1
preHandle2
preHandle3
......控制器逻辑日志打印.....
postHandle3
postHandle2
postHandle1
...........
afterCompletion3
afterCompletion2
afterCompletion1

规律总结:

  • preHandle 方法:按拦截器注册顺序执行(先注册先执行);
  • postHandle 方法:按拦截器注册逆序执行(后注册先执行);
  • afterCompletion 方法:按拦截器注册逆序执行(后注册先执行)。

某一个 preHandle 返回 false 的执行结果

修改 RoleInterceptor2 的 preHandle 方法,使其返回 false:

@Override  
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
    System.out.println("preHandle2");  
    return false; // 返回 false
}  

执行结果如下:

preHandle1
preHandle2
afterCompletion1

规律总结:

  • 当某个拦截器的 preHandle 返回 false 时,后续的所有拦截器的 preHandle 方法都不会执行;
  • 控制器核心逻辑、后续拦截器的 postHandle 方法也不会执行;
  • 仅会执行当前拦截器之前已执行过 preHandle 方法的拦截器的 afterCompletion 方法(按注册逆序)。

图解:
image

拦截器的应用

如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如apache都具有这个功能,但此处我们演示一下使用拦截器怎么实现。

实现分析:

1、在进入处理器之前记录开始时间,即在拦截器的preHandle记录开始时间;
2、在结束请求处理之后记录结束时间,即在拦截器的afterCompletion记录结束实现,并用结束时间-开始时间得到这次请求的处理时间。

问题:

我们的拦截器是单例,因此不管用户请求多少次都只有一个拦截器实现,即线程不安全,那我们应该怎么记录时间呢?
解决方案是使用ThreadLocal,它是线程绑定的变量,提供线程局部变量(一个线程一个ThreadLocal,A线程的ThreadLocal只能看到A线程的ThreadLocal,不能看到B线程的ThreadLocal)。

package com.intercreptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.springframework.core.NamedThreadLocal;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
public class StopWatchInterceptor implements HandlerInterceptor {
    // 线程局部变量,拦截器是单例形式存在,所以每一个请求的开始时间应该绑定在当前线程上
    // NameThreadLocal 与 ThreadLocal 的区别:NameThreadLocal 可以为线程绑定变量,且变量名可以自定义,而 ThreadLocal 只能绑定变量
    private static NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<Long>("StopWatch-StartTime");
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 处理器执行之前获取系统当前时间(开始时间)
        var begin = System.currentTimeMillis();
        startTimeThreadLocal.set(begin); //线程绑定变量(该数据只有当前请求的线程可见)
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        // 处理器执行之后获取系统当前时间(结束时间)
        var end = System.currentTimeMillis();
        var begin = startTimeThreadLocal.get();
        log.info("请求耗时:{}ms", end - begin);
        startTimeThreadLocal.remove(); // 移除线程变量
    }
}

同样需要配置该拦截器,与上面一致。

posted @ 2025-12-29 11:12  Jing61  阅读(4)  评论(0)    收藏  举报