設計模式 | 責任鏈模式及典型應用

小旋鋒發表於2018-10-31

本文的主要內容:

  • 介紹責任鏈模式
  • 請假流程示例
  • 責任鏈模式總結
  • 原始碼分析Tomcat Filter中的責任鏈模式

更多內容可訪問我的個人部落格:laijianfeng.org
關注【小旋鋒】微信公眾號,及時接收博文推送

長按關注【小旋鋒】微信公眾號
****


責任鏈模式

一個事件需要經過多個物件處理是一個挺常見的場景,譬如採購審批流程,請假流程,軟體開發中的異常處理流程,web請求處理流程等各種各樣的流程,可以考慮使用責任鏈模式來實現。

以請假流程為例,一般公司普通員工的請假流程簡化如下:

普通員工請假簡化流程圖

普通員工發起一個請假申請,當請假天數小於3天時只需要得到主管批准即可;當請假天數大於3天時,主管批准後還需要提交給經理審批,經理審批通過,若請假天數大於7天還需要進一步提交給總經理審批。

使用 if-else 來實現這個請假流程的簡化程式碼如下:

public class LeaveApproval {
    public boolean process(String request, int number) {
        boolean result = handleByDirector(request); // 主管處理
        if (result == false) {  // 主管不批准
            return false;
        } else if (number < 3) {    // 主管批准且天數小於 3
            return true;
        }

        result = handleByManager(request); // 準管批准且天數大於等於 3,提交給經理處理
        if (result == false) {   // 經理不批准
            return false;
        } else if (number < 7) { // 經理批准且天數小於 7
            return true;
        }

        result = handleByTopManager(request);   // 經理批准且天數大於等於 7,提交給總經理處理
        if (result == false) { // 總經理不批准
            return false;
        }
        return true;    // 總經理最後批准
    }

    public boolean handleByDirector(String request) {
        // 主管處理該請假申請
    }

    public boolean handleByManager(String request) {
        // 經理處理該請假申請
    }

    public boolean handleByTopManager(String request) {
        // 總經理處理該請假申請
    }
}
複製程式碼

問題看起來很簡單,三下五除二就搞定,但是該方案存在幾個問題

  1. LeaveApproval 類比較龐大,各個上級的審批方法都集中在該類中,違反了 "單一職責原則",測試和維護難度大

  2. 當需要修改該請假流程,譬如增加當天數大於30天時還需提交給董事長處理,必須修改該類原始碼(並重新進行嚴格地測試),違反了 "開閉原則"

  3. 該流程缺乏靈活性,流程確定後不可再修改(除非修改原始碼),客戶端無法定製流程

使用責任鏈模式可以解決上述問題。

定義

責任鏈模式(Chain of Responsibility Pattern):避免請求傳送者與接收者耦合在一起,讓多個物件都有可能接收請求,將這些物件連線成一條鏈,並且沿著這條鏈傳遞請求,直到有物件處理它為止。職責鏈模式是一種物件行為型模式。

角色

Handler(抽象處理者):它定義了一個處理請求的介面,一般設計為抽象類,由於不同的具體處理者處理請求的方式不同,因此在其中定義了抽象請求處理方法。因為每一個處理者的下家還是一個處理者,因此在抽象處理者中定義了一個抽象處理者型別的物件,作為其對下家的引用。通過該引用,處理者可以連成一條鏈。

ConcreteHandler(具體處理者):它是抽象處理者的子類,可以處理使用者請求,在具體處理者類中實現了抽象處理者中定義的抽象請求處理方法,在處理請求之前需要進行判斷,看是否有相應的處理許可權,如果可以處理請求就處理它,否則將請求轉發給後繼者;在具體處理者中可以訪問鏈中下一個物件,以便請求的轉發。

類圖如下所示:

責任鏈模式.類圖

純與不純的責任鏈模式

純的責任鏈模式

  • 一個具體處理者物件只能在兩個行為中選擇一個:要麼承擔全部責任,要麼將責任推給下家,不允許出現某一個具體處理者物件在承擔了一部分或全部責任後 又將責任向下傳遞的情況
  • 一個請求必須被某一個處理者物件所接收,不能出現某個請求未被任何一個處理者物件處理的情況

不純的責任鏈模式

  • 允許某個請求被一個具體處理者部分處理後再向下傳遞
  • 或者一個具體處理者處理完某請求後其後繼處理者可以繼續處理該請求
  • 而且一個請求可以最終不被任何處理者物件所接收

示例

使用責任鏈模式(不純)重構請假流程

請假資訊類,包含請假人姓名和請假天數

@Data
@AllArgsConstructor
public class LeaveRequest {
    private String name;    // 請假人姓名
    private int numOfDays;  // 請假天數
}
複製程式碼

抽象處理者類 Handler,維護一個 nextHandler 屬性,該屬性為當前處理者的下一個處理者的引用;宣告瞭抽象方法 process

@Data
public abstract class Handler {
    protected String name; // 處理者姓名
    protected Handler nextHandler;  // 下一個處理者

    public Handler(String name) {
        this.name = name;
    }

    public abstract boolean process(LeaveRequest leaveRequest); // 處理請假
}
複製程式碼

三個具體處理類,分別實現了抽象處理類的 process 方法

// 主管處理者
public class Director extends Handler {
    public Director(String name) {
        super(name);
    }

    @Override
    public boolean process(LeaveRequest leaveRequest) {
        boolean result = (new Random().nextInt(10)) > 3; // 隨機數大於3則為批准,否則不批准
        String log = "主管<%s> 審批 <%s> 的請假申請,請假天數: <%d> ,審批結果:<%s> ";
        System.out.println(String.format(log, this.name, leaveRequest.getName(), leaveRequest.getNumOfDays(), result == true ? "批准" : "不批准"));

        if (result == false) {  // 不批准
            return false;
        } else if (leaveRequest.getNumOfDays() < 3) { // 批准且天數小於3,返回true
            return true;
        }
        return nextHandler.process(leaveRequest);   // 批准且天數大於等於3,提交給下一個處理者處理
    }
}

// 經理
public class Manager extends Handler {
    public Manager(String name) {
        super(name);
    }

    @Override
    public boolean process(LeaveRequest leaveRequest) {
        boolean result = (new Random().nextInt(10)) > 3; // 隨機數大於3則為批准,否則不批准
        String log = "經理<%s> 審批 <%s> 的請假申請,請假天數: <%d> ,審批結果:<%s> ";
        System.out.println(String.format(log, this.name, leaveRequest.getName(), leaveRequest.getNumOfDays(), result == true ? "批准" : "不批准"));

        if (result == false) {  // 不批准
            return false;
        } else if (leaveRequest.getNumOfDays() < 7) { // 批准且天數小於7
            return true;
        }
        return nextHandler.process(leaveRequest);   // 批准且天數大於等於7,提交給下一個處理者處理
    }
}

// 總經理
public class TopManager extends Handler {
    public TopManager(String name) {
        super(name);
    }

    @Override
    public boolean process(LeaveRequest leaveRequest) {
        boolean result = (new Random().nextInt(10)) > 3; // 隨機數大於3則為批准,否則不批准
        String log = "總經理<%s> 審批 <%s> 的請假申請,請假天數: <%d> ,審批結果:<%s> ";
        System.out.println(String.format(log, this.name, leaveRequest.getName(), leaveRequest.getNumOfDays(), result == true ? "批准" : "不批准"));

        if (result == false) { // 總經理不批准
            return false;
        }
        return true;    // 總經理最後批准
    }
}
複製程式碼

客戶端測試

public class Client {
    public static void main(String[] args) {
        Handler zhangsan = new Director("張三");
        Handler lisi = new Manager("李四");
        Handler wangwu = new TopManager("王五");

        // 建立責任鏈
        zhangsan.setNextHandler(lisi);
        lisi.setNextHandler(wangwu);

        // 發起請假申請
        boolean result1 = zhangsan.process(new LeaveRequest("小旋鋒", 1));
        System.out.println("最終結果:" + result1 + "\n");

        boolean result2 = zhangsan.process(new LeaveRequest("小旋鋒", 4));
        System.out.println("最終結果:" + result2 + "\n");

        boolean result3 = zhangsan.process(new LeaveRequest("小旋鋒", 8));
        System.out.println("最終結果:" + result3 + "\n");
    }
}
複製程式碼

可能的結果如下:(由於是否批准是通過隨機數模擬的,所以你的結果可能跟我不同)

主管<張三> 審批 <小旋鋒> 的請假申請,請假天數: <1> ,審批結果:<批准> 
最終結果:true

主管<張三> 審批 <小旋鋒> 的請假申請,請假天數: <4> ,審批結果:<不批准> 
最終結果:false

主管<張三> 審批 <小旋鋒> 的請假申請,請假天數: <8> ,審批結果:<批准> 
經理<李四> 審批 <小旋鋒> 的請假申請,請假天數: <8> ,審批結果:<批准> 
總經理<王五> 審批 <小旋鋒> 的請假申請,請假天數: <8> ,審批結果:<批准> 
最終結果:true
複製程式碼

類圖如下所示

示例.責任鏈類圖

總結

職責鏈模式的主要優點

  • 物件僅需知道該請求會被處理即可,且鏈中的物件不需要知道鏈的結構,由客戶端負責鏈的建立,降低了系統的耦合度

  • 請求處理物件僅需維持一個指向其後繼者的引用,而不需要維持它對所有的候選處理者的引用,可簡化物件的相互連線

  • 在給物件分派職責時,職責鏈可以給我們更多的靈活性,可以在執行時對該鏈進行動態的增刪改,改變處理一個請求的職責

  • 新增一個新的具體請求處理者時無須修改原有程式碼,只需要在客戶端重新建鏈即可,符合 "開閉原則"

職責鏈模式的主要缺點

  • 一個請求可能因職責鏈沒有被正確配置而得不到處理

  • 對於比較長的職責鏈,請求的處理可能涉及到多個處理物件,系統效能將受到一定影響,且不方便除錯

  • 可能因為職責鏈建立不當,造成迴圈呼叫,導致系統陷入死迴圈

適用場景

  • 有多個物件可以處理同一個請求,具體哪個物件處理該請求待執行時刻再確定,客戶端只需將請求提交到鏈上,而無須關心請求的處理物件是誰以及它是如何處理的

  • 在不明確指定接收者的情況下,向多個物件中的一個提交一個請求

  • 可動態指定一組物件處理請求,客戶端可以動態建立職責鏈來處理請求,還可以改變鏈中處理者之間的先後次序

責任鏈模式的典型應用

Tomcat 過濾器中的責任鏈模式

Servlet 過濾器是可用於 Servlet 程式設計的 Java 類,可以實現以下目的:在客戶端的請求訪問後端資源之前,攔截這些請求;在伺服器的響應傳送回客戶端之前,處理這些響應。

Servlet 定義了過濾器介面 Filter 和過濾器連結口 FilterChain 的原始碼如下

public interface Filter {
    public void init(FilterConfig filterConfig) throws ServletException;
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
    public void destroy();
}

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
複製程式碼

我們自定義一個過濾器的步驟是:

1)寫一個過濾器類,實現 javax.servlet.Filter 介面,如下所示

public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 做一些自定義處理....
        System.out.println("執行doFilter()方法之前...");
        chain.doFilter(request, response);              // 傳遞請求給下一個過濾器
        System.out.println("執行doFilter()方法之後...");
    }

    @Override
    public void destroy() {
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
}
複製程式碼

2)在 web.xml 檔案中增加該過濾器的配置,譬如下面是攔截所有請求

<filter>  
        <filter-name>MyFilter</filter-name>  
        <filter-class>com.whirly.filter.MyFilter</filter-class>  
</filter>
  
<filter-mapping>  
        <filter-name>MyFilter</filter-name>  
        <url-pattern>/*</url-pattern>  
</filter-mapping>
複製程式碼

當啟動 Tomcat 是我們的過濾器就可以發揮作用了。那麼過濾器是怎樣執行的呢?

Tomcat 有 Pipeline Valve機制,也是使用了責任鏈模式,一個請求會在 Pipeline 中流轉,Pipeline 會呼叫相應的 Valve 完成具體的邏輯處理;
其中的一個基礎Valve為 StandardWrapperValve,其中的一個作用是呼叫 ApplicationFilterFactory 生成 Filter鏈,具體程式碼在 invoke 方法中

在執行過濾器之前需要完成過濾器的載入和初始化,以及根據配置資訊生成過濾器鏈

  1. 過濾器的載入具體是在 ContextConfig 類的 configureContext 方法中,分別載入 filterfilterMap 的相關資訊,並儲存在上下文環境中

  2. 過濾器的初始化在 StandardContext 類的 startInternal 方法中完成,儲存在 filterConfigs 中並存到上下文環境中

  3. 請求流轉到 StandardWrapperValve 時,在 invoke 方法中,會根據過濾器對映配置資訊,為每個請求建立對應的 ApplicationFilterChain,其中包含了目標 Servlet 以及對應的過濾器鏈,並呼叫過濾器鏈的 doFilter 方法執行過濾器

StandardWrapperValve 呼叫 ApplicationFilterFactory 為請求建立過濾器鏈並呼叫過濾器鏈的關鍵程式碼如下:

final class StandardWrapperValve extends ValveBase {
    public final void invoke(Request request, Response response) throws IOException, ServletException {
        // 省略其他的邏輯處理...
        // 呼叫 ApplicationFilterChain.createFilterChain() 建立過濾器鏈
        ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
        
        if (servlet != null && filterChain != null) {
            // 省略
        } else if (request.isAsyncDispatching()) {
            request.getAsyncContextInternal().doInternalDispatch();
        } else if (comet) {
            filterChain.doFilterEvent(request.getEvent());
        } else {
            // 呼叫過濾器鏈的 doFilter 方法開始過濾
            filterChain.doFilter(request.getRequest(), response.getResponse());
        }
複製程式碼

過濾器鏈 ApplicationFilterChain 的關鍵程式碼如下,過濾器鏈實際是一個 ApplicationFilterConfig 陣列

final class ApplicationFilterChain implements FilterChain, CometFilterChain {
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; // 過濾器鏈
    private Servlet servlet = null; // 目標
    // ...
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if( Globals.IS_SECURITY_ENABLED ) {
            // ...
        } else {
            internalDoFilter(request,response); // 呼叫 internalDoFilter 方法
        }
    }
    
    private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        // Call the next filter if there is one
        if (pos < n) {
            // 從過濾器陣列中取出當前過濾器配置,然後下標自增1
            ApplicationFilterConfig filterConfig = filters[pos++];
            Filter filter = null;
            try {
                filter = filterConfig.getFilter();  // 從過濾器配置中取出該 過濾器物件

                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal = ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
                } else {
                    // 呼叫過濾器的 doFilter,完成一個過濾器的過濾功能
                    filter.doFilter(request, response, this);
                }
            return;  // 這裡很重要,不會重複執行後面的  servlet.service(request, response)
        }
        
        // 執行完過濾器鏈的所有過濾器之後,呼叫 Servlet 的 service 完成請求的處理
        if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse)) {
            if( Globals.IS_SECURITY_ENABLED ) {
                
            } else {
                servlet.service(request, response);
            }
        } else {
            servlet.service(request, response);
        }
    }
    // 省略...
}
複製程式碼

過濾器

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("執行doFilter()方法之前...");
        chain.doFilter(request, response);              // 傳遞請求給下一個過濾器
        System.out.println("執行doFilter()方法之後...");
    }
複製程式碼

當下標小於過濾器陣列長度 n 時,說明過濾器鏈未執行完,所以從陣列中取出當前過濾器,呼叫過濾器的 doFilter 方法完成過濾處理,在過濾器的 doFilter 中又呼叫 FilterChaindoFilter,回到 ApplicationFilterChain,又繼續根據下標是否小於陣列長度來判斷過濾器鏈是否已執行完,未完則繼續從陣列取出過濾器並呼叫 doFilter 方法,所以這裡的過濾鏈是通過巢狀遞迴的方式來串成一條鏈。

當全部過濾器都執行完畢,最後一次進入 ApplicationFilterChain.doFilter 方法的時候 pos < n 為false,不進入 if (pos < n) 中,而是執行後面的程式碼,判斷 (request instanceof HttpServletRequest) && (response instanceof HttpServletResponse),若為 http 請求則呼叫 servlet.service(request, response); 來處理該請求。

處理完畢之後沿著呼叫過濾器的順序反向退棧,分別執行過濾器中 chain.doFilter() 之後的處理邏輯,需要注意的是if (pos < n) 方法體的最後有一個 return;,這樣就保證了只有最後一次進入 ApplicationFilterChain.doFilter 方法的呼叫能夠執行後面的 servlet.service(request, response) 方法

畫一個簡要的呼叫棧如下所示:

Tomcat 過濾器鏈呼叫棧

ApplicationFilterChain 類扮演了抽象處理者角色,具體處理者角色由各個 Filter 扮演

其他的責任鏈模式的典型應用

其他的責任鏈模式的應用基本都是大同小異

FilterChain 的實現類

這裡列舉幾個典型應用:

  1. Netty 中的 PipelineChannelHandler 通過責任鏈設計模式來組織程式碼邏輯
  2. Spring Security 使用責任鏈模式,可以動態地新增或刪除責任(處理 request 請求)
  3. Spring AOP 通過責任鏈模式來管理 Advisor
  4. Dubbo Filter 過濾器鏈也是用了責任鏈模式(連結串列),可以對方法呼叫做一些過濾處理,譬如超時(TimeoutFilter),異常(ExceptionFilter),Token(TokenFilter)等
  5. Mybatis 中的 Plugin 機制使用了責任鏈模式,配置各種官方或者自定義的 Plugin,與 Filter 類似,可以在執行 Sql 語句的時候做一些操作

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析
責任鏈設計模式(過濾器、攔截器)

後記

歡迎評論、轉發、分享,您的支援是我最大的動力

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用
設計模式 | 裝飾者模式及典型應用
設計模式 | 介面卡模式及典型應用
設計模式 | 享元模式及典型應用
設計模式 | 組合模式及典型應用
設計模式 | 模板方法模式及典型應用
設計模式 | 迭代器模式及典型應用
設計模式 | 策略模式及典型應用
設計模式 | 觀察者模式及典型應用
設計模式 | 備忘錄模式及典型應用
設計模式 | 中介者模式及典型應用

相關文章