首先我們通過一張簡化的類圖來回顧一下,從圖上你可以看到各種組件的層次關系,圖中的虛線表示一個請求在 Tomcat 中流轉的過程。
上面這張圖描述了組件之間的靜態關系,如果想讓一個系統能夠對外提供服務,我們需要創建、組裝并啟動這些組件;在服務停止的時候,我們還需要釋放資源,銷毀這些組件,因此這是一個動態的過程。也就是說,Tomcat 需要動態地管理這些組件的生命周期。
在我們實際的工作中,如果你需要設計一個比較大的系統或者框架時,你同樣也需要考慮這幾個問題:如何統一管理組件的創建、初始化、啟動、停止和銷毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動和停止不遺漏、不重復?
今天我們就來解決上面的問題,在這之前,先來看看組件之間的關系。如果你仔細分析過這些組件,可以發現它們具有兩層關系。
- 第一層關系是組件有大有小,大組件管理小組件,比如 Server 管理 Service,Service 又管理連接器和容器。
- 第二層關系是組件有外有內,外層組件控制內層組件,比如連接器是外層組件,負責對外交流,外層組件調用內層組件完成業務功能。也就是說,請求的處理過程是由外層組件來驅動的。
這兩層關系決定了系統在創建組件時應該遵循一定的順序。
- 第一個原則是先創建子組件,再創建父組件,子組件需要被“注入”到父組件中。
- 第二個原則是先創建內層組件,再創建外層組件,內層組建需要被“注入”到外層組件。
因此,最直觀的做法就是將圖上所有的組件按照先小后大、先內后外的順序創建出來,然后組裝在一起。不知道你注意到沒有,這個思路其實很有問題!因為這樣不僅會造成代碼邏輯混亂和組件遺漏,而且也不利于后期的功能擴展。
為了解決這個問題,我們希望找到一種通用的、統一的方法來管理組件的生命周期,就像汽車“一鍵啟動”那樣的效果。
一鍵式啟停:LifeCycle接口
我在前面說到過,設計就是要找到系統的變化點和不變點。這里的不變點就是每個組件都要經歷創建、初始化、啟動這幾個過程,這些狀態以及狀態的轉化是不變的。而變化點是每個具體組件的初始化方法,也就是啟動方法是不一樣的。
因此,我們把不變點抽象出來成為一個接口,這個接口跟生命周期有關,叫作 LifeCycle。LifeCycle 接口里應該定義這么幾個方法:init()、start()、stop() 和 destroy(),每個具體的組件去實現這些方法。
理所當然,在父組件的 init() 方法里需要創建子組件并調用子組件的 init() 方法。同樣,在父組件的 start() 方法里也需要調用子組件的 start() 方法,因此調用者可以無差別的調用各組件的 init() 方法和 start() 方法,這就是組合模式的使用,并且只要調用最頂層組件,也就是 Server 組件的 init() 和 start() 方法,整個 Tomcat 就被啟動起來了。下面是LifeCycle 接口的定義。
可擴展性:LifeCycle事件
我們再來考慮另一個問題,那就是系統的可擴展性。因為各個組件 init() 和 start() 方法的具體實現是復雜多變的,比如在 Host 容器的啟動方法里需要掃描 webApps 目錄下的Web 應用,創建相應的 Context 容器,如果將來需要增加新的邏輯,直接修改 start() 方法?這樣會違反開閉原則,那如何解決這個問題呢?開閉原則說的是為了擴展系統的功能,你不能直接修改系統中已有的類,但是你可以定義新的類。
我們注意到,組件的 init() 和 start() 調用是由它的父組件的狀態變化觸發的,上層組件的初始化會觸發子組件的初始化,上層組件的啟動會觸發子組件的啟動,因此我們把組件的生命周期定義成一個個狀態,把狀態的轉變看作是一個事件。而事件是有監聽器的,在監聽器里可以實現一些邏輯,并且監聽器也可以方便的添加和刪除,這就是典型的觀察者模式。
具體來說就是在 LifeCycle 接口里加入兩個方法:添加監聽器和刪除監聽器。除此之外,我們還需要定義一個 Enum 來表示組件有哪些狀態,以及處在什么狀態會觸發什么樣的事件。因此 LifeCycle 接口和 LifeCycleState 就定義成了下面這樣。
從圖上你可以看到,組件的生命周期有 NEW、INITIALIZING、INITIALIZED、STARTING_PREP、STARTING、STARTED 等,而一旦組件到達相應的狀態就觸發相應的事件,比如 NEW 狀態表示組件剛剛被實例化;而當 init() 方法被調用時,狀態就變成INITIALIZING 狀態,這個時候,就會觸發 BEFORE_INIT_EVENT 事件,如果有監聽器在監聽這個事件,它的方法就會被調用。
重用性:LifeCycleBase抽象基類
有了接口,我們就要用類去實現接口。一般來說實現類不止一個,不同的類在實現接口時往往會有一些相同的邏輯,如果讓各個子類都去實現一遍,就會有重復代碼。那子類如何重用這部分邏輯呢?其實就是定義一個基類來實現共同的邏輯,然后讓各個子類去繼承它,就達到了重用的目的。
而基類中往往會定義一些抽象方法,所謂的抽象方法就是說基類不會去實現這些方法,而是調用這些方法來實現骨架邏輯。抽象方法是留給各個子類去實現的,并且子類必須實現,否則無法實例化。
比如寶馬和榮威的底盤和骨架其實是一樣的,只是發動機和內飾等配套是不一樣的。底盤和骨架就是基類,寶馬和榮威就是子類。僅僅有底盤和骨架還不是一輛真正意義上的車,只能算是半成品,因此在底盤和骨架上會留出一些安裝接口,比如安裝發動機的接口、安裝座椅的接口,這些就是抽象方法。寶馬或者榮威上安裝的發動機和座椅是不一樣的,也就是具體子類對抽象方法有不同的實現。
回到 LifeCycle 接口,Tomcat 定義一個基類 LifeCycleBase 來實現 LifeCycle 接口,把一些公共的邏輯放到基類中去,比如生命狀態的轉變與維護、生命事件的觸發以及監聽器的添加和刪除等,而子類就負責實現自己的初始化、啟動和停止等方法。為了避免跟基類中的方法同名,我們把具體子類的實現方法改個名字,在后面加上 Internal,叫 initInternal()、startInternal() 等。我們再來看引入了基類 LifeCycleBase 后的類圖:
從圖上可以看到,LifeCycleBase 實現了 LifeCycle 接口中所有的方法,還定義了相應的抽象方法交給具體子類去實現,這是典型的模板設計模式。
我們還是看一看代碼,可以幫你加深理解,下面是 LifeCycleBase 的 init() 方法實現。
@Override
public final synchronized void init() throws LifecycleException {
//1.狀態檢查
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
//2.觸發INITIALIZING事件的監聽器
setStateInternal(LifecycleState.INITIALIZING, null, false);
//3.調用具體子類的初始化方法
initInternal();
//4.觸發INITIALIZED事件的監聽器
setStateInternal(LifecycleState.INITIALIZED, null, false);
}
catch (Throwable t) {
...
}
}
這個方法邏輯比較清楚,主要完成了四步:
第一步,檢查狀態的合法性,比如當前狀態必須是 NEW 然后才能進行初始化。
第二步,觸發 INITIALIZING 事件的監聽器:
在這個 setStateInternal 方法里,會調用監聽器的業務方法。
第三步,調用具體子類實現的抽象方法 initInternal() 方法。我在前面提到過,為了實現一鍵式啟動,具體組件在實現 initInternal() 方法時,又會調用它的子組件的 init() 方法。
第四步,子組件初始化后,觸發 INITIALIZED 事件的監聽器,相應監聽器的業務方法就會被調用。
setStateInternal(LifecycleState.INITIALIZED, null, false);
總之,LifeCycleBase 調用了抽象方法來實現骨架邏輯。講到這里, 你可能好奇,LifeCycleBase 負責觸發事件,并調用監聽器的方法,那是什么時候、誰把監聽器注冊進來的呢?
分為兩種情況:
- Tomcat 自定義了一些監聽器,這些監聽器是父組件在創建子組件的過程中注冊到子組件的。比如 MemoryLeakTrackingListener 監聽器,用來檢測 Context 容器中的內存泄漏,這個監聽器是 Host 容器在創建 Context 容器時注冊到 Context 中的。
- 我們還可以在 server.xml 中定義自己的監聽器,Tomcat 在啟動時會解析 server.xml,創建監聽器并注冊到容器組件。
生周期管理總體類圖
通過上面的學習,我相信你對 Tomcat 組件的生命周期的管理有了深入的理解,我們再來看一張總體類圖繼續加深印象。
這里請你注意,圖中的 StandardServer、StandardService 等是 Server 和 Service 組件的具體實現類,它們都繼承了 LifeCycleBase。
StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應容器組件的具體實現類,因為它們都是容器,所以繼承了 ContainerBase 抽象基類,而ContainerBase 實現了 Container 接口,也繼承了 LifeCycleBase 類,它們的生命周期管理接口和功能接口是分開的,這也符合設計中接口分離的原則。
小結
Tomcat 為了實現一鍵式啟停以及優雅的生命周期管理,并考慮到了可擴展性和可重用性,將面向對象思想和設計模式發揮到了極致,分別運用了組合模式、觀察者模式、骨架抽象類和模板方法。
如果你需要維護一堆具有父子關系的實體,可以考慮使用組合模式。觀察者模式聽起來“高大上”,其實就是當一個事件發生后,需要執行一連串更新操作。傳統的實現方式是在事件響應代碼里直接加更新邏輯,當更新邏輯加多了之后,代碼會變得臃
腫,并且這種方式是緊耦合的、侵入式的。而觀察者模式實現了低耦合、非侵入式的通知與更新機制。
而模板方法在抽象基類中經常用到,用來實現通用邏輯。