不得不知的責任鏈設計模式

tan日拱一兵發表於2019-06-21

世界上最遙遠的距離,不是生與死,而是它從你的世界路過無數次,你卻選擇視而不見,你無情,你冷酷啊......

love-1089665_640.jpg

被你忽略的就是責任鏈設計模式,希望它再次經過你身旁你會猛的發現,並對它微微一笑......

責任鏈設計模式介紹

抽象介紹

初次見面,瞭解表象,深入交流之後(看完文中的 demo 和框架中的實際應用後),你我便是靈魂之交(重新站在上帝視角來理解這個概念會更加深刻)

責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。這種模式給予請求的型別,對請求的傳送者和接收者進行解耦。這種型別的設計模式屬於行為型模式。在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個物件能或不能處理該請求,它都會把相同的請求傳給下一個接收者,依此類推,直至責任鏈結束。

接下來將概念圖形化,用大腦圖形處理區理解此概念
W3sDesign_Chain_of_Responsibility_Design_Pattern_UML.jpg

  1. 上圖左側的 UML 類圖中,Sender 類不直接引用特定的接收器類。 相反,Sender 引用Handler 介面來處理請求handler.handleRequest(),這使得 Sender 獨立於具體的接收器(概念當中說的解耦) Receiver1,Receiver2 和 Receiver3 類通過處理或轉發請求來實現 Handler 介面(取決於執行時條件)
  2. 上圖右側的 UML 序列圖顯示了執行時互動,在此示例中,Sender 物件在 receiver1 物件(型別為Handler)上呼叫 handleRequest(), 接收器 1 將請求轉發給接收器 2,接收器 2 又將請求轉發到處理(執行)請求的接收器3

具象介紹

大家小時候都玩過擊鼓傳花的遊戲,遊戲的每個參與者就是責任鏈中的一個處理物件,花球就是待處理的請求,花球就在責任鏈(每個參與者中)進行傳遞,只不過責任鏈的結束時間點是鼓聲的結束. 來看 Demo 和實際案例

Demo設計

程式猿和 log 是老交情了,使用 logback 配置日誌的時候有 ConsoleAppender 和 RollingFileAppender,這兩個 Appender 就組成了一個 log 記錄的責任鏈。下面的 demo 就是模擬 log 記錄:ConsoleLogger 列印所有級別的日誌;EmailLogger 記錄特定業務級別日誌 ;FileLogger 中只記錄 warning 和 Error 級別的日誌

抽象概念介紹中,說過實現責任鏈要有一個抽象接收器介面,和具體接收器,demo 中 Logger 就是這個抽象介面,由於該介面是 @FunctionalInterface (函式式介面), 它的具體實現就是 Lambda 表示式,關鍵程式碼都已做註釋標註

import java.util.Arrays;
import java.util.EnumSet;
import java.util.function.Consumer;

@FunctionalInterface
public interface Logger {
    /**
     * 列舉log等級
     */
    public enum LogLevel {
        //定義 log 等級
        INFO, DEBUG, WARNING, ERROR, FUNCTIONAL_MESSAGE, FUNCTIONAL_ERROR;

        public static LogLevel[] all() {
            return values();
        }
    }

    /**
     * 函式式介面中的唯一抽象方法
     * @param msg
     * @param severity
     */
    abstract void message(String msg, LogLevel severity);

    default Logger appendNext(Logger nextLogger) {
        return (msg, severity) -> {
            // 前序logger處理完才用當前logger處理
            message(msg, severity);
            nextLogger.message(msg, severity);
        };
    }

    static Logger logger(LogLevel[] levels, Consumer<String> writeMessage) {
        EnumSet<LogLevel> set = EnumSet.copyOf(Arrays.asList(levels));
        return (msg, severity) -> {
            // 判斷當前logger是否能處理傳遞過來的日誌級別
            if (set.contains(severity)) {
                writeMessage.accept(msg);
            }
        };
    }

    static Logger consoleLogger(LogLevel... levels) {
        return logger(levels, msg -> System.err.println("寫到終端: " + msg));
    }

    static Logger emailLogger(LogLevel... levels) {
        return logger(levels, msg -> System.err.println("通過郵件傳送: " + msg));
    }

    static Logger fileLogger(LogLevel... levels) {
        return logger(levels, msg -> System.err.println("寫到日誌檔案中: " + msg));
    }

    public static void main(String[] args) {
        /**
         * 構建一個固定順序的鏈 【終端記錄——郵件記錄——檔案記錄】
         * consoleLogger:終端記錄,可以列印所有等級的log資訊
         * emailLogger:郵件記錄,列印功能性問題 FUNCTIONAL_MESSAGE 和 FUNCTIONAL_ERROR 兩個等級的資訊
         * fileLogger:檔案記錄,列印 WARNING 和 ERROR 兩個等級資訊
         */
        
        Logger logger = consoleLogger(LogLevel.all())
                .appendNext(emailLogger(LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR))
                .appendNext(fileLogger(LogLevel.WARNING, LogLevel.ERROR));

        // consoleLogger 可以記錄所有 level 的資訊
        logger.message("進入到訂單流程,接收到引數,引數內容為XXXX", LogLevel.DEBUG);
        logger.message("訂單記錄生成.", LogLevel.INFO);

        // consoleLogger 處理完,fileLogger 要繼續處理
        logger.message("訂單詳細地址缺失", LogLevel.WARNING);
        logger.message("訂單省市區資訊缺失", LogLevel.ERROR);

        // consoleLogger 處理完,emailLogger 繼續處理
        logger.message("訂單簡訊通知服務失敗", LogLevel.FUNCTIONAL_ERROR);
        logger.message("訂單已派送.", LogLevel.FUNCTIONAL_MESSAGE);
    }
}

ConsoleLogger、EmailLogger 和 FileLogger 組成一個責任鏈,分工明確;FileLogger 中包含 EmailLogger 的引用,EmailLogger 中包含 ConsoleLogger 的引用,當前具體 Logger 是否記錄日誌的判斷條件是傳入的 log level 是否在它的責任範圍內. 最終呼叫 message 方法時的責任鏈順序 ConsoleLogger -> EmailLogger -> FileLogger. 如果不能很好的理解 Lambda ,我們可以通過介面與實現類的方式實現

案例介紹

為什麼說責任鏈模式從我們身邊路過無數次,你卻忽視它,看下面這兩個案例,你也許會一聲長嘆.

Filter過濾器

下面這段程式碼有沒有很熟悉,沒錯,我們配置攔截器重寫 doFilter 方法時都會執行下面這段程式碼,傳遞給下一個 Filter 進行處理

chain.doFilter(request, response);

隨意定義一個攔截器 CustomFilter,都要執行 chain.doFilter(request, response) 方法進行 Filter 鏈的傳遞

import javax.servlet.*;
import java.io.IOException;

/**
 * @author tan日拱一兵
 * @date 2019-06-19 13:45
 */
public class CustomFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}

以 debug 模式啟動應用,隨意請求一個沒有被加入 filter 白名單的介面,都會看到如下的呼叫棧資訊:
Xnip2019-06-19_13-53-51.jpg

紅色標記框的內容是 Tomcat 容器設定的責任鏈,從 Engine 到 Cotext 再到 Wrapper 都是通過這個責任鏈傳遞請求,如下類圖所示,他們都實現了 Valve 介面中的 invoke 方法
Xnip2019-06-19_13-54-39.jpg

但這並不是這裡要說明的重點,這裡要看的是和我們自定義 Filter 息息相關的藍色框的內容 ApplicationFilterChain ,我們要了解它是如何應用責任鏈設計模式的?

既然是責任鏈,所有的過濾器是怎樣加入到這個鏈條當中的呢?

ApplicationFilterChain 類中定義了一個 ApplicationFilterConfig 型別的陣列,用來儲存過濾器

/**
 * Filters.
 */
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

ApplicationFilterConfig 是什麼?

ApplicationFilterConfig 是 Filter 的容器,類的描述是:在 web 第一次啟動的時候管理 filter 的例項化

/**
 * Implementation of a <code>javax.servlet.FilterConfig</code> useful in
 * managing the filter instances instantiated when a web application
 * is first started.
 *
 * @author Craig R. McClanahan
 */

ApplicationFilterConfig[] 是一個大小為 0 的空陣列,那它在什麼時候被重新賦值的呢?

是在 ApplicationFilterChain 類呼叫 addFilter 的時候重新賦值的

/**
 * The int which gives the current number of filters in the chain.
 */
private int n = 0;

public static final int INCREMENT = 10;

/**
 * Add a filter to the set of filters that will be executed in this chain.
 *
 * @param filterConfig The FilterConfig for the servlet to be executed
 */
void addFilter(ApplicationFilterConfig filterConfig) {

    // Prevent the same filter being added multiple times
    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;

}

變數 n 用來記錄當前過濾器鏈裡面擁有的過濾器數目,預設情況下 n 等於 0,ApplicationFilterConfig 物件陣列的長度也等於0,所以當第一次呼叫 addFilter() 方法時,if (n == filters.length) 的條件成立,ApplicationFilterConfig 陣列長度被改變。之後 filters[n++] = filterConfig;將變數 filterConfig 放入 ApplicationFilterConfig 陣列中並將當前過濾器鏈裡面擁有的過濾器數目+1(注意這裡 n++ 的使用)

有了這些我們看整個鏈是怎樣流轉起來的
上圖紅色框的最頂部呼叫了 StandardWrapperValveinvoke 方法:

...
// Create the filter chain for this request
ApplicationFilterChain filterChain =
        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
...
filterChain.doFilter(request.getRequest(), response.getResponse());

通過 ApplicationFilterFactory.createFilterChain 例項化 ApplicationFilterChain (工廠模式),呼叫 filterChain.doFilter 方法正式進入責任鏈條,來看該方法,方法內部呼叫了 internalDoFilter 方法,來看關鍵程式碼:

/**
 * The int which is used to maintain the current position
 * in the filter chain.
 */
private int pos = 0;

// Call the next filter if there is one
if (pos < n) {
    ApplicationFilterConfig filterConfig = filters[pos++];
    try {
        Filter filter = filterConfig.getFilter();

        if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                filterConfig.getFilterDef().getAsyncSupported())) {
            request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
        }
        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 {
            filter.doFilter(request, response, this);
        }
    } catch (IOException | ServletException | RuntimeException e) {
        throw e;
    } catch (Throwable e) {
        e = ExceptionUtils.unwrapInvocationTargetException(e);
        ExceptionUtils.handleThrowable(e);
        throw new ServletException(sm.getString("filterChain.filter"), e);
    }
    return;
}

pos 變數用來標記 filter chain 執行的當前位置,然後呼叫 filter.doFilter(request, response, this); 傳遞 this (ApplicationFilterChain)進行鏈路傳遞,直至 pos > n 的時候停止 (類似擊鼓傳花中的鼓聲停止),即所有攔截器都執行完畢。

繼續向下看另外一個從我們身邊路過無數次的責任鏈模式

Mybatis攔截器

Mybatis 攔截器執行過程解析 中留一個問題彩蛋責任鏈模式,那在 Mybatis 攔截器中是怎樣應用的呢?

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

以 Executor 型別的攔截為例,如果存在多個同型別的攔截器,當執行到 pluginAll 方法時,他們是怎樣在責任鏈條中傳遞的呢?
呼叫interceptor.plugin(target) 為當前 target 生成代理物件,當多個攔截器遍歷的時候,也就是會繼續為代理物件再生成代理物件,直至遍歷結束,拿到最外層的代理物件,觸發 invoke 方法就可以完成鏈條攔截器的傳遞,以圖來說明一下
Xnip2019-06-19_15-19-36.jpg

看了這些,你和責任鏈設計模式會是靈魂之交嗎?

總結與思考

敲黑板,敲黑板,敲黑板 (重要的事情敲三次黑板)
看了這麼多之後,我們要總結出責任鏈設計模式的關鍵了

  1. 設計一個鏈條,和抽象處理方法
  2. 將具體處理器初始化到鏈條中,並做抽象方法具體的實現
  3. 具體處理器之間的引用和處理條件判斷
  4. 設計鏈條結束標識

    1,2 都可以很模組化設計,3,4 設計可以多種多樣,比如文中通過 pos 遊標,或巢狀動態代理等.

在實際業務中,如果存在相同型別的任務需要順序執行,我們就可以拆分任務,將任務處理單元最小化,這樣易複用,然後串成一個鏈條,應用責任鏈設計模式就好了. 同時讀框架原始碼時如果看到 chain 關鍵字,也八九不離十是應用責任鏈設計模式了,看看框架是怎樣應用責任鏈設計模式的。

現在請你回看文章開頭,重新站在上帝視角審視責任鏈設計模式,什麼感覺,歡迎留言交流


靈魂追問

  1. Lambda 函數語言程式設計,你可以靈活應用,實現優雅程式設計嗎?
  2. 多個攔截器或過濾器,如果需要特定的責任鏈順序,我們都有哪些方式控制順序?

那些可以提高效率的工具

a (1).png

VNote

留言中有朋友讓我推薦一款 MarkDown 編輯器,我用過很多種(包括線上的),這次推薦 VNote, VNote 是一個受Vim啟發的更懂程式設計師和Markdown的一個筆記軟體, 都說 vim是最好的編輯器,更懂程式猿,但是多數還是應用在類 Unix 環境的 shell 指令碼編寫中,熟練使用 vim 也是我們必備的基本功,VNote 滿足這一切需求,同時提供非常多方便的快捷鍵滿足日常 MarkDown 的編寫. 通過寫文字順路學習 vim,快哉...

Xnip2019-06-19_15-56-41.jpg


a (1).png

相關文章