前言
近期在做 Cicada 的攔截器功能,正好用到了責任鏈模式。
這個設計模式在日常使用中頻率還是挺高的,藉此機會來分析分析。
責任鏈模式
先來看看什麼是責任鏈模式。
引用一段維基百科對其的解釋:
責任鏈模式在物件導向程式設計裡是一種軟體設計模式,它包含了一些命令物件和一系列的處理物件。每一個處理物件決定它能處理哪些命令物件,它也知道如何將它不能處理的命令物件傳遞給該鏈中的下一個處理物件。該模式還描述了往該處理鏈的末尾新增新的處理物件的方法。
光看這段描述可能大家會覺得懵,簡單來說就是該設計模式用於對某個物件或者請求進行一系列的處理,這些處理邏輯正好組成一個鏈條。
下面來簡單演示使用與不使用責任鏈模式有什麼區別和優勢。
責任鏈模式的應用
傳統實現
假設這樣的場景:傳入了一段內容,需要對這段文字進行加工;比如過濾敏感詞、錯別字修改、最後署上版權等操作。
常見的寫法如下:
public class Main {
public static void main(String[] args) {
String msg = "內容內容內容" ;
String result = Process.sensitiveWord()
.typo()
.copyright();
}
}
複製程式碼
這樣看似沒啥問題也能解決需求,但如果我還需要為為內容加上一個統一的標題呢?在現有的方式下就不得不新增處理方法,並且是在這個客戶端(Process
)的基礎上進行新增。
顯然這樣的擴充套件性不好。
責任鏈模式實現
這時候就到了責任鏈模式發揮作用了。
該需求非常的符合對某一個物件、請求進行一系列處理的特徵。
於是我們將程式碼修改:
這時 Process
就是一個介面了,用於定義真正的處理函式。
public interface Process {
/**
* 執行處理
* @param msg
*/
void doProcess(String msg) ;
}
複製程式碼
同時之前對內容的各種處理只需要實現該介面即可:
public class SensitiveWordProcess implements Process {
@Override
public void doProcess(String msg) {
System.out.println(msg + "敏感詞處理");
}
}
public class CopyrightProcess implements Process {
@Override
public void doProcess(String msg) {
System.out.println(msg + "版權處理");
}
}
public class CopyrightProcess implements Process {
@Override
public void doProcess(String msg) {
System.out.println(msg + "版權處理");
}
}
複製程式碼
然後只需要給客戶端提供一個執行入口以及新增責任鏈的入口即可:
public class MsgProcessChain {
private List<Process> chains = new ArrayList<>() ;
/**
* 新增責任鏈
* @param process
* @return
*/
public MsgProcessChain addChain(Process process){
chains.add(process) ;
return this ;
}
/**
* 執行處理
* @param msg
*/
public void process(String msg){
for (Process chain : chains) {
chain.doProcess(msg);
}
}
}
複製程式碼
這樣使用起來就非常簡單:
public class Main {
public static void main(String[] args) {
String msg = "內容內容內容==" ;
MsgProcessChain chain = new MsgProcessChain()
.addChain(new SensitiveWordProcess())
.addChain(new TypoProcess())
.addChain(new CopyrightProcess()) ;
chain.process(msg) ;
}
}
複製程式碼
當我需要再增加一個處理邏輯時只需要新增一個處理單元即可(addChain(Process process)
),並對客戶端 chain.process(msg)
是無感知的,不需要做任何的改動。
可能大家沒有直接寫過責任鏈模式的相關程式碼,但不經意間使用到的卻不少。
比如 Netty
中的 pipeline
就是一個典型的責任鏈模式,它可以讓一個請求在整個管道中進行流轉。

通過官方圖就可以非常清楚的看出是一個責任鏈模式:

用責任鏈模式設計一個攔截器
對於攔截器來說使用責任鏈模式再好不過了。
下面來看看在 Cicada
中的實現:
首先是定義了和上文 Process
介面類似的 CicadaInterceptor
抽象類:
public abstract class CicadaInterceptor {
public boolean before(CicadaContext context,Param param) throws Exception{
return true;
}
public void after(CicadaContext context,Param param) throws Exception{}
}
複製程式碼
同時定義了一個 InterceptProcess
的客戶端:

其中的 loadInterceptors()
會將所有的攔截器加入到責任鏈中。
再提供了兩個函式分別對應了攔截前和攔截後的入口:

實際應用
現在來看看具體是怎麼使用的吧。

在請求的 handle
中首先進行載入(loadInterceptors(AppConfig appConfig)
),也就是初始化責任鏈。
接下來則是客戶端的入口;呼叫攔截前後的入口方法即可。
由於是攔截器,那麼在
before
函式中是可以對請求進行攔截的。只要返回false
就不會繼續向後處理。所以這裡做了一個返回值的判斷。
同時對於使用者來說只需要建立攔截器類繼承 CicadaInterceptor
類即可。
這裡做了一個演示,分別有兩個攔截器:
- 記錄一個業務
handle
的執行時間。 - 在
after
裡列印了請求引數。 - 同時可在第一個攔截器中返回
false
讓請求被攔截。
先來做前兩個試驗:


這樣當我請求其中一個介面時會將剛才的日誌列印出來:

接下來我讓列印執行時間的攔截器中攔截請求,同時輸入向前端輸入一段文字:

請求介面可以看到如下內容:


同時後面的請求引數也沒有列印出來,說明請求確實被攔截下來。
同時我也可以調整攔截順序,只需要在@Interceptor(order = 1)
註解中定義這個 order
屬性即可(預設值是 0,越小越先執行)。
之前是列印請求引數的攔截器先執行,這次我手動將它的 order 調整為 2,而列印時間的 order 為 1 。

再次請求介面觀察後臺日誌:

發現列印執行時間的攔截器先執行。
那這個執行執行順序如何實現自定義配置的呢?
其實也比較簡單,有以下幾步:
- 在載入攔截器時將註解裡的
order
儲存起來。 - 設定攔截器到責任鏈中時通過反射將
order
的值儲存到各個攔截器中。 - 最終通過排序重新排列這個責任鏈的順序。
貼一些核心程式碼。
掃描攔截器時儲存 order
值:

儲存 order
值到攔截器中:

重新對責任鏈排序:

總結
整個責任鏈模式已經講完,希望對這個設計模式還不瞭解的朋友帶來些幫助。
上文中的原始碼如下:
歡迎關注公眾號一起交流:
