Spring5 全家桶 | 21 - Spring MVC Interceptor

1,258 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

一、拦截器

Spring MVC提供了拦截器机制,允许在运行目标方法前进行一些拦截工作,或者在目标方法运行之后进行一些其他处理

Spring MVC 中的拦截器是HandlerInterceptor接口,该接口包含了三个方法

image.png

  • preHandler:这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求进行处理,如果需要该拦截器对请求进行拦截处理后还要调用其他拦截器,或者是业务处理器进行处理,则返回True既放行请求,如果不需要再调用其他组件就返回false,既不放行请求
  • postHandler:这个方法在业务处理器处理完成请求后调用,但是DispatcherServlet向客户端返回响应前被调用,在该方法中对用户请求request进行处理
  • afterCompletion:这个方法在DispatcherServlet完全处理请求后被调用,可以在该方法中进行一些资源清理的操作

二、自定义拦截器

拷贝spring-mvc-ajax项目,并重命名为spring-mvc-handler,删除除了配置之外的类及文件。

拦截器的正常流程

新建一个HandlerInterceptorSamplerController,在该Controller中定义interceptor方法,测试自定义的拦截器,并返回success页面

@Controller
public class HandlerInterceptorSamplerController {

    @RequestMapping("/interceptor")
    public String interceptor(){

        System.out.println("interceptor方法被调用");
        return "success";
    }
}

在index.jsp页面增加一个超链接

<a href="/interceptor">拦截该请求</a>

新增interceptor包,新建一个自定义的拦截器ZuluInterceptor,自定义拦截器必须实现HandlerInterceptor接口,在拦截器中的每个方法中添加了日志打印

public class ZuluInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println(this.getClass().getName() + " preHandler方法运行了");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(this.getClass().getName() + " postHandle方法运行了");
        System.out.println(modelAndView.getViewName());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(this.getClass().getName() + " afterCompletion方法运行了");
    }
}

在Spring MVC 配置文件中注册拦截器,配置这个拦截器拦截哪些请求

<mvc:interceptors>
    <!--第一种方式配置某个拦截器,默认是拦截所有请求的-->
    <bean class="com.citi.interceptor.ZuluInterceptor"></bean>
</mvc:interceptors>

启动应用,点击首页的超链接

image.png

根据控制台的输出,自定义的拦截器被成功调用

因此拦截器的正常流程是:拦截器preHandler方法 -> 目标方法 -> 拦截器的postHandler方法 -> 页面渲染 -> 拦截器的afterCompletion方法

拦截器的异常流程

异常流程1 - preHandler返回false

在ZuluInterceptor拦截器的preHandler方法返回false,再次启动,点击首页的超链接 image.png 控制台只输出了preHandler方法的执行信息,因此只要preHandler返回false,既不放行就不会有以后的方法的执行。

异常流程2 - 其他异常

保持preHandler方法返回true,同时在Controller中的interceptor方法返回页面前增加异常代码

@RequestMapping("/interceptor")
public String interceptor(){

    System.out.println("interceptor方法被调用");
    // 异常代码
    int i = 10 / 0;
    return "success";
}

再次重新启动应用,点击页面的超链接 image.png 页面出现有异常代码导致的报错

image.png 此时控制台执行了afterCompletion方法

三、多个拦截器执行顺序

在interceptor包中拷贝ZuluInterceptor并重命名为DeltaInterceptor;在Spring MVC配置文件中注册新定义的拦截器

<mvc:interceptors>
    <!--第一种方式配置某个拦截器,默认是拦截所有请求的-->
    <bean class="com.citi.interceptor.ZuluInterceptor"></bean>
    <bean class="com.citi.interceptor.DeltaInterceptor"></bean>
</mvc:interceptors>

将Controller中的interceptor方法中的异常代码注销,重新启动,点击页面的插连接

image.png 根据控制台的输出可以确定,限制性了Zulu拦截器中的preHandler方法,接着执行Delta拦截器的preHandler方法,再执行目标方法,接着调用Delta拦截器的postHandler,再执行Zulu拦截器的postHandler,再执行Delta拦截器的afterCompletion方法,最后再执行Zulu拦截器的afterCompletion方法

拦截顺序: 拦截器拦截顺序是按照配置的先后顺序,调整拦截器配置顺序

<mvc:interceptors>
    <!--第一种方式配置某个拦截器,默认是拦截所有请求的-->
    <bean class="com.citi.interceptor.DeltaInterceptor"></bean>
    <bean class="com.citi.interceptor.ZuluInterceptor"></bean>
</mvc:interceptors>

再次启动,点击首页的超链接 image.png 根据控制台输出,配置文件中先配置的Delta拦截器最先执行了

多个拦截器的异常流程:

保持Spring MVC配置文件中Delta拦截器在前,Zulu拦截器在后的顺序。如果Delta拦截器不放行,也就没有后面所有的调用;如果Zulu拦截器不放行,会是什么结果?

在Zulu拦截器中返回false,重新启动应用,并点击首页的超链接

image.png 根据控制台的输出可以确定,即是Zulu拦截器不放行,但是Delta的afterCompletion方法还是会执行。

已放行了的拦截器的afterCompletion方法总会执行

四、拦截器源码分析

拦截器返回true的情况

在DispatcherServlet类的doDispatch方法上打断点,启动Debug模式,Step Over到1044行,这类可以确定在获取处理器的时候,将所有的拦截器也一并获取到了 image.png

继续Step Over到1062行 image.png 这里在目标方法执行前执行了拦截器的preHandle方法,控制台输出了自定义的两个拦截器的preHandle方法。

重新启动Debug, Step Into进入applyPreHandle方法

image.png 可以看出这里对拦截器列表进行循环,自定义的拦截器就在该列表中,继续Step Over循环到自定义的拦截器

image.png Step Over到执行preHandler方法,再Step Into就进入到了自定义的拦截器中的preHandle方法

image.png 在自定义的拦截器中继续Step Over,控制台打印出了preHandle方法中的输出信息,并且拦截器返回true,接着回到applyPreHandle方法中

image.png

这里if条件中做了一个取反操作,Delta拦截器返回true之后,这取反变为false,就不会执行if条件下的代码块,而是直接循环到下一个拦截器既Zulul拦截器,再去执行拦截器的preHandle方法

image.png

上图中可以看到这里循环到Zulu拦截器

image.png 在点击Step Over,于是Zulu拦截器的preHandle方法执行了,输出了preHandle方法中的内容

在所有拦截器的preHandle方法执行完之后,接着就会执行目标方法

image.png 继续Debug,连续三次Step Over进入到第1074行(执行完目标方法后)

image.png 进入applyPostHandle方法

image.png 可以看出这里也是一个for循环,循环执行所有拦截器的postHandle方法。要注意的是这里是从最大的索引开始遍历,所以第一个便是自定义的Zulu拦截器

image.png 继续Step Over,可以看出控制台执行了Zulu拦截器的postHandle方法

image.png 所有拦截器的postHandle方法执行完之后,接着Step Over到1087行

image.png Step IntotriggerAfterCompletion方法

image.png 再到Step IntotriggerAfterCompletion方法

image.png 这里又进入一个for循环,循环执行拦截器的afterCompletion方法,并且是从列表中最后一个拦截器开始执行。连续两次Step Over就进入到了自定义的Zulu拦截器 image.png 继续Step Over,会执行Zulu拦截器的afterCompletion方法

image.png 接着会执行Delta的afterCompletion方法

image.png

拦截器返回false的情况

如果拦截器的preHandler方法返回false,将Delta拦截器的preHandler返回false,重新启动Debug

进入applyPreHandle方法,循环到自定义的Delta拦截器 image.png

Delta拦截器的preHandler方法返回false,取反就变为true,于是这里会执行if代码块中的内容

image.png 点击Step Over下一步,在Step Over会return false

image.png 回到doDispatch方法中,这里通过取反变为true,于是进入if语句中,而这里只有一个return

image.png

进入try-catch-finally代码块中的finally代码块,再点击Step Over

image.png 于是直接进入到else代码块中,这里又做了一个判断,是否是Multipart request,既文件上请求,这里不是文件上传请求,所以为false,在点击Step Over

image.png 这里退出了doDispatch方法,

拦截器执行流程总结

image.png