1. 程式人生 > >Tomcat 架構原理解析到架構設計借鑑

Tomcat 架構原理解析到架構設計借鑑

> Tomcat 發展這麼多年,已經比較成熟穩定。在如今『追新求快』的時代,Tomcat 作為 Java Web 開發必備的工具似乎變成了『熟悉的陌生人』,難道說如今就沒有必要深入學習它了麼?學習它我們又有什麼收穫呢? **靜下心來,細細品味經典的開源作品** 。提升我們的「內功」,具體來說就是學習大牛們如何設計、架構一箇中間件系統,並且讓這些經驗為我所用。 美好的事物往往是整潔而優雅的。但這並不等於簡單,而是要將複雜的系統分解成一個個小模組,並且各個模組的職責劃分也要清晰合理。 與此相反的是凌亂無序,比如你看到城中村一堆互相糾纏在一起的電線,可能會感到不適。維護的程式碼一個類幾千行、一個方法好幾百行。方法之間相互耦合糅雜在一起,你可能會說 what the f*k! ![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/演算法/20200406202331.png) ## 學習目的 ### 掌握 Tomcat 架構設計與原理提高內功 **巨集觀上看** Tomcat 作為一個 「`Http` 伺服器 + `Servlet` 容器」,對我們遮蔽了應用層協議和網路通訊細節,給我們的是標準的 `Request` 和 `Response` 物件;對於具體的業務邏輯則作為變化點,交給我們來實現。我們使用了`SpringMVC` 之類的框架,可是卻從來不需要考慮 `TCP` 連線、 `Http` 協議的資料處理與響應。就是因為 Tomcat 已經為我們做好了這些,我們只需要關注每個請求的具體業務邏輯。 **微觀上看** `Tomcat` 內部也隔離了變化點與不變點,使用了元件化設計,目的就是為了實現「俄羅斯套娃式」的高度定製化(組合模式),而每個元件的生命週期管理又有一些共性的東西,則被提取出來成為介面和抽象類,讓具體子類實現變化點,也就是模板方法設計模式。 當今流行的微服務也是這個思路,按照功能將單體應用拆成「微服務」,拆分過程要將共性提取出來,而這些共性就會成為核心的基礎服務或者通用庫。「中臺」思想亦是如此。 設計模式往往就是封裝變化的一把利器,合理的運用設計模式能讓我們的程式碼與系統設計變得優雅且整潔。 這就是學習優秀開源軟體能獲得的「內功」,從不會過時,其中的設計思想與哲學才是根本之道。從中借鑑設計經驗,合理運用設計模式封裝變與不變,更能從它們的原始碼中汲取經驗,提升自己的系統設計能力。 ### 巨集觀理解一個請求如何與 Spring 聯絡起來 在工作過程中,我們對 Java 語法已經很熟悉了,甚至「背」過一些設計模式,用過很多 Web 框架,但是很少有機會將他們用到實際專案中,讓自己獨立設計一個系統似乎也是根據需求一個個 Service 實現而已。腦子裡似乎沒有一張 Java Web 開發全景圖,比如我並不知道瀏覽器的請求是怎麼跟 Spring 中的程式碼聯絡起來的。 為了突破這個瓶頸,為何不站在巨人的肩膀上學習優秀的開源系統,看大牛們是如何思考這些問題。 學習 Tomcat 的原理,我發現 `Servlet` 技術是 Web 開發的原點,幾乎所有的 Java Web 框架(比如 Spring)都是基於 `Servlet` 的封裝,Spring 應用本身就是一個 `Servlet`(`DispatchSevlet`),而 Tomcat 和 Jetty 這樣的 Web 容器,負責載入和執行 `Servlet`。如圖所示: ![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/演算法/20200406210730.png) ### 提升自己的系統設計能力 學習 Tomcat ,我還發現用到不少 Java 高階技術,比如 Java 多執行緒併發程式設計、Socket 網路程式設計以及反射等。之前也只是瞭解這些技術,為了面試也背過一些題。但是總感覺「知道」與會用之間存在一道溝壑,通過對 Tomcat 原始碼學習,我學會了什麼場景去使用這些技術。 還有就是系統設計能力,比如面向介面程式設計、元件化組合模式、骨架抽象類、一鍵式啟停、物件池技術以及各種設計模式,比如模板方法、觀察者模式、責任鏈模式等,之後我也開始模仿它們並把這些設計思想運用到實際的工作中。 ## 整體架構設計 今天咱們就來一步一步分析 Tomcat 的設計思路,一方面我們可以學到 Tomcat 的總體架構,學會從巨集觀上怎麼去設計一個複雜系統,怎麼設計頂層模組,以及模組之間的關係;另一方面也為我們深入學習 Tomcat 的工作原理打下基礎。 Tomcat 啟動流程:`startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()` Tomcat 實現的 2 個核心功能: - 處理 `Socket` 連線,負責網路位元組流與 `Request` 和 `Response` 物件的轉化。 - 載入並管理 `Servlet` ,以及處理具體的 `Request` 請求。 **所以 Tomcat 設計了兩個核心元件聯結器(Connector)和容器(Container)。聯結器負責對外交流,容器負責內部 處理** `Tomcat`為了實現支援多種 `I/O` 模型和應用層協議,一個容器可能對接多個聯結器,就好比一個房間有多個門。 ![Tomcat整體架構](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407171031.png) - Server 對應的就是一個 Tomcat 例項。 - Service 預設只有一個,也就是一個 Tomcat 例項預設一個 Service。 - Connector:一個 Service 可能多個 聯結器,接受不同連線協議。 - Container: 多個聯結器對應一個容器,頂層容器其實就是 Engine。 **每個元件都有對應的生命週期,需要啟動,同時還要啟動自己內部的子元件,比如一個 Tomcat 例項包含一個 Service,一個 Service 包含多個聯結器和一個容器。而一個容器包含多個 Host, Host 內部可能有多個 Contex t 容器,而一個 Context 也會包含多個 Servlet,所以 Tomcat 利用組合模式管理元件每個元件,對待過個也想對待單個組一樣對待**。整體上每個元件設計就像是「俄羅斯套娃」一樣。 ### 聯結器 在開始講聯結器前,我先鋪墊一下 `Tomcat`支援的多種 `I/O` 模型和應用層協議。 `Tomcat`支援的 `I/O` 模型有: - `NIO`:非阻塞 `I/O`,採用 `Java NIO` 類庫實現。 - `NIO2`:非同步`I/O`,採用 `JDK 7` 最新的 `NIO2` 類庫實現。 - `APR`:採用 `Apache`可移植執行庫實現,是 `C/C++` 編寫的本地庫。 Tomcat 支援的應用層協議有: - `HTTP/1.1`:這是大部分 Web 應用採用的訪問協議。 - `AJP`:用於和 Web 伺服器整合(如 Apache)。 - `HTTP/2`:HTTP 2.0 大幅度的提升了 Web 效能。 所以一個容器可能對接多個聯結器。聯結器對 `Servlet` 容器遮蔽了網路協議與 `I/O` 模型的區別,無論是 `Http` 還是 `AJP`,在容器中獲取到的都是一個標準的 `ServletRequest` 物件。 細化聯結器的功能需求就是: - 監聽網路埠。 - 接受網路連線請求。 - 讀取請求網路位元組流。 - 根據具體應用層協議(`HTTP/AJP`)解析位元組流,生成統一的 `Tomcat Request` 物件。 - 將 `Tomcat Request` 物件轉成標準的 `ServletRequest`。 - 呼叫 `Servlet`容器,得到 `ServletResponse`。 - 將 `ServletResponse`轉成 `Tomcat Response` 物件。 - 將 `Tomcat Response` 轉成網路位元組流。 - 將響應位元組流寫回給瀏覽器。 需求列清楚後,我們要考慮的下一個問題是,聯結器應該有哪些子模組?優秀的模組化設計應該考慮**高內聚、低耦合**。 - **高內聚**是指相關度比較高的功能要儘可能集中,不要分散。 - **低耦合**是指兩個相關的模組要儘可能減少依賴的部分和降低依賴的程度,不要讓兩個模組產生強依賴。 我們發現聯結器需要完成 3 個**高內聚**的功能: - 網路通訊。 - 應用層協議解析。 - `Tomcat Request/Response` 與 `ServletRequest/ServletResponse` 的轉化。 因此 Tomcat 的設計者設計了 3 個元件來實現這 3 個功能,分別是 `EndPoint、Processor 和 Adapter`。 網路通訊的 I/O 模型是變化的, 應用層協議也是變化的,但是整體的處理邏輯是不變的,`EndPoint` 負責提供位元組流給 `Processor`,`Processor`負責提供 `Tomcat Request` 物件給 `Adapter`,`Adapter`負責提供 `ServletRequest`物件給容器。 **封裝變與不變** 因此 Tomcat 設計了一系列抽象基類來**封裝這些穩定的部分**,抽象基類 `AbstractProtocol`實現了 `ProtocolHandler`介面。每一種應用層協議有自己的抽象基類,比如 `AbstractAjpProtocol`和 `AbstractHttp11Protocol`,具體協議的實現類擴充套件了協議層抽象基類。 這就是模板方法設計模式的運用。 ![應用層協議抽象](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407174647.png) 總結下來,聯結器的三個核心元件 `Endpoint`、`Processor`和 `Adapter`來分別做三件事情,其中 `Endpoint`和 `Processor`放在一起抽象成了 `ProtocolHandler`元件,它們的關係如下圖所示。 ![聯結器](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407174755.png) #### ProtocolHandler 元件 主要處理 **網路連線** 和 **應用層協議** ,包含了兩個重要部件 EndPoint 和 Processor,兩個元件組合形成 ProtocoHandler,下面我來詳細介紹它們的工作原理。 ##### EndPoint `EndPoint`是通訊端點,即通訊監聽的介面,是具體的 Socket 接收和傳送處理器,是對傳輸層的抽象,因此 `EndPoint`是用來實現 `TCP/IP` 協議資料讀寫的,本質呼叫作業系統的 socket 介面。 `EndPoint`是一個介面,對應的抽象實現類是 `AbstractEndpoint`,而 `AbstractEndpoint`的具體子類,比如在 `NioEndpoint`和 `Nio2Endpoint`中,有兩個重要的子元件:`Acceptor`和 `SocketProcessor`。 其中 Acceptor 用於監聽 Socket 連線請求。`SocketProcessor`用於處理 `Acceptor` 接收到的 `Socket`請求,它實現 `Runnable`介面,在 `Run`方法裡呼叫應用層協議處理元件 `Processor` 進行處理。為了提高處理能力,`SocketProcessor`被提交到執行緒池來執行。 我們知道,對於 Java 的多路複用器的使用,無非是兩步: 1. 建立一個 Seletor,在它身上註冊各種感興趣的事件,然後呼叫 select 方法,等待感興趣的事情發生。 2. 感興趣的事情發生了,比如可以讀了,這時便建立一個新的執行緒從 Channel 中讀資料。 在 Tomcat 中 `NioEndpoint` 則是 `AbstractEndpoint` 的具體實現,裡面元件雖然很多,但是處理邏輯還是前面兩步。它一共包含 `LimitLatch`、`Acceptor`、`Poller`、`SocketProcessor`和 `Executor` 共 5 個元件,分別分工合作實現整個 TCP/IP 協議的處理。 - LimitLatch 是連線控制器,它負責控制最大連線數,NIO 模式下預設是 10000,達到這個閾值後,連線請求被拒絕。 - `Acceptor`跑在一個單獨的執行緒裡,它在一個死迴圈裡呼叫 `accept`方法來接收新連線,一旦有新的連線請求到來,`accept`方法返回一個 `Channel` 物件,接著把 `Channel`物件交給 Poller 去處理。 - `Poller` 的本質是一個 `Selector`,也跑在單獨執行緒裡。`Poller`在內部維護一個 `Channel`陣列,它在一個死迴圈裡不斷檢測 `Channel`的資料就緒狀態,一旦有 `Channel`可讀,就生成一個 `SocketProcessor`任務物件扔給 `Executor`去處理。 - SocketProcessor 實現了 Runnable 介面,其中 run 方法中的 `getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);` 程式碼則是獲取 handler 並執行處理 socketWrapper,最後通過 socket 獲取合適應用層協議處理器,也就是呼叫 Http11Processor 元件來處理請求。Http11Processor 讀取 Channel 的資料來生成 ServletRequest 物件,Http11Processor 並不是直接讀取 Channel 的。這是因為 Tomcat 支援同步非阻塞 I/O 模型和非同步 I/O 模型,在 Java API 中,相應的 Channel 類也是不一樣的,比如有 AsynchronousSocketChannel 和 SocketChannel,為了對 Http11Processor 遮蔽這些差異,Tomcat 設計了一個包裝類叫作 SocketWrapper,Http11Processor 只調用 SocketWrapper 的方法去讀寫資料。 - `Executor`就是執行緒池,負責執行 `SocketProcessor`任務類,`SocketProcessor` 的 `run`方法會呼叫 `Http11Processor` 來讀取和解析請求資料。我們知道,`Http11Processor`是應用層協議的封裝,它會呼叫容器獲得響應,再把響應通過 `Channel`寫出。 工作流程如下所示: ![NioEndPoint](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/NioEndPoint.jpg) ##### Processor Processor 用來實現 HTTP 協議,Processor 接收來自 EndPoint 的 Socket,讀取位元組流解析成 Tomcat Request 和 Response 物件,並通過 Adapter 將其提交到容器處理,Processor 是對應用層協議的抽象。 ![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407180342.png) **從圖中我們看到,EndPoint 接收到 Socket 連線後,生成一個 SocketProcessor 任務提交到執行緒池去處理,SocketProcessor 的 Run 方法會呼叫 HttpProcessor 元件去解析應用層協議,Processor 通過解析生成 Request 物件後,會呼叫 Adapter 的 Service 方法,方法內部通過 以下程式碼將請求傳遞到容器中。** ```java // Calling the container connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); ``` #### Adapter 元件 由於協議的不同,Tomcat 定義了自己的 `Request` 類來存放請求資訊,這裡其實體現了面向物件的思維。但是這個 Request 不是標準的 `ServletRequest` ,所以不能直接使用 Tomcat 定義 Request 作為引數直接容器。 Tomcat 設計者的解決方案是引入 `CoyoteAdapter`,這是介面卡模式的經典運用,聯結器呼叫 `CoyoteAdapter` 的 `Sevice` 方法,傳入的是 `Tomcat Request` 物件,`CoyoteAdapter`負責將 `Tomcat Request` 轉成 `ServletRequest`,再呼叫容器的 `Service`方法。 ### 容器 聯結器負責外部交流,容器負責內部處理。具體來說就是,聯結器處理 Socket 通訊和應用層協議的解析,得到 `Servlet`請求;而容器則負責處理 `Servlet`請求。 容器:顧名思義就是拿來裝東西的, 所以 Tomcat 容器就是拿來裝載 `Servlet`。 Tomcat 設計了 4 種容器,分別是 `Engine`、`Host`、`Context`和 `Wrapper`。`Server` 代表 Tomcat 例項。 要注意的是這 4 種容器不是平行關係,屬於父子關係,如下圖所示: ![容器](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407185056.png) 你可能會問,為啥要設計這麼多層次的容器,這不是增加複雜度麼?其實這背後的考慮是,**Tomcat 通過一種分層的架構,使得 Servlet 容器具有很好的靈活性。因為這裡正好符合一個 Host 多個 Context, 一個 Context 也包含多個 Servlet,而每個元件都需要統一生命週期管理,所以組合模式設計這些容器** `Wrapper` 表示一個 `Servlet` ,`Context` 表示一個 Web 應用程式,而一個 Web 程式可能有多個 `Servlet` ;`Host` 表示一個虛擬主機,或者說一個站點,一個 Tomcat 可以配置多個站點(Host);一個站點( Host) 可以部署多個 Web 應用;`Engine` 代表 引擎,用於管理多個站點(Host),一個 Service 只能有 一個 `Engine`。 可通過 Tomcat 配置檔案加深對其層次關係理解。 ```xml ``` 如何管理這些容器?我們發現容器之間具有父子關係,形成一個樹形結構,是不是想到了設計模式中的 **組合模式** 。 Tomcat 就是用組合模式來管理這些容器的。具體實現方法是,**所有容器元件都實現了 `Container`介面,因此組合模式可以使得使用者對單容器物件和組合容器物件的使用具有一致性**。這裡單容器物件指的是最底層的 `Wrapper`,組合容器物件指的是上面的 `Context`、`Host`或者 `Engine`。`Container` 介面定義如下: ```java public interface Container extends Lifecycle { public void setName(String name); public Container getParent(); public void setParent(Container container); public void addChild(Container child); public void removeChild(Container child); public Container findChild(String name); } ``` 我們看到了`getParent`、`SetParent`、`addChild`和 `removeChild`等方法,這裡正好驗證了我們說的組合模式。我們還看到 `Container`介面拓展了 `Lifecycle` ,Tomcat 就是通過 `Lifecycle` 統一管理所有容器的元件的生命週期。通過組合模式管理所有容器,拓展 `Lifecycle` 實現對每個元件的生命週期管理 ,`Lifecycle` 主要包含的方法`init()、start()、stop() 和 destroy()`。 #### 請求定位 Servlet 的過程 一個請求是如何定位到讓哪個 `Wrapper` 的 `Servlet` 處理的?答案是,Tomcat 是用 Mapper 元件來完成這個任務的。 `Mapper` 元件的功能就是將使用者請求的 `URL` 定位到一個 `Servlet`,它的工作原理是:`Mapper`元件裡儲存了 Web 應用的配置資訊,其實就是**容器元件與訪問路徑的對映關係**,比如 `Host`容器裡配置的域名、`Context`容器裡的 `Web`應用路徑,以及 `Wrapper`容器裡 `Servlet` 對映的路徑,你可以想象這些配置資訊就是一個多層次的 `Map`。 當一個請求到來時,`Mapper` 元件通過解析請求 URL 裡的域名和路徑,再到自己儲存的 Map 裡去查詢,就能定位到一個 `Servlet`。請你注意,一個請求 URL 最後只會定位到一個 `Wrapper`容器,也就是一個 `Servlet`。 ![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407192105.png) 假如有使用者訪問一個 URL,比如圖中的`http://user.shopping.com:8080/order/buy`,Tomcat 如何將這個 URL 定位到一個 Servlet 呢? 1. **首先根據協議和埠號確定 Service 和 Engine**。Tomcat 預設的 HTTP 聯結器監聽 8080 埠、預設的 AJP 聯結器監聽 8009 埠。上面例子中的 URL 訪問的是 8080 埠,因此這個請求會被 HTTP 聯結器接收,而一個聯結器是屬於一個 Service 元件的,這樣 Service 元件就確定了。我們還知道一個 Service 元件裡除了有多個聯結器,還有一個容器元件,具體來說就是一個 Engine 容器,因此 Service 確定了也就意味著 Engine 也確定了。 2. **根據域名選定 Host。** Service 和 Engine 確定後,Mapper 元件通過 URL 中的域名去查詢相應的 Host 容器,比如例子中的 URL 訪問的域名是`user.shopping.com`,因此 Mapper 會找到 Host2 這個容器。 3. **根據 URL 路徑找到 Context 元件。** Host 確定以後,Mapper 根據 URL 的路徑來匹配相應的 Web 應用的路徑,比如例子中訪問的是 /order,因此找到了 Context4 這個 Context 容器。 4. **根據 URL 路徑找到 Wrapper(Servlet)。** Context 確定後,Mapper 再根據 web.xml 中配置的 Servlet 對映路徑來找到具體的 Wrapper 和 Servlet。 聯結器中的 Adapter 會呼叫容器的 Service 方法來執行 Servlet,最先拿到請求的是 Engine 容器,Engine 容器對請求做一些處理後,會把請求傳給自己子容器 Host 繼續處理,依次類推,最後這個請求會傳給 Wrapper 容器,Wrapper 會呼叫最終的 Servlet 來處理。那麼這個呼叫過程具體是怎麼實現的呢?答案是使用 Pipeline-Valve 管道。 `Pipeline-Valve` 是責任鏈模式,責任鏈模式是指在一個請求處理的過程中有很多處理者依次對請求進行處理,每個處理者負責做自己相應的處理,處理完之後將再呼叫下一個處理者繼續處理,Valve 表示一個處理點(也就是一個處理閥門),因此 `invoke`方法就是來處理請求的。 ```java public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void invoke(Request request, Response response) } ``` 繼續看 Pipeline 介面 ```java public interface Pipeline { public void addValve(Valve valve); public Valve getBasic(); public void setBasic(Valve valve); public Valve getFirst(); } ``` `Pipeline`中有 `addValve`方法。Pipeline 中維護了 `Valve`連結串列,`Valve`可以插入到 `Pipeline`中,對請求做某些處理。我們還發現 Pipeline 中沒有 invoke 方法,因為整個呼叫鏈的觸發是 Valve 來完成的,`Valve`完成自己的處理後,呼叫 `getNext.invoke()` 來觸發下一個 Valve 呼叫。 其實每個容器都有一個 Pipeline 物件,只要觸發了這個 Pipeline 的第一個 Valve,這個容器裡 `Pipeline`中的 Valve 就都會被呼叫到。但是,不同容器的 Pipeline 是怎麼鏈式觸發的呢,比如 Engine 中 Pipeline 需要呼叫下層容器 Host 中的 Pipeline。 這是因為 `Pipeline`中還有個 `getBasic`方法。這個 `BasicValve`處於 `Valve`連結串列的末端,它是 `Pipeline`中必不可少的一個 `Valve`,負責呼叫下層容器的 Pipeline 裡的第一個 Valve。 ![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/20200407193058.png) 整個過程分是通過聯結器中的 `CoyoteAdapter` 觸發,它會呼叫 Engine 的第一個 Valve: ```java @Override public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) { // 省略其他程式碼 // Calling the container connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); ... } ``` Wrapper 容器的最後一個 Valve 會建立一個 Filter 鏈,並呼叫 `doFilter()` 方法,最終會調到 `Servlet`的 `service`方法。 前面我們不是講到了 `Filter`,似乎也有相似的功能,那 `Valve` 和 `Filter`有什麼區別嗎?它們的區別是: - `Valve`是 `Tomcat`的私有機制,與 Tomcat 的基礎架構 `API`是緊耦合的。`Servlet API`是公有的標準,所有的 Web 容器包括 Jetty 都支援 Filter 機制。 - 另一個重要的區別是 `Valve`工作在 Web 容器級別,攔截所有應用的請求;而 `Servlet Filter` 工作在應用級別,只能攔截某個 `Web` 應用的所有請求。如果想做整個 `Web`容器的攔截器,必須通過 `Valve`來實現。 #### Lifecycle 生命週期 前面我們看到 `Container`容器 繼承了 `Lifecycle` 生命週期。如果想讓一個系統能夠對外提供服務,我們需要建立、組裝並啟動這些元件;在服務停止的時候,我們還需要釋放資源,銷燬這些元件,因此這是一個動態的過程。也就是說,Tomcat 需要動態地管理這些元件的生命週期。 如何統一管理元件的建立、初始化、啟動、停止和銷燬?如何做到程式碼邏輯清晰?如何方便地新增或者刪除元件?如何做到元件啟動和停止不遺漏、不重複? ##### 一鍵式啟停:LifeCycle 介面 設計就是要找到系統的變化點和不變點。這裡的不變點就是每個元件都要經歷建立、初始化、啟動這幾個過程,這些狀態以及狀態的轉化是不變的。而變化點是每個具體元件的初始化方法,也就是啟動方法是不一樣的。 因此,Tomcat 把不變點抽象出來成為一個介面,這個介面跟生命週期有關,叫作 LifeCycle。LifeCycle 接口裡定義這麼幾個方法:`init()、start()、stop() 和 destroy()`,每個具體的元件(也就是容器)去實現這些方法。 在父元件的 `init()` 方法裡需要建立子元件並呼叫子元件的 `init()` 方法。同樣,在父元件的 `start()`方法裡也需要呼叫子元件的 `start()` 方法,因此呼叫者可以無差別的呼叫各元件的 `init()` 方法和 `start()` 方法,這就是**組合模式**的使用,並且只要呼叫最頂層元件,也就是 Server 元件的 `init()`和`start()` 方法,整個 Tomcat 就被啟動起來了。所以 Tomcat 採取組合模式管理容器,容器繼承 LifeCycle 介面,這樣就可以向針對單個物件一樣一鍵管理各個容器的生命週期,整個 Tomcat 就啟動起來。 ##### 可擴充套件性:LifeCycle 事件 我們再來考慮另一個問題,那就是系統的可擴充套件性。因為各個元件`init()` 和 `start()` 方法的具體實現是複雜多變的,比如在 Host 容器的啟動方法裡需要掃描 webapps 目錄下的 Web 應用,建立相應的 Context 容器,如果將來需要增加新的邏輯,直接修改`start()` 方法?這樣會違反開閉原則,那如何解決這個問題呢?開閉原則說的是為了擴充套件系統的功能,你不能直接修改系統中已有的類,但是你可以定義新的類。 **元件的 `init()` 和 `start()` 呼叫是由它的父元件的狀態變化觸發的,上層元件的初始化會觸發子元件的初始化,上層元件的啟動會觸發子元件的啟動,因此我們把元件的生命週期定義成一個個狀態,把狀態的轉變看作是一個事件。而事件是有監聽器的,在監聽器裡可以實現一些邏輯,並且監聽器也可以方便的新增和刪除**,這就是典型的**觀察者模式**。 以下就是 `Lyfecycle` 介面的定義: ![`Lyfecycle` ](https://magebyte.oss-cn-shenzhen.aliyuncs.com/tomcat/Lifecycle.png) ##### 重用性:LifeCycleBase 抽象基類 再次看到抽象模板設計模式。 有了介面,我們就要用類去實現介面。一般來說實現類不止一個,不同的類在實現介面時往往會有一些相同的邏輯,如果讓各個子類都去實現一遍,就會有重複程式碼。那子類如何重用這部分邏輯呢?其實就是定義一個基類來實現共同的邏輯,然後讓各個子類去繼承它,就達到了重用的目的。 Tomcat 定義一個基類 LifeCycleBase 來實現 LifeCycle 介面,把一些公共的邏輯放到基類中去,比如生命狀態的轉變與維護、生命事件的觸發以及監聽器的新增和刪除等,而子類就負責實現自己的初始化、啟動和停止等方法。 ```java public abstract class LifecycleBase implements Lifecycle{ // 持有所有的觀察者 private fi