Spring MVC学习—HandlerInterceptor处理器拦截器机制全解

Posted L-Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC学习—HandlerInterceptor处理器拦截器机制全解相关的知识,希望对你有一定的参考价值。

  基于最新Spring 5.x,详细介绍了Spring MVC的HandlerInterceptor处理器拦截器机制,以及它的一系列拦截方法。

  本次我们来学习Sring MVC的HandlerInterceptor处理器拦截器机制,HandlerInterceptor和Filter类似,可用于拦截请求,实现比如登陆、鉴权、过滤等自定义扩展的逻辑。

Spring MVC学习 系列文章

Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例

Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念

Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程

Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例

Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】

Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】

Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】

Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解

Spring MVC学习(9)—项目统一异常处理机制详解与使用案例

Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码

Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题

1 拦截器与过滤器

  1. 处理器拦截器HandlerInterceptor是Spring MVC提供的特性,依赖于Spring MVC框架,而不依赖Servlet容器,Filter则是Servlet的特性,属于Servlet的规范,并且依赖Servlet容器。
  2. 应用中可以存在多个拦截器形成拦截器链,也可以存在多个过滤器形成过滤器链!
  3. 拦截器链和过滤器链的预处理和后处理的调用顺序都是相反的,即预处理调用时按照链从前向后调用,而后处理调用时则按照链从后向前调用。
  4. 过滤器可用于对所有到达该应用的请求进行拦截,而拦截器则只能对通过DispatcherServlet进行处理的请求进行拦截。
  5. 在Request请求到达Servlet之前执行过滤器的预处理逻辑,在请求到达DispatcherServlet之后、执行Handler之前执行拦截器的预处理逻辑,并在成功执行Handler之后执行拦截器的后处理逻辑,在DispatcherServlet返回之后最后执行过滤器的后处理逻辑。
  6. Filter在doFilter一个方法中定义预处理和后处理逻辑,在方法中通过filterChain.doFilter进行分隔,而HandlerInterceptor将预处理和后处理逻辑拆分成两个方法,即preHandle、postHandle方法。
  7. Filter有该类本身的初始化和销毁的回调方法,即init和destroy,而HandlerInterceptor则没有,但是HandlerInterceptor拥有afterCompletion处理方法,无论有没有抛出异常,在DispatcherServlet请求处理的最后都会执行!

  所以说,很多功能其实Filter和HandlerInterceptor都能做,具体看你怎么选了,如果使用Spring MVC框架,那么建议使用HandlerInterceptor吧,它可以类似于普通bean直接注册到Spring容器中被管理。
  关于Filter过滤器,我们在此前讲Java Web的时候就讲过了,在此不再赘述,我们主要讲Spring MVC的HandlerInterceptor。

2 HandlerInterceptor拦截器的原理

  HandlerMapping在根据request查找Handler时,最终会返回一个HandlerExecutionChain对象,字面翻译就是处理器执行链对象,其内部包含了一个Handler和可以应用于该Handler的HandlerInterceptor执行链。
  所有的HandlerMapping实现都支持查找HandlerInterceptor链,想要自定义Handler的拦截器,必须实现org.springframework.web.servlet.HandlerInterceptor接口,此接口中有三个抽象方法,用于灵活的实现拦截器的功能:

  1. preHandle:在执行Handler之前(执行业务逻辑之前),根据拦截器链顺序执行;
  2. postHandle:在执行Handler成功(执行业务逻辑成功)之后,根据拦截器链倒序执行,如果前面的流程中抛出异常或者请求被拦截则不会执行!
  3. afterCompletion:在请求处理完毕之后执行,无论是否有响应视图,无论有没有通过preHandle,无论有没有抛出异常。只会对此前放行成功(preHandle返回true)的拦截器进行倒序调用。

  preHandle方法返回boolean类型的值,可以使用此方法中断或继续处理执行链。当当前此方法返回true时,拦截器链将继续先后执行,即继续执行后续拦截器的preHandle方法。当当前某个拦截器的preHandle方法返回false 时,DispatcherServlet 会假定拦截器本身已处理完毕请求(例如,已经渲染了合适的视图),此时将尝试直接倒序执行此前已放行的拦截器链的afterCompletion方法,随后retrun结束处理,不会继续执行执行链中的后续其他拦截器和Handler实际处理程序(业务逻辑)以及后续其他流程。
  对于采用了@ResponseBody注解或者返回ResponseEntity的方法,postHandle后处理不太有效,因为在Handler执行成功时响应的数据(比如JSON数据)已经被写入response并且已被提交,并且是在postHandle方法被执行之前进行的!此时对于response的任何修改(比如添加额外的头部信息)都为时已晚。对于这种情况,我们可以实现ResponseBodyAdvice接口并且声明为ControllerAdvice,或者直接在RequestMappingHandlerAdapter中配置。

3 配置拦截器

3.1 编写实现类

  我们需要编写一个实现了拦截器接口HandlerInterceptor的类

/**
 1. 自定义拦截器
 2.  3. @author lx
 */
@Component
public class MyInterceptor1 implements HandlerInterceptor {
    /**
     * 预处理,controller方法执行前
     *
     * @return true表示, 执行下一个拦截器, 没有拦截器了就执行controller中的方法;false表示不放行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor1 preHandle invoke ,true");
        return true;
    }

    /**
     * 后处理
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor1 postHandle invoke");
    }

    /**
     * 请求处理完毕调用
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor1 afterCompletion invoke");
    }
}

3.2 配置拦截器

3.2.1 配置拦截规则

  我们可以为某个拦截器设置拦截或者不拦截的路径,拦截器的路径也可以使用通配符,如下:

  1. ? 表示匹配任何单个字符。
  2. * 表示匹配0或者任意数量的字符。
  3. ** 表示匹配后续的任何0或者更多级的目录。
路径解析
/**表示拦截全部路径的请求
/**/*.jsp表示拦截以.jsp结尾的全部请求
/a/*.jsp表示拦截a路径下的以.jsp结尾的全部请求
/b/jp.表示拦截b路径下的以j开头和p结尾的具有任何后缀的请求
/?/?.xx表示拦截单个字符路径下一单个字符名的xx后缀请求

3.2.2 基于JavaCOnfig配置拦截器

  基于Java的配置很简单。编写配置类并继承WebMvcConfigurer类,重写其中的方法 addInterceptors,并且将这个配置类交给Spring管理!

@Configuration
@EnableWebMvc  //支持MVC配置
public class WebConfig implements WebMvcConfigurer {

    /**
     * 添加拦截器,默认执行顺序为添加顺序
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleChangeInterceptor());
        registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        /*
         * 添加拦截器
         * 通过addPathPatterns配置拦截器的拦截路径,可以多次调用该方法
         * 通过excludePathPatterns配置拦截器的不拦截路径,可以多次调用该方法
         */
        registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/a/**");
    }
}

3.2.3 基于XML配置拦截器

  基于XML的配置如下!

<!--配置拦截器-->
<mvc:interceptors>
    <!--配置拦截器 默认拦截所有-->
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
    <!--配置拦截器 -->
    <mvc:interceptor>
        <!--拦截-->
        <mvc:mapping path="/**"/>
        <!--不拦截-->
        <mvc:exclude-mapping path="/admin/**"/>
        <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
    </mvc:interceptor>
    <!--配置自定义拦截器-->
    <mvc:interceptor>
        <!--拦截-->
        <mvc:mapping path="/a/**"/>
        <bean class="com.spring.mvc.controller.MyInterceptor1"/>
    </mvc:interceptor>
</mvc:interceptors>

3.3 测试

  有一个Controller:

/**
 * @author lx
 */
@Controller
public class InterceptorController {

    public InterceptorController() {
        System.out.println("InterceptorController create");
    }

    @RequestMapping(path = "/a/b/c")
    public String interceptor1() {
        System.out.println("---interceptor1 Controller invoke---");
        return "/index.jsp";
    }


    @RequestMapping(path = "/a/bbb/c")
    public String interceptor2() {
        System.out.println("---interceptor2 Controller invoke---");
        return "/index.jsp";

    }


    @RequestMapping(path = "/b")
    public String interceptor3() {
        System.out.println("---interceptor3 Controller invoke---");
        return "/index.jsp";
    }
}

  我们测试访问/a/bbb/c和/a/b/c,发现请求都会被拦截:
在这里插入图片描述
  而访问/b则不会!

4 拦截器的细节

4.1 多个拦截器

  我们可以配置多个拦截并应用到一个请求中!
  多个拦截器默认情况下是按照在XML或者JavaConfig中的定义的顺序执行的,如果想要指定Order排序,那么需要自己创建InterceptorRegistration。
  我们将MyInterceptor1类复制一份并改为MyInterceptor2配置到拦截器链中:

@Configuration   //配置类
@EnableWebMvc  //支持Spring MVC注解配置
public class WebConfig implements WebMvcConfigurer {

    /**
     * 添加拦截器,默认执行顺序为添加顺序
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleChangeInterceptor());
        registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        /*
         * 添加拦截器
         * 通过addPathPatterns配置拦截器的拦截路径,可以多次调用该方法
         * 通过excludePathPatterns配置拦截器的不拦截路径,可以多次调用该方法
         */
        registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/a/**");
        registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/a/**");
    }
}

  我们测试访问/a/bbb/c,结果如下:
在这里插入图片描述
  正常情况下,preHandle方法在拦截器链中会顺序执行,而postHandle和afterCompletion方法则会倒序执行。

4.2 拦截测试

  如果我们在第二个拦截器中preHandle返回false,也就是不放行,对请求进行拦截,那么访问/a/bbb/c结果如下:
在这里插入图片描述
  可以看到,Controller方法并没有被执行,拦截器链的postHandle方法也不会被执行,并且只执行了MyInterceptor1的afterCompletion方法。
  实际上,如果某个拦截器不放行,那么后续的拦截器的preHandle预处理方法、Handler处理器、所有的postHandle后处理方法都不会被执行,并且在最后只会对此前放行成功的拦截器倒序执行afterCompletion方法。

4.3 拦截器跳转

  拦截器的preHandle、postHandle、afterCompletion方法均支持forward、include、redirect转发到其他路径或者动态资源!但是要注意在跳转前如果响应已提交,那么可能会造成异常(比如response写了数据并且执行了PrintWriter或者OutputStream的flush或者close方法,比如调用了sendError、sendRedirect方法,那么将会抛出一个IllegalStateException,并且操作不会完成)!
  基于这个特性,我们可以校验用户是否登陆,如果没有登陆,那么直接跳转到登录页面即可!

/**
 * 预处理,controller方法执行前
     *
     * @return true表示, 执行下一个拦截器, 没有拦截器了就执行controller中的方法;false表示不放行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor2 preHandle invoke ,true");

        //支持转发
//        RequestDispatcher requestDispatcher = request.getRequestDispatcher("/index.jsp");
//        requestDispatcher.forward(request, response);

        //支持包含
        RequestDispatcher requestDispatcher = request.getRequestDispatcher("/index.jsp");
        requestDispatcher.include(request, response);

        //支持重定向
//        response.sendRedirect("/mvc/index.jsp");
        return false;
    }

  再次访问/a/bbb/c,结果如下,确实转发到了index.jsp中
在这里插入图片描述

相关文章:
  https://spring.io/
  Spring Framework 5.x 学习
  Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

以上是关于Spring MVC学习—HandlerInterceptor处理器拦截器机制全解的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC学习笔记---Spring MVC 的HelloWorld

Spring MVC学习笔记---Spring MVC 的HelloWorld

Spring MVC学习—Spring MVC中容器的层次结构以及父子容器的概念

Spring MVC学习笔记——Controller

Spring MVC学习笔记——AbstractController

Spring MVC学习笔记——WebContentGenerator