天天看點

職責鍊模式(下):架構中常用的過濾器、攔截器是如何實作的?

我們之前學習了職責鍊模式的原理與實作,并且通過一個敏感詞過濾架構的例子,展示了職責鍊模式的設計意圖。本質上來說,它跟大部分設計模式一樣,都是為了解耦代碼,應對代碼的複雜性,讓代碼滿足開閉原則,提高代碼的可擴充性。

除此之外,我們還提到,職責鍊模式常用在架構的開發中,為架構提供擴充點,讓架構的使用者在不修改架構源碼的情況下,基于擴充點添加新的功能。實際上,更具體點來說,職責鍊模式最常用來開發架構的過濾器和攔截器。今天,我們就通過 Servlet Filter、Spring Interceptor 這兩個 Java 開發中常用的元件,來具體講講它在架構開發中的應用。

Servlet Filter

Servlet Filter 是 Java Servlet 規範中定義的元件,翻譯成中文就是過濾器,它可以實作對 HTTP 請求的過濾功能,比如鑒權、限流、記錄日志、驗證參數等等。因為它是 Servlet 規範的一部分,是以,隻要是支援 Servlet 的 Web 容器(比如,Tomcat、Jetty 等),都支援過濾器功能。為了幫助你了解,我畫了一張示意圖闡述它的工作原理,如下所示。

職責鍊模式(下):架構中常用的過濾器、攔截器是如何實作的?

在實際項目中,我們該如何使用 Servlet Filter 呢?我寫了一個簡單的示例代碼,如下所示。添加一個過濾器,我們隻需要定義一個實作 javax.servlet.Filter 接口的過濾器類,并且将它配置在 web.xml 配置檔案中。Web 容器啟動的時候,會讀取 web.xml 中的配置,建立過濾器對象。當有請求到來的時候,會先經過過濾器,然後才由 Servlet 來處理。

public class LogFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    // 在建立Filter時自動調用,
    // 其中filterConfig包含這個Filter的配置參數,比如name之類的(從配置檔案中讀取的)
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    System.out.println("攔截用戶端發送來的請求.");
    chain.doFilter(request, response);
    System.out.println("攔截發送給用戶端的響應.");
  }

  @Override
  public void destroy() {
    // 在銷毀Filter時自動調用
  }
}

// 在web.xml配置檔案中如下配置:
<filter>
  <filter-name>logFilter</filter-name>
  <filter-class>com.xzg.cd.LogFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>logFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

           

從剛剛的示例代碼中,我們發現,添加過濾器非常友善,不需要修改任何代碼,定義一個實作 javax.servlet.Filter 的類,再改改配置就搞定了,完全符合開閉原則。那 Servlet Filter 是如何做到如此好的擴充性的呢?我想你應該已經猜到了,它利用的就是職責鍊模式。現在,我們通過剖析它的源碼,詳細地看看它底層是如何實作的。

在上一節課中,我們講到,職責鍊模式的實作包含處理器接口(IHandler)或抽象類(Handler),以及處理器鍊(HandlerChain)。對應到 Servlet Filter,javax.servlet.Filter 就是處理器接口,FilterChain 就是處理器鍊。接下來,我們重點來看 FilterChain 是如何實作的。

不過,我們前面也講過,Servlet 隻是一個規範,并不包含具體的實作,是以,Servlet 中的 FilterChain 隻是一個接口定義。具體的實作類由遵從 Servlet 規範的 Web 容器來提供,比如,ApplicationFilterChain 類就是 Tomcat 提供的 FilterChain 的實作類,源碼如下所示。

為了讓代碼更易讀懂,我對代碼進行了簡化,隻保留了跟設計思路相關的代碼片段。完整的代碼你可以自行去 Tomcat 中檢視。

public final class ApplicationFilterChain implements FilterChain {
  private int pos = 0; //目前執行到了哪個filter
  private int n; //filter的個數
  private ApplicationFilterConfig[] filters;
  private Servlet servlet;
  
  @Override
  public void doFilter(ServletRequest request, ServletResponse response) {
    if (pos < n) {
      ApplicationFilterConfig filterConfig = filters[pos++];
      Filter filter = filterConfig.getFilter();
      filter.doFilter(request, response, this);
    } else {
      // filter都處理完畢後,執行servlet
      servlet.service(request, response);
    }
  }
  
  public void addFilter(ApplicationFilterConfig filterConfig) {
    for (ApplicationFilterConfig filter:filters)
      if (filter==filterConfig)
         return;

    if (n == filters.length) {//擴容
      ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig[n + INCREMENT];
      System.arraycopy(filters, 0, newFilters, 0, n);
      filters = newFilters;
    }
    filters[n++] = filterConfig;
  }
}
           

ApplicationFilterChain 中的 doFilter() 函數的代碼實作比較有技巧,實際上是一個遞歸調用。你可以用每個 Filter(比如 LogFilter)的 doFilter() 的代碼實作,直接替換 ApplicationFilterChain 的第 12 行代碼,一眼就能看出是遞歸調用了。我替換了一下,如下所示。

@Override
  public void doFilter(ServletRequest request, ServletResponse response) {
    if (pos < n) {
      ApplicationFilterConfig filterConfig = filters[pos++];
      Filter filter = filterConfig.getFilter();
      //filter.doFilter(request, response, this);
      //把filter.doFilter的代碼實作展開替換到這裡
      System.out.println("攔截用戶端發送來的請求.");
      chain.doFilter(request, response); // chain就是this
      System.out.println("攔截發送給用戶端的響應.")
    } else {
      // filter都處理完畢後,執行servlet
      servlet.service(request, response);
    }
  }
           

這樣實作主要是為了在一個 doFilter() 方法中,支援雙向攔截,既能攔截用戶端發送來的請求,也能攔截發送給用戶端的響應,你可以結合着 LogFilter 那個例子,以及對比待會要講到的 Spring Interceptor,來自己了解一下。而我們上一節課給出的兩種實作方式,都沒法做到在業務邏輯執行的前後,同時添加處理代碼。

Spring Interceptor

剛剛講了 Servlet Filter,現在我們來講一個功能上跟它非常類似的東西,Spring Interceptor,翻譯成中文就是攔截器。盡管英文單詞和中文翻譯都不同,但這兩者基本上可以看作一個概念,都用來實作對 HTTP 請求進行攔截處理。

它們不同之處在于,Servlet Filter 是 Servlet 規範的一部分,實作依賴于 Web 容器。Spring Interceptor 是 Spring MVC 架構的一部分,由 Spring MVC 架構來提供實作。用戶端發送的請求,會先經過 Servlet Filter,然後再經過 Spring Interceptor,最後到達具體的業務代碼中。我畫了一張圖來闡述一個請求的處理流程,具體如下所示。

職責鍊模式(下):架構中常用的過濾器、攔截器是如何實作的?

在項目中,我們該如何使用 Spring Interceptor 呢?我寫了一個簡單的示例代碼,如下所示。LogInterceptor 實作的功能跟剛才的 LogFilter 完全相同,隻是實作方式上稍有差別。LogFilter 對請求和響應的攔截是在 doFilter() 一個函數中實作的,而 LogInterceptor 對請求的攔截在 preHandle() 中實作,對響應的攔截在 postHandle() 中實作。

public class LogInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("攔截用戶端發送來的請求.");
    return true; // 繼續後續的處理
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    System.out.println("攔截發送給用戶端的響應.");
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    System.out.println("這裡總是被執行.");
  }
}

//在Spring MVC配置檔案中配置interceptors
<mvc:interceptors>
   <mvc:interceptor>
       <mvc:mapping path="/*"/>
       <bean class="com.xzg.cd.LogInterceptor" />
   </mvc:interceptor>
</mvc:interceptors>
           

同樣,我們還是來剖析一下,Spring Interceptor 底層是如何實作的。

當然,它也是基于職責鍊模式實作的。其中,HandlerExecutionChain 類是職責鍊模式中的處理器鍊。它的實作相較于 Tomcat 中的 ApplicationFilterChain 來說,邏輯更加清晰,不需要使用遞歸來實作,主要是因為它将請求和響應的攔截工作,拆分到了兩個函數中實作。HandlerExecutionChain 的源碼如下所示,同樣,我對代碼也進行了一些簡化,隻保留了關鍵代碼。

public class HandlerExecutionChain {
 private final Object handler;
 private HandlerInterceptor[] interceptors;
 
 public void addInterceptor(HandlerInterceptor interceptor) {
  initInterceptorList().add(interceptor);
 }

 boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
   for (int i = 0; i < interceptors.length; i++) {
    HandlerInterceptor interceptor = interceptors[i];
    if (!interceptor.preHandle(request, response, this.handler)) {
     triggerAfterCompletion(request, response, null);
     return false;
    }
   }
  }
  return true;
 }

 void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
  HandlerInterceptor[] interceptors = 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);
   }
  }
 }

 void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex)
   throws Exception {
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
   for (int i = this.interceptorIndex; i >= 0; i--) {
    HandlerInterceptor interceptor = interceptors[i];
    try {
     interceptor.afterCompletion(request, response, this.handler, ex);
    } catch (Throwable ex2) {
     logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
    }
   }
  }
 }
}
           

在 Spring 架構中,DispatcherServlet 的 doDispatch() 方法來分發請求,它在真正的業務邏輯執行前後,執行 HandlerExecutionChain 中的 applyPreHandle() 和 applyPostHandle() 函數,用來實作攔截的功能。具體的代碼實作很簡單,你自己應該能腦補出來,這裡就不羅列了。感興趣的話,你可以自行去檢視。

繼續閱讀