天天看点

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

正如标题所言,本文主要介绍两部分。

  1. 如何SpringMVC中注册拦截器的方式?多个拦截器执行的顺序是什么样的?
  2. 结合SpringMVC源码,对拦截器进行分析

SpringMVC中注册拦截器的方式

首先创建一个拦截器,然后注册到SpringMVC中即可。

第一步,创建拦截器,SpringMVC中提供两种方式,

  1. 实现接口HandlerInterceptor
  2. 继承抽象类HandlerInterceptorAdapter

2其实是基于1的,而且已经弃用了

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

我们来基于第一种方法创建拦截器。

模拟一个需求,自定义Log注解,方法上使用了注解的就记录日志,没有的就不记录日志

需要定义一个注解@Log作用在方法。拦截器只能拦截方法上的注解

/**
 * @author yujiaxing
 * @date 2021/09/28
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}
           

定义拦截器

/**
 * @author yujiaxing
 * @date 2021/09/28
 */
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler) throws Exception {
        log.info("handler type : {}", handler.getClass());  // handler type : class org.springframework.web.method.HandlerMethod
        Log logAnnotation = getLogAnnotation(handler);
        if (Objects.nonNull(logAnnotation)) {
            log.info("【日志信息】:preHandle  {}", logAnnotation.value());
            log.info("【请求信息】,URL:{} ,Remote Host :{}", request.getRequestURI(), request.getRemoteHost());
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler, ModelAndView modelAndView) throws Exception {
        Log logAnnotation = getLogAnnotation(handler);
        log.info("【日志信息】postHandle :{}", logAnnotation.value());
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler, Exception ex) throws Exception {
        Log logAnnotation = getLogAnnotation(handler);
        log.info("【日志信息】afterCompletion :{}", logAnnotation.value());
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    private Log getLogAnnotation(Object handler) {
        // HandlerMethod 提供对方法参数、方法返回值、方法注释等的便捷访问。
        if (handler instanceof HandlerMethod) {
            HandlerMethod methodHandle = (HandlerMethod) handler;
            Method method = methodHandle.getMethod();
            Log annotation = method.getAnnotation(Log.class);
            if (Objects.nonNull(annotation)) {
                return annotation;
            }
            return null;
        }
        return null;
    }
}


           

将拦截器注册到springMVC中

这个也有两种方式:

  1. 实现接口WebMvcConfigurer
  2. 继承抽象类WebMvcConfigurerAdapter

2也是在1的基础上实现的。而且也被弃用了

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

这里基于接口WebMvcConfigurer实现注册拦截器

/**
 * @author yujiaxing
 * @date 2021/09/28
 */
@Configuration
public class InterceptorRegisterConfig implements WebMvcConfigurer {

    @Autowired
    LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 需要匹配的路径
        List<String> includePatterns = new ArrayList<>();
        includePatterns.add("/api/test");
        includePatterns.add("/api/test/include");
        // 需要排除的路径
        List<String> excludePatterns = new ArrayList<>();
        excludePatterns.add("/api/test/exclude");
        // 注册拦截器
        registry.addInterceptor(logInterceptor)
                .addPathPatterns(includePatterns)
                .excludePathPatterns(excludePatterns);
    }
}
           

定义一个Controller,来检测一下拦截器。

/**
 * @author yujiaxing
 * @date 2021/09/28
 */
@RestController
public class TestController {

    @Log("测试日志信息")
    @GetMapping("/api/test")
    public String test() {
        return "this is /api/test";
    }

    @Log("测试日志信息---测试匹配路径")
    @GetMapping("/api/test/include")
    public String testInclude() {
        return "this is /api/test/include";
    }

    @Log("测试日志信息---测试不匹配路径")
    @GetMapping("/api/test/exclude")
    public String testExclude() {
        return "this is /api/test/exclude";
    }
}

           

访问地址: http://localhost:8081/api/test

浏览器:

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

查看控制台:

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

访问:http://localhost:8081/api/test/include 与上面类似

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

但访问 http://localhost:8081/api/test/exclude,就不会打印出相关日志信息。

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

这是因为拦截器在注册到Spring MVC时配置了能够匹配的路径和排除匹配的路径。这样就达到我们的需求了,只有在特定URL下,添加了@Log注解的方法才会被日志拦截器拦截,进行日志记录。类似登陆,鉴权也可以用类似的方式实现。

那么多个拦截器怎么定义拦截器的执行顺序呢?

在添加一个拦截器LogInterceptorV2

/**
 * @author yujiaxing
 * @date 2021/09/28
 */
@Slf4j
@Component
public class LogInterceptorV2 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("handler type : {}", handler.getClass());  // handler type : class org.springframework.web.method.HandlerMethod
        Log logAnnotation = getLogAnnotation(handler);
        if (Objects.nonNull(logAnnotation)) {
            log.info("【日志信息V2】:preHandle  {}", logAnnotation.value());
            log.info("【请求信息】,URL:{} ,Remote Host :{}", request.getRequestURI(), request.getRemoteHost());
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        Log logAnnotation = getLogAnnotation(handler);
        log.info("【日志信息V2】postHandle :{}", logAnnotation.value());
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Log logAnnotation = getLogAnnotation(handler);
        log.info("【日志信息V2】afterCompletion :{}", logAnnotation.value());
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    private Log getLogAnnotation(Object handler) {
        // HandlerMethod 提供对方法参数、方法返回值、方法注释等的便捷访问。
        if (handler instanceof HandlerMethod) {
            HandlerMethod methodHandle = (HandlerMethod) handler;
            Method method = methodHandle.getMethod();
            Log annotation = method.getAnnotation(Log.class);
            if (Objects.nonNull(annotation)) {
                return annotation;
            }
            return null;
        }
        return null;
    }
}
           

注册到springMVC中,修改InterceptorRegisterConfig

@Configuration
public class InterceptorRegisterConfig implements WebMvcConfigurer {

    @Autowired
    LogInterceptor logInterceptor;
    @Autowired
    LogInterceptorV2 logInterceptorV2;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 需要匹配的路径
        List<String> includePatterns = new ArrayList<>();
        includePatterns.add("/api/test");
        includePatterns.add("/api/test/include");
        // 需要排除的路径
        List<String> excludePatterns = new ArrayList<>();
        excludePatterns.add("/api/test/exclude");
        // 注册拦截器


        registry.addInterceptor(logInterceptorV2)
                .addPathPatterns(includePatterns)
                .excludePathPatterns(excludePatterns);

        registry.addInterceptor(logInterceptor)
                .addPathPatterns(includePatterns)
                .excludePathPatterns(excludePatterns);
    }

}
           

这个与拦截器注册到springMVC中的顺序有关,因为注册器InterceptorRegistry是维护的是List,所以拦截器访问是有序的,先注册的在前,后注册的在后面。

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析
如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

一个拦截器时,执行的顺序是:preHandle --> postHandle -->afterCompletion

多个拦截器时:拦截器V1,拦截器V2

V1.preHandle ------> V2.preHandle ------> V2.postHandle------>V1.postHandle------>V2.afterCompletion------>V1.afterCompletion

结合SpringMVC源码,对拦截器进行分析

首先来看HandlerInterceptor接口

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

结合这张图:主要是在2,3这个步骤中,使用拦截器进行拦截。

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

三个方法:

  • preHandle:执行处理程序之前的拦截点。 在 HandlerMapping 确定合适的处理程序对象之后,但在 HandlerAdapter 调用处理程序之前调用。
  • postHandle:成功执行处理程序后的拦截点。 在 HandlerAdapter 实际调用处理程序之后调用,但在 DispatcherServlet 呈现视图之前调用
  • afterCompletion:请求处理完成后的回调,即渲染视图后。注意:只有在此拦截器的preHandle方法成功完成并返回true才会调用!

来看下源码的调用流程:重启项目,第一次调用时,会出先下图

结合我们追前打印出来的结果:

如果没有debug日志信息,在application.properties配置文件中添加web包的日志级别

logging.level.web=debug
           
如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析
  1. RequestMappingHandlerMapping中打印了映射到的处理程序对象。在执行preHandle方法之前,而preHandle在HandlerMapping 确定合适的处理程序对象之后。

    来看下RequestMappingHandlerMapping这个类,实现了HandlerMapping,所以preHandle的确是在HandlerMapping 确定合适的处理程序对象之后。

    如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析
  2. RequestResponseBodyMethodProcessor这个类打印出了参数和返回值,参考下图,参数解析,方法返回值处理,说明这个是调用了映射的处理程序对象了。也证明了preHandle方法在 HandlerAdapter 调用处理程序之前调用。
    如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

    3.postHandle方法在调用了处理程序之后调用。

    4.afterCompletion在postHandle方法之后调用。

来看下上图中HandlerMapping接收到一个请求,如何返回HandlerExecutionChain。

HandlerMapping#getHandler,该方法是在DispatcherServlet#getHandler中调用HandlerMapping#getHandler方法

DispatcherServlet#getHandler

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

HandlerMapping#getHandler

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

AbstractHandlerMapping 实现了HandlerMapping,重写了getHandler方法,而RequestMappingHandlerMapping继承了AbstractHandlerMapping。三者之间的关系如下

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

AbstractHandlerMapping#getHandler

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

RequestMappingHandlerMapping#getHandler

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

所以控制台可以看到

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

这就是一个HttpServletRequest请求到HandlerMapping中的过程,返回一个HandlerExecutionChain,包含的就是这个请求所匹配到的拦截器,没有匹配到的已经被过滤了

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

其中interceptorList就是handler所要执行的拦截器列表。下一步就是执行拦截器,遍历拦截器列表,执行拦截器。

在HandlerExecutionChain#applyPreHandle进行,下面的HandlerExecutionChain#applyPostHandle,HandlerExecutionChain#triggerAfterCompletion就是拦截器中postHandle,afterCompletion的执行。

如何SpringMVC中注册拦截器?注册的多个拦截器执行的顺序是什么样的?结合SpringMVC源码,对拦截器进行分析

这就是Spring MVC中 拦截器模式源代码的分析。

一个请求到达DispatcherServlet,调用DispatcherServlet#getHandler,在到HandlerMapping#getHandler中,在到RequestMappingHandlerMapping#getHandler得到一个HandlerExecutionChain,然后在执行HandlerExecutionChain#applyPreHandle,在到HandlerInterceptor#preHandle,在到HandlerExecutionChain#applyPostHandle在到HandlerInterceptor#postHandle,在到HandlerExecutionChain#triggerAfterCompletion

在到HandlerInterceptor#afterCompletion。

继续阅读