徹底搞懂管道-過(guò)濾器模式
在面向請(qǐng)求/響應(yīng)式的系統(tǒng)中,我們經(jīng)常會(huì)在多個(gè)請(qǐng)求之間實(shí)現(xiàn)一些集中而通用處理的需求,比如要對(duì)每個(gè)請(qǐng)求檢查數(shù)據(jù)編碼方式、記錄日志信息、壓縮輸出等。要滿(mǎn)足這些需求,就要在請(qǐng)求響應(yīng)代碼的主流程中嵌入一些定制化組件。
所以,從架構(gòu)設(shè)計(jì)上講,為了容易添加或刪除定制化組件,而不干擾主流程,我們需要在定制化組件與請(qǐng)求處理主流程之間實(shí)現(xiàn)松耦合;
同時(shí),從提升它們的可重用性上講,我們也需要確保每個(gè)組件都能自主存在,而且組件之間能相互獨(dú)立。如下圖所示:
圖片
如何實(shí)現(xiàn)這樣的效果呢?幸好,在架構(gòu)設(shè)計(jì)領(lǐng)域中存在一種類(lèi)似的模式,就是管道 - 過(guò)濾器模式。
接下來(lái)我具體講一講什么是管道 - 過(guò)濾器模式,以及如何實(shí)現(xiàn)。
什么是管道 - 過(guò)濾器模式?
管道 - 過(guò)濾器在結(jié)構(gòu)上是一種組合行為,通常以切面(Aspect)的方式在主流程上添加定制化組件。當(dāng)我們?cè)陂喿x一些開(kāi)源框架和代碼,看到 Filter(過(guò)濾器)或 Interceptor(攔截器)等名詞時(shí),往往就是碰到了管道 - 過(guò)濾器模式。
管道 - 過(guò)濾器結(jié)構(gòu)主要包括過(guò)濾器(Filter)和管道(Pipe)兩種組件:
圖片
在管道 - 過(guò)濾器結(jié)構(gòu)中,過(guò)濾器負(fù)責(zé)執(zhí)行具體的業(yè)務(wù)邏輯,每個(gè)過(guò)濾器都會(huì)接收來(lái)自主流程的請(qǐng)求,并返回一個(gè)響應(yīng)結(jié)果到主流程中。而管道則用來(lái)獲取來(lái)自過(guò)濾器的請(qǐng)求和響應(yīng),并傳遞到后續(xù)的過(guò)濾器中,相當(dāng)于是一種通道。
管道 - 過(guò)濾器風(fēng)格的一個(gè)典型應(yīng)用是 Web 容器的 Filter 機(jī)制。你可以看到,在生成最終的 HTTP 響應(yīng)之前,Web 容器通過(guò)添加多個(gè) Filter 對(duì) HTTP 響應(yīng)結(jié)果進(jìn)行處理:
圖片
管道 - 過(guò)濾器模式示例
在介紹完管道 - 過(guò)濾器模式的基本概念之后,我們來(lái)看一個(gè)它的簡(jiǎn)單示例,幫你更深入地認(rèn)識(shí)這一模式。
設(shè)想我們存在一個(gè) Order 對(duì)象,代表現(xiàn)實(shí)世界中的訂單概念,包含一些常規(guī)屬性比如訂單編號(hào)(orderNumber)、聯(lián)系方式(contactInfo)、收貨地址(address)和貨物信息(goods):
public class Order {
private String orderNumber;
private String contactInfo;
private String address;
private String goods;
}
基于這個(gè) Order 對(duì)象,我們構(gòu)建一個(gè) Filter 鏈來(lái)分別完成 Order 中核心屬性的校驗(yàn):
- GoodsNotEmptyFilter,驗(yàn)證 Order 對(duì)象中“goods”字段是否為空;
- OrderNumberNotInvalidFilter,驗(yàn)證 Order 對(duì)象中“orderNumber”字段是否符合特定的命名規(guī)則;
- AddressNotInvalidFilter,驗(yàn)證 Order 對(duì)象中“address”字段是否是一個(gè)合法的收貨地址。
這些 Filter 通過(guò)對(duì)應(yīng)的 Pipe 組件構(gòu)成一個(gè)完成的處理鏈路:
圖片
從這個(gè)例子中,你可以看到管道 - 過(guò)濾器模式的特點(diǎn)在于:把一系列的定制化需求轉(zhuǎn)換成一種類(lèi)似數(shù)據(jù)流的處理方式,數(shù)據(jù)通過(guò)管道流經(jīng)一系列的過(guò)濾器,在每個(gè)過(guò)濾器中完成特定的業(yè)務(wù)邏輯。
顯然,每個(gè)過(guò)濾器能夠獨(dú)立完成自身的職責(zé),不需要依賴(lài)于其他過(guò)濾器,過(guò)濾器之間沒(méi)有耦合度。
這種特性使得系統(tǒng)的擴(kuò)展性得到了巨大的提升,我們很容易就能對(duì)現(xiàn)有的過(guò)濾器進(jìn)行替換,而且對(duì)過(guò)濾器進(jìn)行動(dòng)態(tài)添加和刪除也不會(huì)對(duì)整個(gè)處理流程產(chǎn)生任何影響。
管道 - 過(guò)濾器模式在開(kāi)源框架中的應(yīng)用
現(xiàn)在,相信你對(duì)管道 - 過(guò)濾器的基本結(jié)構(gòu)和實(shí)現(xiàn)方式已經(jīng)有了基本了解。接下來(lái),我來(lái)講解開(kāi)源框架中的管道 - 過(guò)濾器設(shè)計(jì)方法和實(shí)現(xiàn)細(xì)節(jié),進(jìn)一步加深你的理解。
事實(shí)上,很多開(kāi)源框架中都應(yīng)用了管道 - 過(guò)濾器這個(gè)架構(gòu)模式,也都提供了基于過(guò)濾器鏈的實(shí)現(xiàn)方式,例如 Dubbo 中的過(guò)濾器概念就基本符合我們對(duì)這一模式的理解。
接下來(lái)我們以 Dubbo 為例,先看一下過(guò)濾器鏈的構(gòu)建過(guò)程,再介紹 Dubbo 中現(xiàn)有過(guò)濾器的實(shí)現(xiàn)方法。
圖片
Dubbo 中對(duì) Filter 的處理基于一種 Wrapper 機(jī)制。所謂 Wrapper,顧名思義,是對(duì) Dubbo 中各種擴(kuò)展點(diǎn)的一種包裝。(如上圖)
目前,縱觀整個(gè) Dubbo 框架,只存在一個(gè) Wrapper,即 ProtocolFilterWrapper。
在該類(lèi)中,存在這樣一個(gè)用來(lái)構(gòu)建調(diào)用鏈的 buildInvokerChain 方法:
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
//獲取Invoker對(duì)象
Invoker<T> last = invoker;
//加載過(guò)濾器列表
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (filters.size() > 0) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
last = new Invoker<T>() {
//講Filter作用于Invoker對(duì)象
};
}
}
return last;
}
我們可以看到用于獲取擴(kuò)展點(diǎn)的 ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension() 方法。注意,這里對(duì)通過(guò)擴(kuò)展點(diǎn)加載的過(guò)濾器進(jìn)行了排序,從而確保過(guò)濾器鏈按設(shè)想的順序進(jìn)行執(zhí)行。
看完過(guò)濾器鏈,我們來(lái)看一下過(guò)濾器。Dubbo 中的 Filter 接口定義是這樣的:
@SPI
public interface Filter {
Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
}
可以看到 Filter 接口能夠獲取傳入的 Invoker,從而對(duì)其進(jìn)行攔截和處理。針對(duì) Filter 接口,Dubbo 中一共存在一大批個(gè)實(shí)現(xiàn)類(lèi),類(lèi)層結(jié)構(gòu)如下圖所示:
圖片
這些過(guò)濾器可以大致分成兩類(lèi):
- 面向服務(wù)提供者的過(guò)濾器
- 面向服務(wù)消費(fèi)者的過(guò)濾器
其中面向服務(wù)提供者的過(guò)濾器只會(huì)在服務(wù)暴露時(shí)對(duì) Invoker 進(jìn)行過(guò)濾,例如上圖中的 TimeoutFilter 和 TraceFilter。
而面向服務(wù)消費(fèi)者的過(guò)濾器發(fā)生作用的階段是在服務(wù)引用時(shí),例如上圖中的 ConsumerContextFilter 和 FutureFilter。
一般的過(guò)濾器只能屬于這兩種類(lèi)型中的一種,但是 MonitorFilter 是個(gè)例外,它可以同時(shí)作用于服務(wù)暴露和服務(wù)引用階段,因?yàn)樗枰獙?duì)這兩個(gè)階段都進(jìn)行監(jiān)控。
我挑選一個(gè)有代表性的 TokenFilter 給你介紹一下。
TokenFilter 的作用很明確,就是通過(guò) Token 進(jìn)行訪(fǎng)問(wèn)鑒權(quán),通過(guò)對(duì)比 Invoker 中的 Token 和傳入?yún)?shù)中的 Token 來(lái)判斷是否是合法的請(qǐng)求,其代碼如下所示:
@Activate(group = Constants.PROVIDER, value = Constants.TOKEN_KEY)
public class TokenFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation inv)
throws RpcException {
//獲取Token
String token = invoker.getUrl().getParameter(Constants.TOKEN_KEY);
if (ConfigUtils.isNotEmpty(token)) {
Class<?> serviceType = invoker.getInterface();
//獲取請(qǐng)求的輔助信息
Map<String, String> attachments = inv.getAttachments();
//獲取遠(yuǎn)程Token
String remoteToken = attachments == null ? null : attachments.get(Constants.TOKEN_KEY);
//判斷本地Token和遠(yuǎn)程Token是否一致
if (!token.equals(remoteToken)) {
throw new RpcException("Invalid token! Forbid invoke remote service " + serviceType + " method " + inv.getMethodName() + "() from consumer " + RpcContext.getContext().getRemoteHost() + " to provider " + RpcContext.getContext().getLocalHost());
}
}
return invoker.invoke(inv);
}
}
在代碼中可以看到,通過(guò) invoker.getUrl() 方法獲取了 Invoker 中的 URL 對(duì)象,而我們知道 Dubbo 中的 URL 作為統(tǒng)一數(shù)據(jù)模型,它包含了所有服務(wù)調(diào)用過(guò)程中的參數(shù),這里的 Invocation 對(duì)象則封裝了請(qǐng)求數(shù)據(jù)。
這樣,一方面我們通過(guò) URL 對(duì)象獲取本地 Token 參數(shù),另一方面通過(guò) Invocation 的 Attachments 也獲取了 RemoteToken,可以執(zhí)行對(duì)比和校驗(yàn)操作。這也是在 Dubbo 中處理調(diào)用信息傳遞的很常見(jiàn)的一種做法,你可以在很多地方看到類(lèi)似的代碼。
總結(jié)
可以說(shuō),如何動(dòng)態(tài)把握請(qǐng)求的處理流程是任何系統(tǒng)開(kāi)發(fā)面臨的一大問(wèn)題,而今天講解的管道 - 過(guò)濾器模式就為解決這一問(wèn)題提供了有效的方案。
在日常開(kāi)發(fā)過(guò)程中,我們可以在確保主流程不受影響的基礎(chǔ)上,通過(guò)管道 - 過(guò)濾器模式添加各種定制化的附加流程,滿(mǎn)足不同的應(yīng)用場(chǎng)景。
從架構(gòu)設(shè)計(jì)上,管道 - 過(guò)濾器可以說(shuō)是高內(nèi)聚、低耦合思想的典型實(shí)現(xiàn)方案,也符合開(kāi)放 - 關(guān)閉原則。各個(gè)過(guò)濾器各司其職,相互獨(dú)立,多個(gè)過(guò)濾器之間集成也比較簡(jiǎn)單,在功能重用性和維護(hù)性上都具備優(yōu)勢(shì)。
另一方面,我們也應(yīng)該認(rèn)識(shí)到管道 - 過(guò)濾器的最大問(wèn)題在于,導(dǎo)致系統(tǒng)形成一種成批操作行為,因此在使用過(guò)程中需要設(shè)計(jì)并協(xié)調(diào)數(shù)據(jù)的流向。