【Tomcat】pipeline valve 設計模式
這裡側重設計模式的角度,具體結合tomcat細節的執行機制,準備在另一篇寫。
只有先把這個模式抽出來理解清楚,再看tomcat的實現會更簡單。
pipeline模式並不難理解,類似servlet規範中的filter。指的是一個pipeline,需要多個閥門序列處理,流水線作業。
那麼很簡單,我們可以寫出如下的程式碼:
import java.util.ArrayList; import java.util.List; public class Pipeline { List<Valve> list = new ArrayList<Valve>(); public Pipeline(){ list.add(new Valve1()); list.add(new Valve2()); list.add(new Valve3()); list.add(new Valve4()); } public void invoke(){ for(Valve valve : list){ valve.invoke(); } } public static void main(String args[]){ new Pipeline().invoke(); } }
public interface Valve {
void invoke();
}
public class Valve1 implements Valve {
public void invoke() {
System.out.println("valve1 invoke");
}
}
其餘valve類似。
上面的程式碼實現很簡單,也可以完成pipeline的模型,既然如此,為什麼還需要搞一個設計模式出來?
必然是上面的程式碼存在一定的問題:
沒有抽象。分兩層,一是對外提供服務,呼叫pipeline的使用方直接依賴了pipeline的具體實現,我們說軟體設計一個重要的原則是“依賴倒置”,如果我們的功能是對外提供的,那麼最好是以介面這種穩定的方式提供,這不是僅僅在pipeline模式中適用的,而是所有軟體設計都應該重視的。依賴介面的好處是我們可以隨意變更介面的實現,而依賴方不需要很大的修改。舉個例子,上面的pipeline是基於list實現的,現在我想要新增一個array實現的,而且呼叫方在某種情況下需要呼叫array實現的,那麼我們必須新建一個ArrayPipeline的類,然後在需要呼叫array的地方把建立pipeline例項的方式改了,呼叫方式也有可能改。如果我們使用了介面,那麼任何呼叫方的程式碼不需要需改,只需要修改介面的賦值語句。如果使用了spring,那麼只需要修改一個bean的定義檔案即可。二是對內實現上,上面的實現pipeline和valve是未解耦的,問題和對外的一樣,需要使用介面解耦,其次pipeline的構建是硬編碼的,事實上,我麼很有可能需要動態新增valve,因此pipeline需要有addValve之類的介面。
綜上,上面的程式碼最大問題是沒有解耦,在之後很難做擴充套件。因此需要抽象出每一種角色的職責。這其實也是所有設計模式出現的初衷,設計模式很大程度上就是為了抽象,讓實現以介面的方式進行。
重新設計:
public interface Pipeline {
void setBasic(Valve valve);
void add(Valve valve);
void remove(Valve valve);
void invoke();
}
import java.util.ArrayList; import java.util.List; public class StandardPipeline implements Pipeline { List<Valve> valves = new ArrayList<Valve>(); Valve basic; public void setBasic(Valve valve) { basic = valve; } public void add(Valve valve) { valves.add(valve); } public void remove(Valve valve) { valves.remove(valve); } public void invoke() { for(Valve valve : valves){ valve.invoke(); } } }
抽象出了valve和pipeline的職責,就形成了上面的程式碼。
tomcat的pipeline規定必須有有個basic的valve,該valve是該流水線的最終處理器,完成這條流水線的主要職責,basic前可以沒有valve,但是basic是必須存在的。就好比可以沒有前面的filter但是必須有最後的serlvet來處理一個http請求。
這裡的pipeline的實現是一種for迴圈方式,如果每一個valve可以決定是否打斷當前的執行鏈,這種for迴圈就無法處理了。當然,我們可以讓每一個valve返回一個boolean值,但是這種方式代價高昂,前提是valve本省沒有返回值。如果是類似linux管道的模式,每一個valve要處理上一個valve的返回值,那麼這種方式就不可行。而且,從邏輯上講,是否往下執行,是在一個valve內部需要解決的事情,pipeline不應該干涉。
所以上面for迴圈的方式假定了每一個valve都必須執行一次,沒有打斷執行鏈的能力。
為了更優雅的解決這個問題,tomcat的pipeline模式加入了一個新的角色context。那麼最後的設計變成了這樣:
public interface PipelineContext {
void invokeNext();
}
public interface Valve {
void invoke(PipelineContext context);
}
public class Valve1 implements Valve {
public void invoke(PipelineContext context) {
System.out.println("valve1 invoke");
context.invokeNext();
}
}
import java.util.ArrayList;
import java.util.List;
public class StandardPipeline implements Pipeline {
List<Valve> valves;
Valve basic;
PipelineContext context;
public StandardPipeline(){
valves = new ArrayList<Valve>();
context = new StandardPipelineContext();
}
public void setBasic(Valve valve) {
basic = valve;
}
public void add(Valve valve) {
valves.add(valve);
}
public void remove(Valve valve) {
valves.remove(valve);
}
public void invoke() {
context.invokeNext();
}
private class StandardPipelineContext implements PipelineContext{
int step = 0;
public void invokeNext() {
if(!valves.isEmpty() && step < valves.size())
valves.get(step++).invoke(context);
else
basic.invoke(context);
}
}
public static void main(String args[]){
Pipeline pipeline = new StandardPipeline();
pipeline.setBasic(new BasicValve());
pipeline.add(new Valve1());
pipeline.add(new Valve2());
pipeline.add(new Valve3());
pipeline.add(new Valve4());
pipeline.invoke();
}
}
取消了pipeline裡invoke方法的for迴圈。添加了一個context的角色。之前說每一個valve要有決定是否打斷鏈子的能力,那它必須獲取到一些全域性的資訊才行,這裡的context就是為了讓valve具有這種打斷能力。context提供了valve執行下一個valve的能力,這樣valve在內部就可以通過if來判斷是否需要執行下一個valve。
context本身的實現像是一個遊標,提供了一個介面來滑動遊標。這樣,pipeline裡invoke方法就像是一個迭代器模式,沒有for迴圈。
輸出: