不得不知的責任鏈設計模式
世界上最遙遠的距離,不是生與死,而是它從你的世界路過無數次,你卻選擇視而不見,你無情,你冷酷啊......
被你忽略的就是責任鏈設計模式,希望它再次經過你身旁你會猛的發現,並對它微微一笑......
責任鏈設計模式介紹
抽象介紹
初次見面,瞭解表象,深入交流之後(看完文中的 demo 和框架中的實際應用後),你我便是靈魂之交(重新站在上帝視角來理解這個概念會更加深刻)
責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。這種模式給予請求的型別,對請求的傳送者和接收者進行解耦。這種型別的設計模式屬於行為型模式。在這種模式中,通常每個接收者都包含對另一個接收者的
引用
。如果一個物件能或不能處理該請求,它都會把相同的請求傳給下一個接收者,依此類推,直至責任鏈結束。
接下來將概念圖形化,用大腦圖形處理區理解此概念
- 上圖左側的 UML 類圖中,Sender 類不直接引用特定的接收器類。 相反,Sender 引用Handler 介面來處理請求
handler.handleRequest()
,這使得 Sender 獨立於具體的接收器(概念當中說的解耦
) Receiver1,Receiver2 和 Receiver3 類通過處理或轉發請求來實現 Handler 介面(取決於執行時條件) - 上圖右側的 UML 序列圖顯示了執行時互動,在此示例中,Sender 物件在 receiver1 物件(型別為Handler)上呼叫
handleRequest()
具象介紹
大家小時候都玩過擊鼓傳花的遊戲,遊戲的每個參與者就是責任鏈中的一個處理物件,花球就是待處理的請求,花球就在責任鏈(每個參與者中)進行傳遞,只不過責任鏈的結束時間點是鼓聲的結束. 來看 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++ 的使用)
有了這些我們看整個鏈是怎樣流轉起來的
上圖紅色框的最頂部呼叫了 StandardWrapperValve
的 invoke
方法:
...
// 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 設計可以多種多樣,比如文中通過 pos 遊標,或巢狀動態代理等.
在實際業務中,如果存在相同型別的任務需要順序執行,我們就可以拆分任務,將任務處理單元最小化,這樣易複用,然後串成一個鏈條,應用責任鏈設計模式就好了. 同時讀框架原始碼時如果看到 chain
關鍵字,也八九不離十是應用責任鏈設計模式了,看看框架是怎樣應用責任鏈設計模式的。
現在請你回看文章開頭,重新站在上帝視角審視責任鏈設計模式,什麼感覺,歡迎留言交流
靈魂追問
- Lambda 函數語言程式設計,你可以靈活應用,實現優雅程式設計嗎?
- 多個攔截器或過濾器,如果需要特定的責任鏈順序,我們都有哪些方式控制順序?
那些可以提高效率的工具
VNote
留言中有朋友讓我推薦一款 MarkDown 編輯器,我用過很多種(包括線上的),這次推薦 VNote, VNote 是一個受Vim啟發的更懂程式設計師和Markdown的一個筆記軟體, 都說 vim是最好的編輯器,更懂程式猿,但是多數還是應用在類 Unix 環境的 shell 指令碼編寫中,熟練使用 vim 也是我們必備的基本功,VNote 滿足這一切需求,同時提供非常多方便的快捷鍵滿足日常 MarkDown 的編寫. 通過寫文字順路學習 vim,快哉...