【springboot拦截器】深入探讨

在Spring Boot应用程序中,处理HTTP请求是核心功能。除了Controller负责具体的业务逻辑处理外,我们经常需要在请求到达Controller之前、Controller处理之后、或者整个请求处理完成后执行一些横切关注点(Cross-cutting Concerns),比如用户身份验证、权限检查、日志记录、性能监控等。Spring MVC(以及Spring Boot对其的自动化配置)提供了多种机制来实现这些目的,其中拦截器(Interceptor)就是一种非常强大且灵活的工具。

拦截器是什么?(What is it?)

Spring Boot中的拦截器实际上是基于Spring MVC的HandlerInterceptor接口实现的。它允许你在请求被分发到对应的Handler(通常是Controller的方法)之前、之后,以及视图渲染之后执行自定义逻辑。

一个拦截器定义了三个核心方法:

  • preHandle(HttpServletRequest request, HttpServletResponse response, Object handler):

    这个方法在Controller方法执行之前被调用。它是进行预处理检查的最佳位置,例如身份验证、权限判断、请求参数验证等。

    • 如果此方法返回 true,则请求会继续向下执行,进入下一个拦截器(如果有)或Controller。
    • 如果此方法返回 false,则表示拦截器链被中断,后续的拦截器和Controller都不会再执行。此时,通常需要在该方法中手动处理响应(例如,发送错误码或重定向)。

    参数说明:

    • request: 当前的HttpServletRequest对象。
    • response: 当前的HttpServletResponse对象。
    • handler: 目标Controller方法封装成的HandlerMethod对象(或其他类型的Handler)。
  • postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView):

    这个方法在Controller方法成功执行之后,但在视图渲染之前被调用。它主要用于对ModelAndView对象进行操作,例如添加公共的模型数据、修改视图名称等。对于RESTful API(通常返回JSON/XML而不是视图)来说,这个方法的用途相对较少,因为此时响应体可能还未完全构建。

    参数说明:

    • request: 当前的HttpServletRequest对象。
    • response: 当前的HttpServletResponse对象。
    • handler: 目标Controller方法封装成的HandlerMethod对象。
    • modelAndView: Controller方法返回的ModelAndView对象(如果Controller返回String、View或没有返回值,则此参数可能为null;如果Controller抛出异常,此方法不会被调用)。
  • afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex):

    这个方法在整个请求处理完成之后被调用,无论Controller是否抛出异常,也无论视图是否成功渲染。它是进行资源清理、记录处理时间、记录最终响应状态等操作的理想位置。

    参数说明:

    • request: 当前的HttpServletRequest对象。
    • response: 当前的HttpServletResponse对象。
    • handler: 目标Controller方法封装成的HandlerMethod对象。
    • ex: 如果在请求处理过程中(Controller或之前的拦截器)发生了异常,此参数会包含异常信息;否则为null。

HandlerInterceptor是接口,Spring MVC还提供了一个抽象类HandlerInterceptorAdapter(在较新版本中已推荐使用接口的default方法),但直接实现接口并使用default方法更为现代。

为什么使用拦截器?(Why use it?)

使用拦截器主要为了将一些与核心业务逻辑无关但又必须执行的功能从Controller中剥离出来,实现代码的解耦和模块化。常见的使用场景包括:

  1. 统一的身份验证和权限控制:preHandle中检查用户是否登录,是否有访问某个资源的权限。如果用户未登录或没有权限,可以直接返回错误响应,而无需在每个Controller方法中重复编写检查逻辑。
  2. 请求/响应日志记录:preHandle记录请求进入时间、请求URL、参数等信息;在afterCompletion记录响应状态、处理耗时、返回数据量等信息。这对于审计、监控和问题排查非常有用。
  3. 性能监控:preHandle记录请求开始时间戳,在afterCompletion计算总耗时并记录,以便分析哪些请求处理缓慢。
  4. 数据预处理或后处理: 例如,在preHandle中对请求参数进行一些通用处理;在postHandle中修改返回的模型数据。

  5. 为请求添加通用属性: 比如为每个请求生成一个唯一的请求ID,并在整个请求生命周期中传递,方便分布式追踪和日志关联。可以在preHandle生成并放入MDC (Mapped Diagnostic Context) 或 Request Attributes。

通过使用拦截器,可以避免在Controller中散布大量重复的非业务代码,使Controller更加专注于业务逻辑,提高代码的可维护性和复用性。

拦截器应用在哪里?(Where is it applied?)

拦截器是通过配置来应用到特定的请求路径上的。你可以在配置类中指定哪些URL路径应该被某个拦截器拦截,哪些路径应该被排除。这使得拦截器的作用范围非常灵活和精确。

通常,拦截器会被注册到Spring MVC的InterceptorRegistry中,通过匹配请求的URL路径来决定是否应用。

可以有多少个拦截器?拦截器顺序是怎样的?(How many? Order?)

在一个Spring Boot应用中,你可以定义任意数量的拦截器。

当一个请求到来时,它会依次经过所有匹配该路径的拦截器链。拦截器的执行顺序取决于它们被注册到InterceptorRegistry中的顺序:

  1. preHandle方法的执行顺序: 按照拦截器被注册的顺序依次执行。如果某个拦截器的preHandle方法返回false,则整个拦截器链和Controller的执行都会中断,请求处理结束。
  2. postHandle方法的执行顺序: 按照拦截器被注册的相反顺序依次执行。也就是说,最后注册的拦截器的postHandle方法会最先执行。
  3. afterCompletion方法的执行顺序: 也按照拦截器被注册的相反顺序依次执行。无论在请求处理过程中是否发生异常,所有已执行过preHandle方法(返回true)的拦截器的afterCompletion方法都会被调用。


例如:

假设你注册了拦截器A、拦截器B,顺序是先A后B。

请求到来 -> 拦截器A的preHandle -> 拦截器B的preHandle -> Controller方法执行 -> 拦截器B的postHandle -> 拦截器A的postHandle -> 视图渲染 -> 拦截器B的afterCompletion -> 拦截器A的afterCompletion -> 请求完成。

如果在拦截器A的preHandle返回false,则B的preHandle、Controller、A和B的postHandle、A和B的afterCompletion都不会执行。

如果在拦截器B的preHandle返回false,则Controller、A和B的postHandle、A和B的afterCompletion都不会执行,但A的preHandle已经执行了。

如果在Controller执行时抛出异常,则Controller后面的B和A的postHandle不会执行,但B和A的afterCompletion会被执行(且ex参数包含异常信息)。

可以通过在注册拦截器时使用.order(int order)方法来显式控制拦截器的顺序,值越小优先级越高,越先执行preHandle

如何实现和配置一个简单的拦截器?(How to implement and configure?)

实现和配置一个拦截器通常分为两步:

  1. 创建自定义拦截器类。
  2. 创建配置类并将自定义拦截器注册到Spring MVC中。

步骤 1: 创建自定义拦截器类

创建一个类实现HandlerInterceptor接口,并覆盖你需要的方法。

示例代码: 一个简单的日志拦截器

package com.example.demo.interceptor; 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 // 声明为Spring组件,方便在配置类中注入 public class RequestLoggingInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(RequestLoggingInterceptor.class); // 请求开始时间存储在Request Attribute中 private static final String START_TIME_ATTRIBUTE = "requestStartTime"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在请求进入Controller前执行 long startTime = System.currentTimeMillis(); request.setAttribute(START_TIME_ATTRIBUTE, startTime); // 记录开始时间 logger.info("Request Start: {} {}", request.getMethod(), request.getRequestURI()); // 可以根据需要打印请求头、参数等 // 返回 true 表示继续向下执行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 在Controller执行后,视图渲染前执行 (对于RESTful API可能modelAndView为null) // logger.info("Post Handle: {}", request.getRequestURI()); // 可以在这里修改 modelAndView } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 在整个请求处理完成后执行 (无论成功或失败) long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE); long endTime = System.currentTimeMillis(); long duration = endTime - startTime; // 计算耗时 logger.info("Request End: {} {} - Status: {} - Duration: {}ms", request.getMethod(), request.getRequestURI(), response.getStatus(), duration); if (ex != null) { logger.error("Request Error: {} {} - Exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); } // 可以在这里进行资源清理等操作 } }

在上面的例子中,我们在preHandle记录了请求开始时间并打印日志;在afterCompletion计算了请求耗时、打印了状态码和最终日志,并在发生异常时记录了异常信息。

步骤 2: 创建配置类并注册拦截器

创建一个实现WebMvcConfigurer接口的配置类,并覆盖addInterceptors方法来注册你的自定义拦截器。

示例代码: 注册拦截器

package com.example.demo.config; import com.example.demo.interceptor.RequestLoggingInterceptor; 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; @Configuration // 声明为配置类 public class WebConfig implements WebMvcConfigurer { private final RequestLoggingInterceptor requestLoggingInterceptor; // 通过构造函数注入拦截器 @Autowired public WebConfig(RequestLoggingInterceptor requestLoggingInterceptor) { this.requestLoggingInterceptor = requestLoggingInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { // 注册自定义拦截器 registry.addInterceptor(requestLoggingInterceptor) // 指定拦截的路径模式,可以使用通配符 .addPathPatterns("/**") // 指定排除拦截的路径模式 (例如,静态资源、登录/注册接口等) .excludePathPatterns("/css/**", "/js/**", "/images/**", "/login", "/register"); // 如果有其他拦截器,可以继续注册 // registry.addInterceptor(anotherInterceptor).addPathPatterns("/admin/**").order(1); // 设置顺序 } }

在上面的配置中:

  • 我们使用@Configuration注解表明这是一个配置类。
  • 实现WebMvcConfigurer接口以定制Spring MVC的配置。
  • 通过构造函数注入了之前创建的RequestLoggingInterceptor(因为它被@Component标记,Spring可以管理它的生命周期)。
  • 覆盖addInterceptors方法。
  • 使用registry.addInterceptor()方法添加拦截器实例。
  • 使用.addPathPatterns("/**")指定拦截所有路径。
  • 使用.excludePathPatterns(...)指定排除某些路径,例如静态资源和特定的接口。
  • 可以通过链式调用.order(int order)来设置拦截器的优先级,越小优先级越高。

完成这两步后,启动Spring Boot应用,RequestLoggingInterceptor就会自动对匹配的请求路径生效了。

常见的使用场景举例(Common Scenarios)

场景1: 统一登录检查

在一个需要用户登录才能访问大部分资源的Web应用中,你不想在每个Controller方法里都写一遍用户是否登录的判断。

实现思路:

  1. 创建一个LoginRequiredInterceptor实现HandlerInterceptor
  2. preHandle方法中:
    • 从Session或Token中获取用户信息。
    • 判断用户是否已登录。
    • 如果未登录,设置响应状态码(如401 Unauthorized 或 302 Redirect),并返回false中断请求链。
    • 如果已登录,返回true继续执行。
  3. 创建一个配置类,注册LoginRequiredInterceptor,并使用addPathPatterns指定需要登录的路径,使用excludePathPatterns排除登录页面、注册页面等无需登录的路径。

场景2: 记录接口访问日志和耗时

需要记录系统中所有API的访问情况,包括请求路径、方法、状态码、处理时间等,以便后续分析和排查问题。

实现思路:

  1. 创建一个AccessLogInterceptor实现HandlerInterceptor(如上面示例的RequestLoggingInterceptor)。
  2. preHandle中记录请求开始时间(存储在请求属性或ThreadLocal中)。
  3. afterCompletion中获取开始时间,计算耗时,获取请求URL、方法、响应状态码、Handler信息等。
  4. 将这些信息组织成日志格式,通过日志框架输出。如果发生异常,在afterCompletionex参数中可以获取异常信息并记录。
  5. 在配置类中注册AccessLogInterceptor,通常可以应用于所有接口路径/**

场景3: 请求头或参数的通用处理

需要在所有请求中检查某个特定的请求头是否存在,或者对所有请求的某个参数进行统一格式化。

实现思路:

  1. 创建一个自定义拦截器。
  2. preHandle方法中获取请求头或参数。
  3. 执行校验或格式化逻辑。
  4. 如果校验失败,返回错误响应并返回false。如果需要修改请求参数,可能需要更复杂的技术,如包装HttpServletRequest

拦截器与异步请求(Interceptors and Async Requests)

当Controller方法使用异步处理(如返回Callable, DeferredResult, ListenableFuture等)时,拦截器的行为会稍微复杂一些。

默认情况下,当Controller返回一个异步结果时,请求主线程会立即释放,而拦截器的postHandleafterCompletion方法会在主线程释放前或异步处理完成后的不同时间点被调用。

为了正确处理异步请求,HandlerInterceptor接口提供了两个额外的默认方法:

  • afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler):

    在Controller开始异步处理时立即调用。此时,除了afterCompletion外,其他所有拦截器的preHandlepostHandle都已执行完毕(或被跳过)。
  • afterAllConcurrentHandlingComplete(HttpServletRequest request, HttpServletResponse response, Object handler):

    在异步处理完成并生成响应后,afterCompletion方法被调用之前执行。如果异步处理过程中发生异常,也会调用此方法。

对于异步请求的全面拦截,你可能需要结合使用HandlerInterceptorCallableProcessingInterceptorDeferredResultProcessingInterceptor。但在大多数常见场景下(如日志记录、权限检查),依赖preHandleafterCompletion已足够。afterCompletion保证了无论同步还是异步,无论成功还是失败,它总会在请求处理的最后被调用,是清理资源和最终日志记录的好地方。

总结

Spring Boot中的拦截器提供了一种优雅的方式来处理Web请求的横切关注点。通过实现HandlerInterceptor接口并在配置类中进行注册和路径匹配,可以轻松地在请求处理的不同阶段插入自定义逻辑,从而提高代码的组织性、可维护性和复用性。无论是基础的登录认证,还是复杂的日志和性能监控,拦截器都是实现这些功能的强大工具。

springboot拦截器