世界上最遙遠的距離,不是生與死,而是它從你的世界路過無數次,你卻選擇視而不見,你無情,你冷酷啊......java
被你忽略的就是責任鏈設計模式,但願它再次通過你身旁你會猛的發現,並對它微微一笑......程序員
初次見面,瞭解表象,深刻交流以後(看完文中的 demo 和框架中的實際應用後),你我即是靈魂之交(從新站在上帝視角來理解這個概念會更加深入)web
責任鏈模式(Chain of Responsibility Pattern)爲請求建立了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬於行爲型模式。在這種模式中,一般每一個接收者都包含對另外一個接收者的
引用
。若是一個對象能或不能處理該請求,它都會把相同的請求傳給下一個接收者,依此類推,直至責任鏈結束。shell
接下來將概念圖形化,用大腦圖形處理區理解此概念 編程
handler.handleRequest()
,這使得 Sender 獨立於具體的接收器(概念當中說的解耦
) Receiver1,Receiver2 和 Receiver3 類經過處理或轉發請求來實現 Handler 接口(取決於運行時條件)handleRequest()
, 接收器 1 將請求轉發給接收器 2,接收器 2 又將請求轉發處處理(執行)請求的接收器3你們小時候都玩過擊鼓傳花的遊戲,遊戲的每一個參與者就是責任鏈中的一個處理對象,花球就是待處理的請求,花球就在責任鏈(每一個參與者中)進行傳遞,只不過責任鏈的結束時間點是鼓聲的結束. 來看 Demo 和實際案例vim
程序猿和 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 ,咱們能夠經過接口與實現類的方式實現app
爲何說責任鏈模式從咱們身邊路過無數次,你卻忽視它,看下面這兩個案例,你也許會一聲長嘆.框架
下面這段代碼有沒有很熟悉,沒錯,咱們配置攔截器重寫 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 攔截器中是怎樣應用的呢?
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
關鍵字,也八九不離十是應用責任鏈設計模式了,看看框架是怎樣應用責任鏈設計模式的。
如今請你回看文章開頭,從新站在上帝視角審視責任鏈設計模式,什麼感受,歡迎留言交流
留言中有朋友讓我推薦一款 MarkDown 編輯器,我用過不少種(包括在線的),此次推薦 VNote, VNote 是一個受Vim啓發的更懂程序員和Markdown的一個筆記軟件, 都說 vim是最好的編輯器,更懂程序猿,可是多數仍是應用在類 Unix 環境的 shell 腳本編寫中,熟練使用 vim 也是咱們必備的基本功,VNote 知足這一切需求,同時提供很是多方便的快捷鍵知足平常 MarkDown 的編寫. 經過寫文字順路學習 vim,快哉...