美文网首页Java
Java面试题:SpringMVC过滤器(Filter)与拦截器

Java面试题:SpringMVC过滤器(Filter)与拦截器

作者: 程序员驴子酱 | 来源:发表于2021-12-06 15:02 被阅读0次

    1 概述

    在JavaWeb阶段我们学习了Filter过滤器,提出Filter的概念一开始我们为了过滤字符集乱码,在Servlet体系中拦截目标请求,而拦截器是在SpringMVC中定义的概念名叫HandlerInteceptor。

    在开发过程中,使用拦截器的配置更为灵活,其API接口更丰富,他们的目的都可以达到对请求的前置和后置处理,其本质上区别不大,但由于拦截器可以被Spring容器管理,从而获得被容器赋予的能力,而filter功能单一,所以后期大家都习惯使用拦截器完成某项特定的功能。


    过滤器和拦截器.jpg

    2 过滤器(Filter)

    2.1 过滤器定义

    一个实现了特殊接口(Filter)的Java类,实现对请求资源(jsp、servlet、html)的过滤功能。过滤器是一个运行在服务器的程序,优先于请求资源(jsp、servlet、html)之前执行, 过滤器是javaweb技术中最为实用的技术之一。

    2.2 过滤器作用

    Filter的作用是对目标资源(servlet、jsp)进行过滤,其应用场景有:登录权限检查,解决网站乱码,过滤敏感字符等等。

    Filter 接口中定义了三个方法:

    • init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次,这个方法必须执行成功,否则过滤器会不起作用。
    • doFilter() :容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
    • destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次。

    2.3 过滤器实现

    @Component
    public class MyFilter implements Filter {
         
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
     
            System.out.println("Filter 前置");
        }
     
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
     
            System.out.println("Filter 处理中");
            filterChain.doFilter(servletRequest, servletResponse);
        }
     
        @Override
        public void destroy() {
     
            System.out.println("Filter 后置");
        }
    }
    

    3 拦截器 (Interceptor)

    3.1 拦截器定义

    拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor, 一个请求也可以触发多个拦截器 ,而每个拦截器的调用会依据它的声明顺序依次执行。

    3.2 拦截器的核心API

    SpringMVC拦截器提供三个方法分别是preHandle、postHandle、afterCompletion,我们就是通过这几个方法来对用户的请求进行拦截处理的。

    • preHandle() :这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
    • postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
    • afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行,在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。

    3.3 拦截器的实现

    SpringMVC拦截器有两种实现方式:

    第一种方式是要定义的Interceptor类要实现了Spring的HandlerInterceptor 接口;

    第二种方式是继承实现了HandlerInterceptor接口的类,比如Spring已经提供的实现了HandlerInterceptor接口的抽象类HandlerInterceptorAdapter;

    以下是实现一个登录拦截过程

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    public class CommonInterceptor  extends HandlerInterceptorAdapter{
    
            private final Logger log = LoggerFactory.getLogger(CommonInterceptor.class);
    
            public  static  final  String  LAST_PAGE = "lastPage";
            /** 
             * 在业务处理器处理请求之前被调用 
             * 如果返回false 
             *     从当前的拦截器往回执行所有拦截器的afterCompletion(),再退出拦截器链
             *     
             * 如果返回true 
             *    执行下一个拦截器,直到所有的拦截器都执行完毕 
             *    再执行被拦截的Controller 
             *    然后进入拦截器链, 
             *    从最后一个拦截器往回执行所有的postHandle() 
             *    接着再从最后一个拦截器往回执行所有的afterCompletion() 
             */  
            @Override  
            public boolean preHandle(HttpServletRequest request,  
                    HttpServletResponse response, Object handler) throws Exception {            
                if ("GET".equalsIgnoreCase(request.getMethod())) {
                        RequestUtil.saveRequest();
                }
                log.info("==============执行顺序: 1、preHandle================");  
                String requestUri = request.getRequestURI();
                String contextPath = request.getContextPath();
                String url = requestUri.substring(contextPath.length());         if ("/userController/login".equals(url)) {                  
                        return true;
                }else {               
                        String username =  (String)request.getSession().getAttribute("user"); 
                        if(username == null){
                                log.info("Interceptor:跳转到login页面!");
                                request.getRequestDispatcher("/page/index.html").forward(request, response);
                                return false;
                        }else
                                return true;   
               }
                
            }        
            /**
             * 在业务处理器处理请求执行完成后,生成视图之前执行的动作   
             * 可在modelAndView中加入数据,比如当前时间
             */
            @Override  
            public void postHandle(HttpServletRequest request,  
                    HttpServletResponse response, Object handler,  
                    ModelAndView modelAndView) throws Exception {   
                log.info("==============执行顺序: 2、postHandle================");  
                if(modelAndView != null){  //加入当前时间  
                    modelAndView.addObject("haha", "测试postHandle");  
                }  
            }        
            /** 
             * 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等    
             * 当有拦截器抛出异常时,会从当前拦截器往回执行所有的拦截器的afterCompletion() 
             */  
            @Override  
            public void afterCompletion(HttpServletRequest request,  
                    HttpServletResponse response, Object handler, Exception ex)  
                    throws Exception {  
                log.info("==============执行顺序: 3、afterCompletion================");  
            }  
    }
    

    spring-MVC.xml的相关配置

     <!--配置拦截器, 多个拦截器,顺序执行 -->
        <mvc:interceptors> 
               <mvc:interceptor>
                       <!--  
                           /**的意思是所有文件夹及里面的子文件夹 
                           /*是所有文件夹,不含子文件夹 
                           /是web项目的根目录
                         --> 
                       <mvc:mapping path="/**" /> 
                       <!-- 需排除拦截的地址 -->  
                       <!--  <mvc:exclude-mapping path="/userController/login"/>  -->
                       <bean id="commonInterceptor" class="org.atguigu.interceptor.CommonInterceptor"></bean> <!--这个类就是我们自定义的Interceptor -->
              </mvc:interceptor> 
              <!-- 当设置多个拦截器时,先按顺序调用preHandle方法,然后逆序调用每个拦截器的postHandle和afterCompletion方法  -->
        </mvc:interceptors>
    

    web.xml

     <!-- 不拦截静态文件 -->
        <servlet-mapping>
            <servlet-name>default</servlet-name>
            <url-pattern>/js/*</url-pattern>
            <url-pattern>/css/*</url-pattern>
            <url-pattern>/images/*</url-pattern>
            <url-pattern>/fonts/*</url-pattern>
        </servlet-mapping>
    

    以上代码也可以在springmvc.xml 这样写

    <!-- 对静态资源文件的访问-->
    <mvc:resources mapping="/images/**"  location="/images/"/> 
    <mvc:resources mapping="/css/**"  location="/css/" />
    <mvc:resources mapping="/js/**"  location="/js/" /> 
    <mvc:resources mapping="/favicon.ico"  location="favicon.ico" />
    

    通过以上两个对Filter和Interceptor的配置,我们大致也能上手写项目,下面就针对他们的主要区别展开叙述

    4 过滤器和拦截器的区别

    过滤器和拦截器均体现了AOP的编程思想,都可以实现诸如日志,登录鉴权等功能,但二者的不同点也是比较多的

    • 拦截器是基于Java的反射机制,而过滤器是基于函数回调
    • 拦截器不依赖与Servlet容器,而过滤器依赖Servlet容器
    • 拦截器只能对Controller请求起作用,而过滤器可以对几乎所有请求起作用
    • 拦截器可以访问Controller上下文,值栈里的对象,而过滤器不能
    • 在Spring容器的生命周期中,拦截器可以多次调用,而过滤器只能在容器初始化时被调用一次

    4.1 实现原理不同

    过滤器和拦截器底层实现方式大不相同,过滤器是基于函数回调的,拦截器则是基于Java的反射机制(动态代理)实现的

    在我们自定义的过滤器中都会实现一个 doFilter()方法,这个方法有一个FilterChain 参数,而实际上它是一个回调接口。ApplicationFilterChain是它的实现类, 这个实现类内部也有一个 doFilter() 方法就是回调方法。

    public interface FilterChain {
     
        void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
    }
    
    1634109498474.png

    ApplicationFilterChain里面能拿到我们自定义的xxxFilter类,在其内部回调方法doFilter()里调用各个自定义xxxFilter过滤器,并执行 doFilter() 方法。

    public final class ApplicationFilterChain implements FilterChain {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response) {
                ...//省略
                internalDoFilter(request,response);
        }
      
        private void internalDoFilter(ServletRequest request, ServletResponse response){
        if (pos < n) {
                //获取第pos个filter    
                ApplicationFilterConfig filterConfig = filters[pos++];        
                Filter filter = filterConfig.getFilter();
                ...
                filter.doFilter(request, response, this);
            }
        }
      
    }
    

    而每个xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执,filterChain.doFilter(servletRequest,servletResponse),也就是回调ApplicationFilterChain的doFilter() 方法,以此循环执行实现函数回调。

    @Override
       public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
     
           filterChain.doFilter(servletRequest, servletResponse);
       }
    

    4.2 使用范围不同

    我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。


    而拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。
    3.png

    4.3 触发时机不同

    过滤器 和 拦截器的触发时机也不同,我们看下边这张图。


    4.png

    过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

    拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

    4.4 拦截的请求范围不同

    在上边我们已经同时配置了过滤器和拦截器,再建一个Controller接收请求测试一下。

    @Controller
    @RequestMapping()
    public class Test {
     
        @RequestMapping("/test1")
        @ResponseBody
        public String test1(String a) {
            System.out.println("我是controller");
            return null;
        }
    }
    

    过滤器

    @Autowired
    private TestService testService;
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("Filter 处理中");
            filterChain.doFilter(servletRequest, servletResponse);
     }
    
    

    拦截器

    @Component
    public class MyInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("Interceptor 前置");
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("Interceptor 处理中");
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("Interceptor 后置");
        }
    }
    

    项目启动过程中发现,过滤器的init()方法,随着容器的启动进行了初始化

    此时浏览器发送请求,F12 看到居然有两个请求,一个是我们自定义的 Controller 请求,另一个是访问静态图标资源的请求。


    6.png

    看到控制台的打印日志如下

    执行顺序 :Filter 处理中 -> Interceptor 前置 -> 我是controller -> Interceptor 处理中 -> Interceptor 处理后

    Filter 处理中
    Interceptor 前置
    我是controller
    Interceptor 处理中
    Interceptor 后置
    Filter 处理中
    

    过滤器Filter执行了两次,拦截器Interceptor只执行了一次。这是因为过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

    4.5 注入bean情况不同

    在实际的业务场景中,应用到过滤器或拦截器,为处理业务逻辑难免会引入一些service服务。

    下边我们分别在过滤器和拦截器中都注入service,看看有什么不同?

    @Component
    public class TestServiceImpl implements TestService {
     
        @Override
        public void a() {
            System.out.println("我是方法A");
        }
    }
    

    过滤器中注入service,发起请求测试一下 ,日志正常打印出“我是方法A”。

    @Autowired
    private TestService testService;
     
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
     
            System.out.println("Filter 处理中");
            testService.a();   // 调用service方法  
            filterChain.doFilter(servletRequest, servletResponse);
     }
    

    拦截器中

    @Component
    public class MyInterceptor implements HandlerInterceptor {
      //  @Autowired
      //  private TestService testService;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("Interceptor 前置");
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
           // testService.a();
            System.out.println("Interceptor 处理中");
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("Interceptor 后置");
        }
    }
    

    结果:

    Filter 处理中
    我是方法A
    Interceptor 前置
    我是controller
    Interceptor 处理中
    Interceptor 后置
    

    但是在拦截器中注入service ,发起请求测试,竟然会报错,别急,原因是加载顺序导致的问题

    @Component
    public class MyInterceptor implements HandlerInterceptor {
        @Autowired
        private TestService testService;
        
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            testService.a();
            System.out.println("Interceptor 处理中");
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("Interceptor 前置");
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("Interceptor 后置");
        }
    }
    
    
    7.png

    拦截器加载的时间点是SpringContext之前,而Bean又是由Spring容器管理的, 所以在当然不能获取到bean对象

    解决办法:

    我们在注册拦截器之前,手动将Interceptor进行注入,注意在registry.addInterceptor()注册的是getMyInterceptor()实例

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
     
        @Bean
        public MyInterceptor getMyInterceptor(){
            System.out.println("注入了MyInterceptor");
            return new MyInterceptor();
        }
         
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
     
            registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
        }
    }
    

    这样就可以在MyInterceptor拦截器类中使用service的bean

    4.6 控制执行顺序不同

    实际开发过程中,会出现多个过滤器或拦截器同时存在的情况,不过,有时我们希望某个过滤器或拦截器能优先执行,就涉及到它们的执行顺序。

    过滤器用@Order注解控制执行顺序,通过@Order控制过滤器的级别,值越小级别越高越先执行。

    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Component
    public class MyFilter2 implements Filter {
    

    拦截器默认的执行顺序,就是它的注册顺序,也可以通过Order手动设置控制,值越小越先执行。

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
     
           registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").order(2);
           registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**").order(1);
           registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").order(3);
            
    }
    

    看到输出结果发现,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

    postHandle() 方法被调用的顺序跟 preHandle() 居然是相反的!如果实际开发中严格要求执行顺序,那就需要特别注意这一点。

    Interceptor1 前置
    Interceptor2 前置
    Interceptor 前置
    我是controller
    Interceptor 处理中
    Interceptor2 处理中
    Interceptor1 处理中
    Interceptor 后置
    Interceptor2 处理后
    Interceptor1 处理后
    

    看源码,我们要知道controller 中所有的请求都要经过核心组件DispatcherServlet路由,都会执行它的 doDispatch() 方法,而拦截器postHandle()、preHandle()方法便是在其中调用的。

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
         
            try {
             ...........
                try {
                
                    // 获取可以执行当前Handler的适配器
                    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
     
                    // Process last-modified header, if supported by the handler.
                    String method = request.getMethod();
                    boolean isGet = "GET".equals(method);
                    if (isGet || "HEAD".equals(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if (logger.isDebugEnabled()) {
                            logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                        }
                        if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }
                    // 注意: 执行Interceptor中PreHandle()方法
                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }
     
                    // 注意:执行Handle【包括我们的业务逻辑,当抛出异常时会被Try、catch到】
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
     
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }
                    applyDefaultViewName(processedRequest, mv);
     
                    // 注意:执行Interceptor中PostHandle 方法【抛出异常时无法执行】
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                }
            }
            ...........
        }
    

    看看两个方法applyPreHandle()、applyPostHandle()具体是如何被调用的,就明白为什么postHandle()、preHandle() 执行顺序是相反的了。

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
            HandlerInterceptor[] interceptors = this.getInterceptors();
            if(!ObjectUtils.isEmpty(interceptors)) {
                for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
                    HandlerInterceptor interceptor = interceptors[i];
                    if(!interceptor.preHandle(request, response, this.handler)) {
                        this.triggerAfterCompletion(request, response, (Exception)null);
                        return false;
                    }
                }
            }
     
            return true;
        }
    
    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
            HandlerInterceptor[] interceptors = this.getInterceptors();
            if(!ObjectUtils.isEmpty(interceptors)) {
                for(int i = interceptors.length - 1; i >= 0; --i) {
                    HandlerInterceptor interceptor = interceptors[i];
                    interceptor.postHandle(request, response, this.handler, mv);
                }
            }
        }
    

    发现两个方法中在调用拦截器数组 HandlerInterceptor[] 时,循环的顺序竟然是相反的,导致postHandle()、preHandle() 方法执行的顺序相反。

    相关文章

      网友评论

        本文标题:Java面试题:SpringMVC过滤器(Filter)与拦截器

        本文链接:https://www.haomeiwen.com/subject/grogxrtx.html