世界上最遙遠的距離,不是生與死,而是它從你的世界路過無數次,你卻選擇視而不見,你無情,你冷酷啊......
被你忽略的就是責任鏈設計模式,希望它再次經過你身旁你會猛的發現,并對它微微一笑......
責任鏈設計模式介紹 抽象介紹初次見面,了解表象,深入交流之后(看完文中的 demo 和框架中的實際應用后),你我便是靈魂之交(重新站在上帝視角來理解這個概念會更加深刻)
責任鏈模式(Chain of Responsibility Pattern)為請求創建了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬于行為型模式。在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個對象能或不能處理該請求,它都會把相同的請求傳給下一個接收者,依此類推,直至責任鏈結束。
接下來將概念圖形化,用大腦圖形處理區理解此概念
上圖左側的 UML 類圖中,Sender 類不直接引用特定的接收器類。 相反,Sender 引用Handler 接口來處理請求handler.handleRequest(),這使得 Sender 獨立于具體的接收器(概念當中說的解耦) Receiver1,Receiver2 和 Receiver3 類通過處理或轉發請求來實現 Handler 接口(取決于運行時條件)
上圖右側的 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, ConsumerwriteMessage) { EnumSet 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 javax.servlet.FilterConfig
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 Listinterceptors = new ArrayList (); 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 getInterceptors() { return Collections.unmodifiableList(interceptors); } }
以 Executor 類型的攔截為例,如果存在多個同類型的攔截器,當執行到 pluginAll 方法時,他們是怎樣在責任鏈條中傳遞的呢?
調用interceptor.plugin(target) 為當前 target 生成代理對象,當多個攔截器遍歷的時候,也就是會繼續為代理對象再生成代理對象,直至遍歷結束,拿到最外層的代理對象,觸發 invoke 方法就可以完成鏈條攔截器的傳遞,以圖來說明一下
看了這些,你和責任鏈設計模式會是靈魂之交嗎?
總結與思考敲黑板,敲黑板,敲黑板 (重要的事情敲三次黑板)
看了這么多之后,我們要總結出責任鏈設計模式的關鍵了
設計一個鏈條,和抽象處理方法
將具體處理器初始化到鏈條中,并做抽象方法具體的實現
具體處理器之間的引用和處理條件判斷
設計鏈條結束標識
1,2 都可以很模塊化設計,3,4 設計可以多種多樣,比如文中通過 pos 游標,或嵌套動態代理等.
在實際業務中,如果存在相同類型的任務需要順序執行,我們就可以拆分任務,將任務處理單元最小化,這樣易復用,然后串成一個鏈條,應用責任鏈設計模式就好了. 同時讀框架源碼時如果看到 chain 關鍵字,也八九不離十是應用責任鏈設計模式了,看看框架是怎樣應用責任鏈設計模式的。
現在請你回看文章開頭,重新站在上帝視角審視責任鏈設計模式,什么感覺,歡迎留言交流靈魂追問
Lambda 函數式編程,你可以靈活應用,實現優雅編程嗎?
多個攔截器或過濾器,如果需要特定的責任鏈順序,我們都有哪些方式控制順序?
那些可以提高效率的工具
](https://upload-images.jianshu...
留言中有朋友讓我推薦一款 MarkDown 編輯器,我用過很多種(包括在線的),這次推薦 VNote, VNote 是一個受Vim啟發的更懂程序員和Markdown的一個筆記軟件, 都說 vim是最好的編輯器,更懂程序猿,但是多數還是應用在類 Unix 環境的 shell 腳本編寫中,熟練使用 vim 也是我們必備的基本功,VNote 滿足這一切需求,同時提供非常多方便的快捷鍵滿足日常 MarkDown 的編寫. 通過寫文字順路學習 vim,快哉...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77867.html
摘要:今天就我和大家來談談大數據領域的一些新變化新趨勢。結語以上四個方面是數據科學在實踐發展中提出的新需求,誰能在這些方面得到好的成績,誰便會在這個大數據時代取得領先的位置。 從2012年開始,幾乎人人(至少是互聯網界)言必稱大數據,似乎不和大數據沾點邊都不好意思和別人聊天。從2016年開始,大數據系統逐步開始在企業中進入部署階段,大數據的炒作逐漸散去,隨之而來的是應用的蓬勃發展期,一些代表...
摘要:今天就我和大家來談談大數據領域的一些新變化新趨勢。結語以上四個方面是數據科學在實踐發展中提出的新需求,誰能在這些方面得到好的成績,誰便會在這個大數據時代取得領先的位置。 從2012年開始,幾乎人人(至少是互聯網界)言必稱大數據,似乎不和大數據沾點邊都不好意思和別人聊天。從2016年開始,大數據系統逐步開始在企業中進入部署階段,大數據的炒作逐漸散去,隨之而來的是應用的蓬勃發展期,一些代表...
摘要:前言近期在做的攔截器功能,正好用到了責任鏈模式。通過官方圖就可以非常清楚的看出是一個責任鏈模式用責任鏈模式設計一個攔截器對于攔截器來說使用責任鏈模式再好不過了。設置攔截器到責任鏈中時通過反射將的值保存到各個攔截器中。 showImg(https://segmentfault.com/img/remote/1460000016756077?w=1733&h=1300); 前言 近期在做 ...
摘要:在組件樹中前面啟動的過程中提到過的的線程負責接收連接,接收請求之后調用方法,把包裝成,創建一個任務,從線程池中獲取一個線程處理該任務。 概念 Java Web,是基于Java語言實現web服務的技術總和。介于現在Java在web客戶端應用的比較少,我把學習重點放在了JavaWeb服務端應用。雖然用Springboot就可以很快地搭建一個web項目了,但是如果想要深入了解JavaWeb的...
閱讀 2584·2023-04-25 20:50
閱讀 3929·2023-04-25 18:45
閱讀 2213·2021-11-17 17:00
閱讀 3323·2021-10-08 10:05
閱讀 3073·2019-08-30 15:55
閱讀 3487·2019-08-30 15:44
閱讀 2355·2019-08-29 13:51
閱讀 1111·2019-08-29 12:47