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

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

love-1089665_640.jpg

被你忽略的就是責任鏈設計模式,但願它再次通過你身旁你會猛的發現,並對它微微一笑......程序員

責任鏈設計模式介紹

抽象介紹

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

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

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

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 和實際案例vim

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 ,咱們能夠經過接口與實現類的方式實現app

案例介紹

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

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](https://upload-images.jianshu.io/upload_images/17985603-581538aaa16cee89.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

VNote

留言中有朋友讓我推薦一款 MarkDown 編輯器,我用過不少種(包括在線的),此次推薦 VNote, VNote 是一個受Vim啓發的更懂程序員和Markdown的一個筆記軟件, 都說 vim是最好的編輯器,更懂程序猿,可是多數仍是應用在類 Unix 環境的 shell 腳本編寫中,熟練使用 vim 也是咱們必備的基本功,VNote 知足這一切需求,同時提供很是多方便的快捷鍵知足平常 MarkDown 的編寫. 經過寫文字順路學習 vim,快哉...

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


a (1).png
相關文章
相關標籤/搜索