1. 程式人生 > >不得不知的責任鏈設計模式

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

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

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

責任鏈設計模式介紹

抽象介紹

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

責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。這種模式給予請求的型別,對請求的傳送者和接收者進行解耦。這種型別的設計模式屬於行為型模式。在這種模式中,通常每個接收者都包含對另一個接收者的引用

。如果一個物件能或不能處理該請求,它都會把相同的請求傳給下一個接收者,依此類推,直至責任鏈結束。

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

  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 白名單的介面,都會看到如下的呼叫棧資訊:

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

但這並不是這裡要說明的重點,這裡要看的是和我們自定義 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 方法就可以完成鏈條攔截器的傳遞,以圖來說明一下

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

總結與思考

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

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

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

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

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


靈魂追問

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

那些可以提高效率的工具

VNote

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