Java面試題:SpringMVC過濾器(Filter)與攔截器(Interceptor)的區(qū)別(附源碼)

1 概述

在JavaWeb階段我們學(xué)習(xí)了Filter過濾器,提出Filter的概念一開始我們?yōu)榱诉^濾字符集亂碼,在Servlet體系中攔截目標(biāo)請求,而攔截器是在SpringMVC中定義的概念名叫HandlerInteceptor。

在開發(fā)過程中,使用攔截器的配置更為靈活,其API接口更豐富,他們的目的都可以達到對請求的前置和后置處理,其本質(zhì)上區(qū)別不大,但由于攔截器可以被Spring容器管理,從而獲得被容器賦予的能力,而filter功能單一,所以后期大家都習(xí)慣使用攔截器完成某項特定的功能。


過濾器和攔截器.jpg

2 過濾器(Filter)

2.1 過濾器定義

一個實現(xiàn)了特殊接口(Filter)的Java類,實現(xiàn)對請求資源(jsp、servlet、html)的過濾功能。過濾器是一個運行在服務(wù)器的程序,優(yōu)先于請求資源(jsp、servlet、html)之前執(zhí)行, 過濾器是javaweb技術(shù)中最為實用的技術(shù)之一。

2.2 過濾器作用

Filter的作用是對目標(biāo)資源(servlet、jsp)進行過濾,其應(yīng)用場景有:登錄權(quán)限檢查,解決網(wǎng)站亂碼,過濾敏感字符等等。

Filter 接口中定義了三個方法:

  • init() :該方法在容器啟動初始化過濾器時被調(diào)用,它在 Filter 的整個生命周期只會被調(diào)用一次,這個方法必須執(zhí)行成功,否則過濾器會不起作用。
  • doFilter() :容器中的每一次請求都會調(diào)用該方法, FilterChain 用來調(diào)用下一個過濾器 Filter。
  • destroy(): 當(dāng)容器銷毀 過濾器實例時調(diào)用該方法,一般在方法中銷毀或關(guān)閉資源,在過濾器 Filter 的整個生命周期也只會被調(diào)用一次。

2.3 過濾器實現(xiàn)

@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 攔截器定義

攔截器它是鏈?zhǔn)秸{(diào)用,一個應(yīng)用中可以同時存在多個攔截器Interceptor, 一個請求也可以觸發(fā)多個攔截器 ,而每個攔截器的調(diào)用會依據(jù)它的聲明順序依次執(zhí)行。

3.2 攔截器的核心API

SpringMVC攔截器提供三個方法分別是preHandle、postHandle、afterCompletion,我們就是通過這幾個方法來對用戶的請求進行攔截處理的。

  • preHandle() :這個方法將在請求處理之前進行調(diào)用。「注意」:如果該方法的返回值為false ,將視為當(dāng)前請求結(jié)束,不僅自身的攔截器會失效,還會導(dǎo)致其他的攔截器也不再執(zhí)行。
  • postHandle():只有在 preHandle() 方法返回值為true 時才會執(zhí)行。會在Controller 中的方法調(diào)用之后,DispatcherServlet 返回渲染視圖之前被調(diào)用?!赣幸馑嫉氖恰梗簆ostHandle() 方法被調(diào)用的順序跟 preHandle() 是相反的,先聲明的攔截器 preHandle() 方法先執(zhí)行,而postHandle()方法反而會后執(zhí)行。
  • afterCompletion():只有在 preHandle() 方法返回值為true 時才會執(zhí)行,在整個請求結(jié)束之后, DispatcherServlet 渲染了對應(yīng)的視圖之后執(zhí)行。

3.3 攔截器的實現(xiàn)

SpringMVC攔截器有兩種實現(xiàn)方式:

第一種方式是要定義的Interceptor類要實現(xiàn)了Spring的HandlerInterceptor 接口;

第二種方式是繼承實現(xiàn)了HandlerInterceptor接口的類,比如Spring已經(jīng)提供的實現(xiàn)了HandlerInterceptor接口的抽象類HandlerInterceptorAdapter;

以下是實現(xiàn)一個登錄攔截過程

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";
        /** 
         * 在業(yè)務(wù)處理器處理請求之前被調(diào)用 
         * 如果返回false 
         *     從當(dāng)前的攔截器往回執(zhí)行所有攔截器的afterCompletion(),再退出攔截器鏈
         *     
         * 如果返回true 
         *    執(zhí)行下一個攔截器,直到所有的攔截器都執(zhí)行完畢 
         *    再執(zhí)行被攔截的Controller 
         *    然后進入攔截器鏈, 
         *    從最后一個攔截器往回執(zhí)行所有的postHandle() 
         *    接著再從最后一個攔截器往回執(zhí)行所有的afterCompletion() 
         */  
        @Override  
        public boolean preHandle(HttpServletRequest request,  
                HttpServletResponse response, Object handler) throws Exception {            
            if ("GET".equalsIgnoreCase(request.getMethod())) {
                    RequestUtil.saveRequest();
            }
            log.info("==============執(zhí)行順序: 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:跳轉(zhuǎn)到login頁面!");
                            request.getRequestDispatcher("/page/index.html").forward(request, response);
                            return false;
                    }else
                            return true;   
           }
            
        }        
        /**
         * 在業(yè)務(wù)處理器處理請求執(zhí)行完成后,生成視圖之前執(zhí)行的動作   
         * 可在modelAndView中加入數(shù)據(jù),比如當(dāng)前時間
         */
        @Override  
        public void postHandle(HttpServletRequest request,  
                HttpServletResponse response, Object handler,  
                ModelAndView modelAndView) throws Exception {   
            log.info("==============執(zhí)行順序: 2、postHandle================");  
            if(modelAndView != null){  //加入當(dāng)前時間  
                modelAndView.addObject("haha", "測試postHandle");  
            }  
        }        
        /** 
         * 在DispatcherServlet完全處理完請求后被調(diào)用,可用于清理資源等    
         * 當(dāng)有攔截器拋出異常時,會從當(dāng)前攔截器往回執(zhí)行所有的攔截器的afterCompletion() 
         */  
        @Override  
        public void afterCompletion(HttpServletRequest request,  
                HttpServletResponse response, Object handler, Exception ex)  
                throws Exception {  
            log.info("==============執(zhí)行順序: 3、afterCompletion================");  
        }  
}

spring-MVC.xml的相關(guān)配置

 <!--配置攔截器, 多個攔截器,順序執(zhí)行 -->
    <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> 
          <!-- 當(dāng)設(shè)置多個攔截器時,先按順序調(diào)用preHandle方法,然后逆序調(diào)用每個攔截器的postHandle和afterCompletion方法  -->
    </mvc:interceptors>

web.xml

 <!-- 不攔截靜態(tài)文件 -->
    <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 這樣寫

<!-- 對靜態(tài)資源文件的訪問-->
<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的配置,我們大致也能上手寫項目,下面就針對他們的主要區(qū)別展開敘述

4 過濾器和攔截器的區(qū)別

過濾器和攔截器均體現(xiàn)了AOP的編程思想,都可以實現(xiàn)諸如日志,登錄鑒權(quán)等功能,但二者的不同點也是比較多的

  • 攔截器是基于Java的反射機制,而過濾器是基于函數(shù)回調(diào)
  • 攔截器不依賴與Servlet容器,而過濾器依賴Servlet容器
  • 攔截器只能對Controller請求起作用,而過濾器可以對幾乎所有請求起作用
  • 攔截器可以訪問Controller上下文,值棧里的對象,而過濾器不能
  • 在Spring容器的生命周期中,攔截器可以多次調(diào)用,而過濾器只能在容器初始化時被調(diào)用一次

4.1 實現(xiàn)原理不同

過濾器和攔截器底層實現(xiàn)方式大不相同,過濾器是基于函數(shù)回調(diào)的,攔截器則是基于Java的反射機制(動態(tài)代理)實現(xiàn)的。

在我們自定義的過濾器中都會實現(xiàn)一個 doFilter()方法,這個方法有一個FilterChain 參數(shù),而實際上它是一個回調(diào)接口。ApplicationFilterChain是它的實現(xiàn)類, 這個實現(xiàn)類內(nèi)部也有一個 doFilter() 方法就是回調(diào)方法。

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

ApplicationFilterChain里面能拿到我們自定義的xxxFilter類,在其內(nèi)部回調(diào)方法doFilter()里調(diào)用各個自定義xxxFilter過濾器,并執(zhí)行 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 會先執(zhí)行自身的 doFilter() 過濾邏輯,最后在執(zhí)行結(jié)束前會執(zhí),filterChain.doFilter(servletRequest,servletResponse),也就是回調(diào)ApplicationFilterChain的doFilter() 方法,以此循環(huán)執(zhí)行實現(xiàn)函數(shù)回調(diào)。

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

4.2 使用范圍不同

我們看到過濾器 實現(xiàn)的是 javax.servlet.Filter 接口,而這個接口是在Servlet規(guī)范中定義的,也就是說過濾器Filter 的使用要依賴于Tomcat等容器,導(dǎo)致它只能在web程序中使用。


而攔截器(Interceptor) 它是一個Spring組件,并由Spring容器管理,并不依賴Tomcat等容器,是可以單獨使用的。不僅能應(yīng)用在web程序中,也可以用于Application、Swing等程序中。
3.png

4.3 觸發(fā)時機不同

過濾器 和 攔截器的觸發(fā)時機也不同,我們看下邊這張圖。


4.png

過濾器Filter是在請求進入容器后,但在進入servlet之前進行預(yù)處理,請求結(jié)束是在servlet處理完以后。

攔截器 Interceptor 是在請求進入servlet后,在進入Controller之前進行預(yù)處理的,Controller 中渲染了對應(yīng)的視圖之后請求結(jié)束。

4.4 攔截的請求范圍不同

在上邊我們已經(jīng)同時配置了過濾器和攔截器,再建一個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 后置");
    }
}

項目啟動過程中發(fā)現(xiàn),過濾器的init()方法,隨著容器的啟動進行了初始化

此時瀏覽器發(fā)送請求,F(xiàn)12 看到居然有兩個請求,一個是我們自定義的 Controller 請求,另一個是訪問靜態(tài)圖標(biāo)資源的請求。


6.png

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

執(zhí)行順序 :Filter 處理中 -> Interceptor 前置 -> 我是controller -> Interceptor 處理中 -> Interceptor 處理后

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

過濾器Filter執(zhí)行了兩次,攔截器Interceptor只執(zhí)行了一次。這是因為過濾器幾乎可以對所有進入容器的請求起作用,而攔截器只會對Controller中請求或訪問static目錄下的資源請求起作用。

4.5 注入bean情況不同

在實際的業(yè)務(wù)場景中,應(yīng)用到過濾器或攔截器,為處理業(yè)務(wù)邏輯難免會引入一些service服務(wù)。

下邊我們分別在過濾器和攔截器中都注入service,看看有什么不同?

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

過濾器中注入service,發(fā)起請求測試一下 ,日志正常打印出“我是方法A”。

@Autowired
private TestService testService;
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
 
        System.out.println("Filter 處理中");
        testService.a();   // 調(diào)用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 后置");
    }
}

結(jié)果:

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

但是在攔截器中注入service ,發(fā)起請求測試,竟然會報錯,別急,原因是加載順序?qū)е碌膯栴}

@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容器管理的, 所以在當(dāng)然不能獲取到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 控制執(zhí)行順序不同

實際開發(fā)過程中,會出現(xiàn)多個過濾器或攔截器同時存在的情況,不過,有時我們希望某個過濾器或攔截器能優(yōu)先執(zhí)行,就涉及到它們的執(zhí)行順序。

過濾器用@Order注解控制執(zhí)行順序,通過@Order控制過濾器的級別,值越小級別越高越先執(zhí)行。

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

攔截器默認的執(zhí)行順序,就是它的注冊順序,也可以通過Order手動設(shè)置控制,值越小越先執(zhí)行。

@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);
        
}

看到輸出結(jié)果發(fā)現(xiàn),先聲明的攔截器 preHandle() 方法先執(zhí)行,而postHandle()方法反而會后執(zhí)行。

postHandle() 方法被調(diào)用的順序跟 preHandle() 居然是相反的!如果實際開發(fā)中嚴格要求執(zhí)行順序,那就需要特別注意這一點。

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

看源碼,我們要知道controller 中所有的請求都要經(jīng)過核心組件DispatcherServlet路由,都會執(zhí)行它的 doDispatch() 方法,而攔截器postHandle()、preHandle()方法便是在其中調(diào)用的。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
     
        try {
         ...........
            try {
            
                // 獲取可以執(zhí)行當(dāng)前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;
                    }
                }
                // 注意: 執(zhí)行Interceptor中PreHandle()方法
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
 
                // 注意:執(zhí)行Handle【包括我們的業(yè)務(wù)邏輯,當(dāng)拋出異常時會被Try、catch到】
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
 
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                applyDefaultViewName(processedRequest, mv);
 
                // 注意:執(zhí)行Interceptor中PostHandle 方法【拋出異常時無法執(zhí)行】
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
        }
        ...........
    }

看看兩個方法applyPreHandle()、applyPostHandle()具體是如何被調(diào)用的,就明白為什么postHandle()、preHandle() 執(zhí)行順序是相反的了。

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);
            }
        }
    }

發(fā)現(xiàn)兩個方法中在調(diào)用攔截器數(shù)組 HandlerInterceptor[] 時,循環(huán)的順序竟然是相反的,導(dǎo)致postHandle()、preHandle() 方法執(zhí)行的順序相反。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容