設計模式學習12----責任鏈模式

責任鏈模式定義

責任鏈模式(Chain of Responsibilty Pattern)避免請求發送者與接收者耦合在一起,讓多個對象處理器都有可能接收請求,將這些對象處理器連接成一條鏈,並且沿着這條鏈傳遞請求,直到有對象處理它爲止。責任鏈模式是屬於行爲型模式。責任鏈模式的核心就是設計好一個請求鏈以及鏈結束的標識。
下面先看一個責任鏈的demo。

責任鏈模式的demo

日誌框架中,日誌按照級別分爲,控制檯日誌(DEBUG), 文件日誌(INFO)以及錯誤日誌 (ERROR)。每個級別的日誌需要在其本級和下級打印,例如:ERROR級別的日誌可以在控制檯和日誌文件中輸出。這個需求我們就可以選用責任鏈模式來實現。

類圖

首先我們畫好一個類圖
demo類圖
如上圖,日誌抽象類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.系統性能將受到一定影響,而且在進行代碼調試時不太方便,可能會造成循環調用

應用場景

  1. 有多個對象可以處理同一個請求,具體哪個對象處理該請求由運行時刻自動確定。
    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的時候(類似於擊鼓傳話中的鼓聲停止),即所有的攔截器都執行完畢。

總結

應用責任鏈模式主要有如下幾個步驟

  1. 設計一條鏈條和抽象處理方法 如Servlet的ApplicationFilterChain和Filter
  2. 將具體處理器初始化到鏈條中,並做抽象方法的具體實現,如自定義過濾器XssFilter
  3. 具體處理器之間的引用和處理條件的判斷
  4. 設計鏈條結束標識,如通過pos遊標。
    實際的應用中我們可以將責任鏈模式應用到流程審批(例如:請假的審批)的過程中,因爲每個審批人都可以看成一個具體的處理器,上一個審批人審批完成之後需要將請求轉給下一個審批人,直到審批完成。
    同時需要注意的時候,如果請求鏈過長, 則可能會非常的影響系統性能,也要防止循環調用的情況。

參考

什麼是責任鏈設計模式?
責任鏈模式

發佈了144 篇原創文章 · 獲贊 24 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章