責任鏈模式定義
責任鏈模式(Chain of Responsibilty Pattern)避免請求發送者與接收者耦合在一起,讓多個對象處理器都有可能接收請求,將這些對象處理器連接成一條鏈,並且沿着這條鏈傳遞請求,直到有對象處理它爲止。責任鏈模式是屬於行爲型模式。責任鏈模式的核心就是設計好一個請求鏈以及鏈結束的標識。
下面先看一個責任鏈的demo。
責任鏈模式的demo
日誌框架中,日誌按照級別分爲,控制檯日誌(DEBUG), 文件日誌(INFO)以及錯誤日誌 (ERROR)。每個級別的日誌需要在其本級和下級打印,例如:ERROR級別的日誌可以在控制檯和日誌文件中輸出。這個需求我們就可以選用責任鏈模式來實現。
類圖
首先我們畫好一個類圖
如上圖,日誌抽象類AbstractLogger主要是定義一些公共的方法邏輯,定義了每個子類需要實現的抽象方法,以及每個子類的下一個處理器。ConsoleLogger,FileLogger和ErrorLogger則是三個具體的處理器。其級別分別對應DEBUG,INFO和ERROR級別,每個具體處理器都實現了write
方法。
接着我們來看看上面類圖的具體代碼實現。首先來看看AbstractLogger。
代碼實現
public abstract class AbstractLogger {
protected static int DEBUG = 1;
protected static int INFO = 2;
protected static int ERROR = 3;
protected int level;
protected AbstractLogger nextLogger;
//設置下一個處理器的方法
public void setNextLogger(AbstractLogger nextLogger) {
this.nextLogger = nextLogger;
}
//公共的輸出日誌的方法
public void logMessage(int level, String message) {
if (this.level <= level) {
write(message);
}
if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
//抽象方法,每個具體的處理器都要重寫
protected abstract void write(String message);
}
然後就是三個具體的處理器,篇幅限制,在此只列舉一個ConsoleLogger類,其他的兩個處理器類也類似。
public class ConsoleLogger extends AbstractLogger {
public ConsoleLogger() {
//設置本處理器處理的日誌級別
this.level = DEBUG;
//設置下一個處理器
setNextLogger(new FileLogger());
}
@Override
protected void write(String message) {
System.out.println("**********Console logger:" + message);
}
}
最後看看測試類ChainPatternDemo
public class ChainPatternDemo {
public static void main(String[] args) {
AbstractLogger consoleLogger = new ConsoleLogger();
consoleLogger.logMessage(3, "錯誤信息");
System.out.println();
consoleLogger.logMessage(2, "文件信息");
System.out.println();
consoleLogger.logMessage(1, "控制檯信息");
}
}
運行結果如下:
通過lambda來實現
上面是通過常規的方式來實現的責任鏈模式,那麼我們能不能通過lambda來實現類,通過lambda來實現的話,我們只需要定義一個函數接口,然後通過lambda來構造具體的實現類。代碼如下:
@FunctionalInterface
public interface LambdaLogger {
int DEBUG = 1;
int INFO = 2;
int ERROR = 3;
/**
* 函數式接口中唯一的抽象方法
*
* @param message
*/
void message(String message,int level);
default LambdaLogger appendNext(LambdaLogger nextLogger) {
return (msg,level)->{
//前面logger處理完才用當前logger處理
message(msg, level);
nextLogger.message(msg, level);
};
}
static LambdaLogger logMessage(int level, Consumer<String> writeMessage){
//temLevel是日誌處理器的級別,level是要打印的日誌級別
return (message, temLevel) -> {
if (temLevel >= level) {
writeMessage.accept(message);
}
};
}
//調試日誌
static LambdaLogger consoleLogMessage(int level1) {
return logMessage(level1, (message) -> System.out.println("**********Console logger:" + message));
}
//ERROR日誌
static LambdaLogger errorLogMessage(int level) {
return logMessage(level, message -> System.out.println("***********Error logger: " + message));
}
//INFO日誌
static LambdaLogger fileLogger(int level) {
return logMessage(level, message -> System.out.println("*******File Logger: " + message));
}
static void main(String[] args) {
LambdaLogger logger = consoleLogMessage(1).appendNext(fileLogger(2)).appendNext(errorLogMessage(3));
//consoleLogger
logger.message("控制檯信息", 1);
logger.message("文件信息", 2);
logger.message("錯誤信息", 3);
}
}
說完了責任鏈demo,接下來我們來總結下責任鏈模式的優缺點。
優缺點
優點
1.降低耦合度,它將請求的發送者和接收者解耦,請求只管發送,不管誰來處理。
2. 簡化了對象,使得對象不需要知道鏈的結構
3. 增強給對象指派的靈活性,通過改變鏈內的成員或者調動他們的次序,允許動態地新增或刪除責任
4. 增加新的請求處理類很方便
缺點
1.不能保證請求一定被接收
2.系統性能將受到一定影響,而且在進行代碼調試時不太方便,可能會造成循環調用
應用場景
- 有多個對象可以處理同一個請求,具體哪個對象處理該請求由運行時刻自動確定。
2.在不明確指定接收者的情況下,向多個對象中的一個提交一個請求
3.可動態指定一組對象處理請求
小結
前面介紹了責任鏈模式的定義,優缺點,應用場景,並且實現了一個責任鏈的demo。那麼在實際的開發中,我們有沒有碰到責任鏈模式呢?答案是有的,請看下面。
Servlet中運用Filter
不管是用SpringMVC還是用SpringBoot,我們都繞不開過濾器Filter,同時,我們也會自定義一些過濾器,例如:權限過濾器,字符過濾器。不管是何種過濾器,我們都需要實現過濾器接口Filter,並且重寫doFilter方法
public interface Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
}
如下:我們自定義了一個字符過濾器XssFilter,用來過濾字符,防止XSS注入。我們也是重寫了doFilter
方法,對黑名單中的字符進行過濾,對不在黑名單中的請求直接轉給下一個過濾器。
@Component
@WebFilter(urlPatterns = "/*",filterName = "XssFilter")
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//在黑名單中
if(allUrl(url)){
//走xxs過濾
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
(HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}else{
//走原先方法
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
通過debug模式調試我們看到如下調用棧。如下圖所示:
紅框中標記的內容是Tomcat容器設置的責任鏈,從Engine到Context再到Wrapper都是通過責任鏈的方式來調用的。它們都是繼承了ValueBase抽象類,實現了Value接口。
不過此處我們要重點關注下與我們過濾器XssFilter息息相關的ApplicationFilterChain類,這是一個過濾器的調用鏈的類。在該類中定義了一個ApplicationFilterConfig數組來保存所有需要調用的過濾器Filters。
public final class ApplicationFilterChain implements FilterChain {
/**
* Filters.
*/
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
}
定義了一個初始大小爲0的ApplicationFilterConfig數組,那麼ApplicationFilterConfig是個啥呢?
/**
* 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
*/
public final class ApplicationFilterConfig implements FilterConfig, Serializable {
/**
* The application Filter we are configured for.
*/
private transient Filter filter = null;
/**
* The <code>FilterDef</code> that defines our associated Filter.
*/
private final FilterDef filterDef;
}
ApplicationFilterConfig類是在web應用第一次啓動的時候管理Filter的實例化的,其內部定義了一個Filter類型的全局變量。那麼ApplicationFilterConfig數組的大小又是在啥時候被重新設置的呢?答案是在ApplicationFilterChain調用addFilter
方法的時候,那麼我們來看看addFilter
方法
/**
* The int which gives the current number of filters in the chain.
* 當前鏈中過濾器的數量,默認是0
*/
private int n = 0;
/**
* 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
是用來記錄當前鏈中過濾器的數量,默認爲0,ApplicationFilterConfig對象數組的長度也等於0,所以應用第一次啓動時if (n == filters.length)
條件滿足,添加完一個過濾器之後將n加1。那麼addFilter
方法是在什麼時候調用的呢?答案就是在ApplicationFilterFactory類的createFilterChain
方法中。
public final class ApplicationFilterFactory {
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
....... 省略部分代碼
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
...... 省略部分代碼
}
}
從名稱來看ApplicationFilterFactory是一個工廠類,那麼ApplicationFilterFactory類的createFilterChain
方法又是何時候調用的呢?這就需要回到最頂端的StandardWrapperValve類的invoke
方法。
final class StandardWrapperValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
......
filterChain.doFilter(request.getRequest(), response.getResponse());
}
}
最後我們就來看看 ApplicationFilterChain的doFilter
方法。其內部的核心邏輯在internalDoFilter
方法中。
/**
* The int which is used to maintain the current position
* in the filter chain.
*/
private int pos = 0;
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
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);
}
}
servlet.service(request, response);
}
pos 變量用來標記filterChain執行的當期位置,然後調用filter.doFilter(request, response, this)
傳遞this(ApplicationFilterChain)進行鏈路傳遞,直至pos>n
的時候(類似於擊鼓傳話中的鼓聲停止),即所有的攔截器都執行完畢。
總結
應用責任鏈模式主要有如下幾個步驟
- 設計一條鏈條和抽象處理方法 如Servlet的ApplicationFilterChain和Filter
- 將具體處理器初始化到鏈條中,並做抽象方法的具體實現,如自定義過濾器XssFilter
- 具體處理器之間的引用和處理條件的判斷
- 設計鏈條結束標識,如通過pos遊標。
實際的應用中我們可以將責任鏈模式應用到流程審批(例如:請假的審批)的過程中,因爲每個審批人都可以看成一個具體的處理器,上一個審批人審批完成之後需要將請求轉給下一個審批人,直到審批完成。
同時需要注意的時候,如果請求鏈過長, 則可能會非常的影響系統性能,也要防止循環調用的情況。