趣操做,Tomcat如何實現一鍵式啓停?

最新互聯網大廠面試真題、Java程序員面試策略(面試前的準備、面試中的技巧)請移步GitHubgit


首先咱們經過一張簡化的類圖來回顧一下,從圖上你能夠看到各類組件的層次關係,圖中的虛線表示一個請求在 Tomcat 中流轉的過程。程序員

趣操做,Tomcat如何實現一鍵式啓停?

上面這張圖描述了組件之間的靜態關係,若是想讓一個系統可以對外提供服務,咱們須要建立、組裝並啓動這些組件;在服務中止的時候,咱們還須要釋放資源,銷燬這些組件,所以這是一個動態的過程。也就是說,Tomcat 須要動態地管理這些組件的生命週期。github

在咱們實際的工做中,若是你須要設計一個比較大的系統或者框架時,你一樣也須要考慮這幾個問題:如何統一管理組件的建立、初始化、啓動、中止和銷燬?如何作到代碼邏輯清晰?如何方便地添加或者刪除組件?如何作到組件啓動和中止不遺漏、不重複?web

今天咱們就來解決上面的問題,在這以前,先來看看組件之間的關係。若是你仔細分析過這些組件,能夠發現它們具備兩層關係。面試

  • 第一層關係是組件有大有小,大組件管理小組件,好比 Server 管理 Service,Service 又管理鏈接器和容器。
  • 第二層關係是組件有外有內,外層組件控制內層組件,好比鏈接器是外層組件,負責對外交流,外層組件調用內層組件完成業務功能。也就是說,請求的處理過程是由外層組件來驅動的。

這兩層關係決定了系統在建立組件時應該遵循必定的順序。設計模式

  • 第一個原則是先建立子組件,再建立父組件,子組件須要被「注入」到父組件中。
  • 第二個原則是先建立內層組件,再建立外層組件,內層組建須要被「注入」到外層組件。

所以,最直觀的作法就是將圖上全部的組件按照先小後大、先內後外的順序建立出來,而後組裝在一塊兒。不知道你注意到沒有,這個思路其實頗有問題!由於這樣不只會形成代碼邏輯混亂和組件遺漏,並且也不利於後期的功能擴展。app

爲了解決這個問題,咱們但願找到一種通用的、統一的方法來管理組件的生命週期,就像汽車「一鍵啓動」那樣的效果。框架

一鍵式啓停:LifeCycle接口

我在前面說到過,設計就是要找到系統的變化點和不變點。這裏的不變點就是每一個組件都要經歷建立、初始化、啓動這幾個過程,這些狀態以及狀態的轉化是不變的。而變化點是每一個具體組件的初始化方法,也就是啓動方法是不同的。webapp

所以,咱們把不變點抽象出來成爲一個接口,這個接口跟生命週期有關,叫做 LifeCycle。LifeCycle 接口裏應該定義這麼幾個方法:init()、start()、stop() 和 destroy(),每一個具體的組件去實現這些方法。ide

理所固然,在父組件的 init() 方法裏須要建立子組件並調用子組件的 init() 方法。一樣,在父組件的 start() 方法裏也須要調用子組件的 start() 方法,所以調用者能夠無差異的調用各組件的 init() 方法和 start() 方法,這就是組合模式的使用,而且只要調用最頂層組件,也就是 Server 組件的 init() 和 start() 方法,整個 Tomcat 就被啓動起來了。下面是LifeCycle 接口的定義。

趣操做,Tomcat如何實現一鍵式啓停?

可擴展性:LifeCycle事件

咱們再來考慮另外一個問題,那就是系統的可擴展性。由於各個組件 init() 和 start() 方法的具體實現是複雜多變的,好比在 Host 容器的啓動方法裏須要掃描 webapps 目錄下的Web 應用,建立相應的 Context 容器,若是未來須要增長新的邏輯,直接修改 start() 方法?這樣會違反開閉原則,那如何解決這個問題呢?開閉原則說的是爲了擴展系統的功能,你不能直接修改系統中已有的類,可是你能夠定義新的類。

咱們注意到,組件的 init() 和 start() 調用是由它的父組件的狀態變化觸發的,上層組件的初始化會觸發子組件的初始化,上層組件的啓動會觸發子組件的啓動,所以咱們把組件的生命週期定義成一個個狀態,把狀態的轉變看做是一個事件。而事件是有監聽器的,在監聽器裏能夠實現一些邏輯,而且監聽器也能夠方便的添加和刪除,這就是典型的觀察者模式。

具體來講就是在 LifeCycle 接口裏加入兩個方法:添加監聽器和刪除監聽器。除此以外,咱們還須要定義一個 Enum 來表示組件有哪些狀態,以及處在什麼狀態會觸發什麼樣的事件。所以 LifeCycle 接口和 LifeCycleState 就定義成了下面這樣。

趣操做,Tomcat如何實現一鍵式啓停?

從圖上你能夠看到,組件的生命週期有 NEW、INITIALIZING、INITIALIZED、STARTING_PREP、STARTING、STARTED 等,而一旦組件到達相應的狀態就觸發相應的事件,好比 NEW 狀態表示組件剛剛被實例化;而當 init() 方法被調用時,狀態就變成INITIALIZING 狀態,這個時候,就會觸發 BEFORE_INIT_EVENT 事件,若是有監聽器在監聽這個事件,它的方法就會被調用。

重用性:LifeCycleBase抽象基類

有了接口,咱們就要用類去實現接口。通常來講實現類不止一個,不一樣的類在實現接口時每每會有一些相同的邏輯,若是讓各個子類都去實現一遍,就會有重複代碼。那子類如何重用這部分邏輯呢?其實就是定義一個基類來實現共同的邏輯,而後讓各個子類去繼承它,就達到了重用的目的。

而基類中每每會定義一些抽象方法,所謂的抽象方法就是說基類不會去實現這些方法,而是調用這些方法來實現骨架邏輯。抽象方法是留給各個子類去實現的,而且子類必須實現,不然沒法實例化。

好比寶馬和榮威的底盤和骨架實際上是同樣的,只是發動機和內飾等配套是不同的。底盤和骨架就是基類,寶馬和榮威就是子類。僅僅有底盤和骨架還不是一輛真正意義上的車,只能算是半成品,所以在底盤和骨架上會留出一些安裝接口,好比安裝發動機的接口、安裝座椅的接口,這些就是抽象方法。寶馬或者榮威上安裝的發動機和座椅是不同的,也就是具體子類對抽象方法有不一樣的實現。

回到 LifeCycle 接口,Tomcat 定義一個基類 LifeCycleBase 來實現 LifeCycle 接口,把一些公共的邏輯放到基類中去,好比生命狀態的轉變與維護、生命事件的觸發以及監聽器的添加和刪除等,而子類就負責實現本身的初始化、啓動和中止等方法。爲了不跟基類中的方法同名,咱們把具體子類的實現方法改個名字,在後面加上 Internal,叫 initInternal()、startInternal() 等。咱們再來看引入了基類 LifeCycleBase 後的類圖:

趣操做,Tomcat如何實現一鍵式啓停?

從圖上能夠看到,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(LifecycleState.INITIALIZING, null, false);

在這個 setStateInternal 方法裏,會調用監聽器的業務方法。

第三步,調用具體子類實現的抽象方法 initInternal() 方法。我在前面提到過,爲了實現一鍵式啓動,具體組件在實現 initInternal() 方法時,又會調用它的子組件的 init() 方法。

第四步,子組件初始化後,觸發 INITIALIZED 事件的監聽器,相應監聽器的業務方法就會被調用。

setStateInternal(LifecycleState.INITIALIZED, null, false);

總之,LifeCycleBase 調用了抽象方法來實現骨架邏輯。講到這裏, 你可能好奇,LifeCycleBase 負責觸發事件,並調用監聽器的方法,那是何時、誰把監聽器註冊進來的呢?

分爲兩種狀況:

  • Tomcat 自定義了一些監聽器,這些監聽器是父組件在建立子組件的過程當中註冊到子組件的。好比 MemoryLeakTrackingListener 監聽器,用來檢測 Context 容器中的內存泄漏,這個監聽器是 Host 容器在建立 Context 容器時註冊到 Context 中的。
  • 咱們還能夠在 server.xml 中定義本身的監聽器,Tomcat 在啓動時會解析 server.xml,建立監聽器並註冊到容器組件。

生週期管理整體類圖

經過上面的學習,我相信你對 Tomcat 組件的生命週期的管理有了深刻的理解,咱們再來看一張整體類圖繼續加深印象。

趣操做,Tomcat如何實現一鍵式啓停?

這裏請你注意,圖中的 StandardServer、StandardService 等是 Server 和 Service 組件的具體實現類,它們都繼承了 LifeCycleBase。

StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應容器組件的具體實現類,由於它們都是容器,因此繼承了 ContainerBase 抽象基類,而ContainerBase 實現了 Container 接口,也繼承了 LifeCycleBase 類,它們的生命週期管理接口和功能接口是分開的,這也符合設計中接口分離的原則。

小結

Tomcat 爲了實現一鍵式啓停以及優雅的生命週期管理,並考慮到了可擴展性和可重用性,將面向對象思想和設計模式發揮到了極致,分別運用了組合模式、觀察者模式、骨架抽象類和模板方法。

若是你須要維護一堆具備父子關係的實體,能夠考慮使用組合模式。
觀察者模式聽起來「高大上」,其實就是當一個事件發生後,須要執行一連串更新操做。傳統的實現方式是在事件響應代碼裏直接加更新邏輯,當更新邏輯加多了以後,代碼會變得臃

腫,而且這種方式是緊耦合的、侵入式的。而觀察者模式實現了低耦合、非侵入式的通知與更新機制。

而模板方法在抽象基類中常常用到,用來實現通用邏輯。

相關文章
相關標籤/搜索