Spring是如何使用責任鏈模式的?

  • 2019 年 11 月 8 日
  • 筆記

Java技術棧

www.javastack.cn

優秀的Java技術公眾號

關於責任鏈模式,其有兩種形式,一種是通過外部調用的方式對鏈的各個節點調用進行控制,從而進行鏈的各個節點之間的切換。

另一種是鏈的每個節點自由控制是否繼續往下傳遞鏈的進度,這種比較典型的使用方式就是Netty中的責任鏈模式。本文主要講解我們如何在Spring中使用這兩種責任鏈模式。

1

外部控制模式

對於外部控制的方式,這種方式比較簡單,鏈的每個節點只需要專註於各自的邏輯即可,而當前節點調用完成之後是否繼續調用下一個節點,這個則由外部控制邏輯進行。

這裡我們以一個過濾器的實現邏輯為例進行講解,在平常工作中,我們經常需要根據一系列的條件對某個東西進行過濾。

比如任務服務的設計,在執行某個任務時,其需要經過諸如時效性檢驗,風控攔截,任務完成次數等過濾條件的檢驗之後才能判斷當前任務是否能夠執行,只有在所有的過濾條件都完成之後,我們才能執行該任務。那麼這裡我們就可以抽象出一個Filter介面,其設計如下:

public interface Filter {      /**     * 用於對各個任務節點進行過濾     */    boolean filter(Task task);    }  

這裡的Filter.filter()方法只有一個參數Task,主要就是控制當前task是否需要被過濾掉,其有一個boolean類型的返回值,通過該返回值以告知外部控制邏輯是否需要將該task過濾掉。對於該介面的子類,我們只需要將其聲明為Spring所管理的一個bean即可:

// 時效性檢驗  @Component  public class DurationFilter implements Filter {      @Override    public boolean filter(Task task) {      System.out.println("時效性檢驗");      return true;    }  }  
// 風控攔截  @Component  public class RiskFilter implements Filter {      @Override    public boolean filter(Task task) {      System.out.println("風控攔截");      return true;    }  }  
// 次數限制校驗  @Component  public class TimesFilter implements Filter {      @Override    public boolean filter(Task task) {      System.out.println("次數限制檢驗");      return true;    }  }  

上面我們模擬聲明了三個Filter的子類,用於設計一系列的控制當前task是否需要被過濾的邏輯,結構上的邏輯其實比較簡單,主要就是需要將其聲明為Spring所管理的一個bean。下面是我們的控制邏輯:

@Service  public class ApplicationService {      @Autowired    private List<Filter> filters;      public void mockedClient() {      Task task = new Task(); // 這裡task一般是通過資料庫查詢得到的      for (Filter filter : filters) {        if (!filter.filter(task)) {          return;        }      }        // 過濾完成,後續是執行任務的邏輯    }  }  

在上述的控制邏輯中,對於過濾器的獲取,只需要通過Spring的自動注入即可,這裡注入的是一個List<Filter>,也就是說,如果我們有新的Filter實例需要參與責任鏈的過濾,只需要將其聲明為一個Spring容器所管理的bean即可。

這種責任鏈設計方式的優點在於鏈的控制比較簡單,只需要實現一個統一的介面即可,其基本上能夠滿足大部分的邏輯控制,但是對於某些需要動態調整鏈的需求其就無能為力了。

比如在執行到某個節點之後需要動態的判斷是否執行下一個節點,或者說要執行某些分叉的節點等等。這個時候我們就需要將鏈節點的傳遞工作交由各個節點進行。

2

節點控制模式

對於節點控制調用的方式,其主要有三個控制點:Handler,HandlerContext和Pipeline。Handler中是用於編寫具體的業務程式碼的;HandlerContext則主要是用於對Handler進行包裹,並且用於控制進行下一個節點的調用的。

Pipeline則主要是用於控制整體的流程調用的,比如對於任務的執行,其有任務的查詢,任務的過濾和執行任務等等流程,這些流程整體的邏輯控制就是由Pipeline來控制的,在每個流程中又包含了一系列的子流程,這些子流程則是由一個個的HandlerContext和Handler進行梳理的。這種責任鏈的控制方式整體邏輯如下圖所示:

從圖中可以看出,我們將整個流程通過Pipeline對象進行了抽象,這裡主要分為了三個步驟:查詢task,過濾task和執行task。在每個步驟中,我們都使用了一系列的鏈式調用。

圖中需要注意的是,在每次調用鏈的下一個節點的時候,我們都是通過具體的Handler進行的,也就是說是否進行鏈的下一個節點的調用,我們是通過業務實現方來進行動態控制的。

關於該模式的設計,我們首先需要強調的就是Handler介面的設計,其設計如下所示:

public interface Handler {      /**     * 處理接收到前端請求的邏輯     */    default void receiveTask(HandlerContext ctx, Request request) {      ctx.fireTaskReceived(request);    }      /**     * 查詢到task之後,進行task過濾的邏輯     */    default void filterTask(HandlerContext ctx, Task task) {      ctx.fireTaskFiltered(task);    }      /**     * task過濾完成之後,處理執行task的邏輯     */    default void executeTask(HandlerContext ctx, Task task) {      ctx.fireTaskExecuted(task);    }      /**     * 當實現的前面的方法拋出異常時,將使用當前方法進行異常處理,這樣可以將每個handler的異常     * 都只在該handler內進行處理,而無需額外進行捕獲     */    default void exceptionCaught(HandlerContext ctx, Throwable e) {      throw new RuntimeException(e);    }      /**     * 在整個流程中,保證最後一定會執行的程式碼,主要是用於一些清理工作     */    default void afterCompletion(HandlerContext ctx) {      ctx.fireAfterCompletion(ctx);    }  }  

這裡的Handler介面主要是對具體的業務邏輯的一個抽象,對於該Handler主要有如下幾點需要說明:

  • 在前面圖中Pipeline的每個層級中對應於該Handler都有一個方法,在需要進行具體的業務處理的時候,用戶只需要聲明一個bean,具體實現某個當前業務所需要處理的層級的方法即可,而無需管其他的邏輯;
  • 每個層級的方法中,第一個參數都是一個HandlerContext類型的,該參數主要是用於進行流程式控制制的,比如是否需要將當前層級的調用鏈往下繼續傳遞,這裡鏈的傳遞工作主要是通過ctx.fireXXX()方法進行的;
  • 每個層級的方法都有默認實現,默認實現方式就是將鏈的調用繼續往下進行傳遞;
  • 每個Handler中都有一個exceptionCaught()方法和afterCompletion()方法,這兩個方法分別用於異常控制和所有調用完成後的清理的,這裡的異常控制主要是捕獲當前Handler中的異常,而afterCompletion()方法則會保證在所有步驟之後一定會進行調用的,無論是否拋出異常;
  • 對於Handler的使用,我們希望能夠達到的目的是,適用方只需要實現該介面,並且使用某個註解來將其標誌為Spring的bean即可,而無需管整個Pipeline的組裝和流程式控制制。通過這種方式,我們即保留了每個Spring提供給我們的便利性,也使用了Pipeline模式的靈活性。

上述流程程式碼中,我們注意到,每個層級的方法中都有一個HandlerContext用於傳遞鏈相關的控制資訊,這裡我們來看一下其源碼:

@Component  @Scope("prototype")  public class HandlerContext {      HandlerContext prev;    HandlerContext next;    Handler handler;      private Task task;      public void fireTaskReceived(Request request) {      invokeTaskReceived(next(), request);    }      /**     * 處理接收到任務的事件     */    static void invokeTaskReceived(HandlerContext ctx, Request request) {      if (ctx != null) {        try {          ctx.handler().receiveTask(ctx, request);        } catch (Throwable e) {          ctx.handler().exceptionCaught(ctx, e);        }      }    }      public void fireTaskFiltered(Task task) {      invokeTaskFiltered(next(), task);    }      /**     * 處理任務過濾事件     */    static void invokeTaskFiltered(HandlerContext ctx, Task task) {      if (null != ctx) {        try {          ctx.handler().filterTask(ctx, task);        } catch (Throwable e) {          ctx.handler().exceptionCaught(ctx, e);        }      }    }      public void fireTaskExecuted(Task task) {      invokeTaskExecuted(next(), task);    }      /**     * 處理執行任務事件     */    static void invokeTaskExecuted(HandlerContext ctx, Task task) {      if (null != ctx) {        try {          ctx.handler().executeTask(ctx, task);        } catch (Exception e) {          ctx.handler().exceptionCaught(ctx, e);        }      }    }      public void fireAfterCompletion(HandlerContext ctx) {      invokeAfterCompletion(next());    }      static void invokeAfterCompletion(HandlerContext ctx) {      if (null != ctx) {        ctx.handler().afterCompletion(ctx);      }    }      private HandlerContext next() {      return next;    }      private Handler handler() {      return handler;    }  }  

HandlerContext中,我們需要說明如下幾點:

  • 之前Handler介面默認實現的ctx.fireXXX()方法,在這裡都委託給了對應的invokeXXX()方法進行調用,而且我們需要注意到,在傳遞給invokeXXX()方法的參數里,傳入的HandlerContext對象都是通過next()方法獲取到的。也就是說我們在Handler中調用ctx.fireXXX()方法時,都是在調用當前handler的下一個handler對應層級的方法,通過這種方式我們就實現了鏈的往下傳遞。
  • 在上一點中我們說到,在某個Handler中如果想讓鏈往下傳遞,只需要調用ctx.fireXXX()方法即可,也就是說,如果我們在某個Handler中,如果根據業務,當前層級已經調用完成,而無需調用後續的Handler,那麼我們就不需要調用ctx.fireXXX()方法即可;
  • HandlerContext中,我們也實現了invokeXXX()方法,該方法的主要作用是供給外部的Pipeline進行調用的,以開啟每個層級的鏈;
  • 在每個invokeXXX()方法中,我們都使用try…catch將當前層級的調用拋出的異常給捕獲了,然後調用ctx.handler().exceptionCaught()方法處理該異常,這也就是我們前面說的,如果想處理當前Handler中的異常,只需要實現該Handler中的exceptionCaught()方法即可,異常捕獲流程就是在這裡的HandlerContext中進行處理的;
  • HandlerContext的聲明處,我們需要注意到,其使用了@Component@Scope("prototype")註解進行標註了,這說明我們的HandlerContext是由Spring所管理的一個bean,並且由於我們每一個Handler實際上都由一個HandlerContext維護著,所以這裡必須聲明為prototype類型。通過這種方式,我們的HandlerContext也就具備了諸如Spring相關的bean的功能,也就能夠根據業務需求進行一些額外的處理了;

前面我們講解了HandlerHandlerContext的具體實現,以及實現的過程中需要注意的問題,下面我們就來看一下進行流程式控制制的Pipeline是如何實現的,如下是Pipeline介面的定義:

public interface Pipeline {      Pipeline fireTaskReceived();      Pipeline fireTaskFiltered();      Pipeline fireTaskExecuted();      Pipeline fireAfterCompletion();  }  

這裡 主要是定義了一個Pipeline介面,該介面定義了一系列的層級調用,是每個層級的入口方法。

關注微信公眾號:Java技術棧,在後台回復:設計模式,可以獲取我整理的 N 篇最新設計模式教程,都是乾貨。

如下是該介面的一個實現類:

@Component("pipeline")  @Scope("prototype")  public class DefaultPipeline implements Pipeline, ApplicationContextAware, InitializingBean {    // 創建一個默認的handler,將其注入到首尾兩個節點的HandlerContext中,其作用只是將鏈往下傳遞    private static final Handler DEFAULT_HANDLER = new Handler() {};      // 將ApplicationContext注入進來的主要原因在於,HandlerContext是prototype類型的,因而需要    // 通過ApplicationContext.getBean()方法來獲取其實例    private ApplicationContext context;      // 創建一個頭結點和尾節點,這兩個節點內部沒有做任何處理,只是默認的將每一層級的鏈往下傳遞,    // 這裡頭結點和尾節點的主要作用就是用於標誌整個鏈的首尾,所有的業務節點都在這兩個節點中間    private HandlerContext head;    private HandlerContext tail;      // 用於業務調用的request對象,其內部封裝了業務數據    private Request request;    // 用於執行任務的task對象    private Task task;      // 最初始的業務數據需要通過構造函數傳入,因為這是驅動整個pipeline所需要的數據,    // 一般通過外部調用方的參數進行封裝即可    public DefaultPipeline(Request request) {      this.request = request;    }      // 這裡我們可以看到,每一層級的調用都是通過HandlerContext.invokeXXX(head)的方式進行的,    // 也就是說我們每一層級鏈的入口都是從頭結點開始的,當然在某些情況下,我們也需要從尾節點開始鏈    // 的調用,這個時候傳入tail即可。    @Override    public Pipeline fireTaskReceived() {      HandlerContext.invokeTaskReceived(head, request);      return this;    }      // 觸發任務過濾的鏈調用    @Override    public Pipeline fireTaskFiltered() {      HandlerContext.invokeTaskFiltered(head, task);      return this;    }      // 觸發任務執行的鏈執行    @Override    public Pipeline fireTaskExecuted() {      HandlerContext.invokeTaskExecuted(head, task);      return this;    }      // 觸發最終完成的鏈的執行    @Override    public Pipeline fireAfterCompletion() {      HandlerContext.invokeAfterCompletion(head);      return this;    }      // 用於往Pipeline中添加節點的方法,讀者朋友也可以實現其他的方法用於進行鏈的維護    void addLast(Handler handler) {      HandlerContext handlerContext = newContext(handler);      tail.prev.next = handlerContext;      handlerContext.prev = tail.prev;      handlerContext.next = tail;      tail.prev = handlerContext;    }      // 這裡通過實現InitializingBean介面來達到初始化Pipeline的目的,可以看到,這裡初始的時候    // 我們通過ApplicationContext實例化了兩個HandlerContext對象,然後將head.next指向tail節點,    // 將tail.prev指向head節點。也就是說,初始時,整個鏈只有頭結點和尾節點。    @Override    public void afterPropertiesSet() throws Exception {      head = newContext(DEFAULT_HANDLER);      tail = newContext(DEFAULT_HANDLER);      head.next = tail;      tail.prev = head;    }      // 使用默認的Handler初始化一個HandlerContext    private HandlerContext newContext(Handler handler) {      HandlerContext context = this.context.getBean(HandlerContext.class);      context.handler = handler;      return context;    }      // 注入ApplicationContext對象    @Override    public void setApplicationContext(ApplicationContext applicationContext) {      this.context = applicationContext;    }  }  

關於DefaultPipeline的實現,主要有如下幾點需要說明:

  • DefaultPipeline使用@Component@Scope("prototype")註解進行了標註,前一個註解用於將其聲明為一個Spring容器所管理的bean,而後一個註解則用於表徵DefaultPipeline是一個多例類型的,很明顯,這裡的Pipeline是有狀態的。這裡需要進行說明的是,"有狀態"主要是因為我們可能會根據業務情況動態的調整個鏈的節點情況,而且這裡的RequestTask對象都是與具體的業務相關的,因而必須聲明為prototype類型;
  • 上面的示例中,Request對象是通過構造Pipeline對象的時候傳進來的,而Task對象則是在Pipeline的流轉過程中生成的,這裡比如通過完成fireTaskReceived()鏈的調用之後,就需要通過外部請求Request得到一個Task對象,從而進行整個Pipeline的後續處理;

這裡我們已經實現了PipelineHandlerContextHandler,知道這些bean都是被Spring所管理的bean,那麼我們接下來的問題主要在於如何進行整個鏈的組裝。

這裡的組裝方式比較簡單,其主要需要解決兩個問題:

  • 對於後續寫業務程式碼的人而言,其只需要實現一個Handler介面即可,而無需處理與鏈相關的所有邏輯,因而我們需要獲取到所有實現了Handler介面的bean;
  • 將實現了Handler介面的bean通過HandlerContext進行封裝,然後將其添加到Pipeline中。

這裡的第一個問題比較好處理,因為通過ApplicationContext就可以獲取實現了某個介面的所有bean,而第二個問題我們可以通過聲明一個實現了BeanPostProcessor介面的類來實現。如下是其實現程式碼:

@Component  public class HandlerBeanProcessor implements BeanPostProcessor, ApplicationContextAware {      private ApplicationContext context;      // 該方法會在一個bean初始化完成後調用,這裡主要是在Pipeline初始化完成之後獲取所有實現了    // Handler介面的bean,然後通過調用Pipeline.addLast()方法將其添加到pipeline中    @Override    public Object postProcessAfterInitialization(Object bean, String beanName) {      if (bean instanceof DefaultPipeline) {        DefaultPipeline pipeline = (DefaultPipeline) bean;        Map<String, Handler> handlerMap = context.getBeansOfType(Handler.class);        handlerMap.forEach((name, handler) -> pipeline.addLast(handler));      }        return bean;    }      @Override    public void setApplicationContext(ApplicationContext applicationContext) {      this.context = applicationContext;    }  }  

這裡我們整個鏈的維護工作就已經完成,可以看到,現在基本上已經實現了前面圖中整個鏈式流程的控制。

這裡需要說明的一點是,上面的HandlerBeanProcessor.postProcessAfterInitialization()方法的執行是在InitializingBean.afterPropertySet()方法之後執行的,也就是說這裡HandlerBeanProcessor在執行時,整個Pipeline是已經初始化完成了的。

下面我們來看一下外部客戶端如何進行整個鏈是流程的控制:

@Service  public class ApplicationService {      @Autowired    private ApplicationContext context;      public void mockedClient() {      Request request = new Request();  // request一般是通過外部調用獲取      Pipeline pipeline = newPipeline(request);      try {        pipeline.fireTaskReceived();        pipeline.fireTaskFiltered();        pipeline.fireTaskExecuted();      } finally {        pipeline.fireAfterCompletion();      }    }      private Pipeline newPipeline(Request request) {      return context.getBean(DefaultPipeline.class, request);    }  }  

這裡我們模擬了一個客戶端的調用,首先創建了一個Pipeline對象,然後依次調用其各個層級的方法,並且這裡我們使用try…finally結構來保證Pipeline.fireAfterCompletion()方法一定會執行。

如此我們就完成了整個責任鏈模式的構造。這裡我們使用前面用到的時效性過濾的filter來作為示例來實現一個Handler

@Component  public class DurationHandler implements Handler {      @Override    public void filterTask(HandlerContext ctx, Task task) {      System.out.println("時效性檢驗");      ctx.fireTaskFiltered(task);    }  }  

關於這裡的具體業務Handler我們需要說明的有如下幾點:

  • Handler必須使用@Conponent註解來將其聲明為Spring容器所管理的一個bean,這樣我們前面實現的HandlerBeanProcessor才能將其動態的添加到整個Pipeline中;
  • 在每個Handler中,需要根據當前的業務需要來實現具體的層級方法,比如這裡是進行時效性檢驗,就是"任務過濾"這一層級的邏輯,因為時效性檢驗通過我們才能執行這個task,因而這裡需要實現的是Handler.filterTask()方法,如果我們需要實現的是執行task的邏輯,那麼需要實現的就是Handler.executeTask()方法;
  • 在實現完具體的業務邏輯之後,我們可以根據當前的業務需要看是否需要將當前層級的鏈繼續往下傳遞,也就是這裡的ctx.fireTaskFiltered(task);方法的調用,我們可以看前面HandlerContext.fireXXX()方法就是會獲取當前節點的下一個節點,然後進行調用。如果根據業務需要,不需要將鏈往下傳遞,那麼就不需要調用ctx.fireTaskFiltered(task);

3

小結

如此,我們就通過兩種方式實現了責任鏈模式,而且我們實現的責任鏈模式都是符合"開-閉"原則的,也就是說後續我們要為鏈添加新的節點的時候,只需要根據規範實現相應的介面即可,而無需處理鏈的維護相關的工作。

關於第二種實現方式,這裡我們並沒有實現鏈節點的順序控制功能,以及如何動態的添加或刪除鏈的節點,更有甚者,如果控制每個Handler是單例的還是多例的。

當然,有了前面的框架,這些點實現起來也比較簡單,這裡權當起到一個拋磚引玉的作用,讀者朋友可根據自己的需要進行實現。

作者:愛寶貝丶

來源:my.oschina.net/zhangxufeng/blog/3055328