天天看點

面試官:談談 Tomcat 架構及啟動過程,我一臉懵逼。。

​​https://github.com/c-rainstorm/blog/tree/master/tomcat​​

這個題目命的其實是很大的,寫的時候還是很忐忑的,但我盡可能把這個過程描述清楚。因為這是讀過源碼以後寫的總結,在寫的過程中可能會忽略一些前提條件,如果有哪些比較突兀就出現,或不好了解的地方可以給我提 Issue,我會盡快補充修訂相關内容。

很多東西在時序圖中展現的已經非常清楚了,沒有必要再一步一步的作介紹,是以本文以圖為主,然後對部分内容加以簡單解釋。

繪制圖形使用的工具是 PlantUML + Visual Studio Code + PlantUML Extension

本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 為标準。

​​https://tomcat.apache.org/tomcat-9.0-doc/index.html​​

Overview

面試官:談談 Tomcat 架構及啟動過程,我一臉懵逼。。
  1. Bootstrap 作為 Tomcat 對外界的啟動類,在 $CATALINA_BASE/bin 目錄下,它通過反射建立 Catalina 的執行個體并對其進行初始化及啟動。
  2. Catalina 解析 $CATALINA_BASE/conf/server.xml 檔案并建立 StandardServer、StandardService、StandardEngine、StandardHost 等
  3. StandardServer 代表的是整個 Servlet 容器,他包含一個或多個 StandardService
  4. StandardService 包含一個或多個 Connector,和一個 Engine,Connector 和 Engine 都是在解析 conf/server.xml 檔案時建立的,Engine 在 Tomcat 的标準實作是 StandardEngine
  5. MapperListener 實作了 LifecycleListener 和 ContainerListener 接口用于監聽容器事件和生命周期事件。該監聽器執行個體監聽所有的容器,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,當容器有變動時,注冊容器到 Mapper。
  6. Mapper 維護了 URL 到容器的映射關系。當請求到來時會根據 Mapper 中的映射資訊決定将請求映射到哪一個 Host、Context、Wrapper。
  7. Http11NioProtocol 用于處理 HTTP/1.1 的請求
  8. NioEndpoint 是連接配接的端點,在請求處理流程中該類是核心類,會重點介紹。
  9. CoyoteAdapter 用于将請求從 Connctor 交給 Container 處理。使 Connctor 和 Container 解耦。
  10. StandardEngine 代表的是 Servlet 引擎,用于處理 Connector 接受的 Request。包含一個或多個 Host(虛拟主機), Host 的标準實作是 StandardHost。
  11. StandardHost 代表的是虛拟主機,用于部署該虛拟主機上的應用程式。通常包含多個 Context (Context 在 Tomcat 中代表應用程式)。Context 在 Tomcat 中的标準實作是 StandardContext。
  12. StandardContext 代表一個獨立的應用程式,通常包含多個 Wrapper,一個 Wrapper 容器封裝了一個 Servlet,Wrapper的标準實作是 StandardWrapper。
  13. StandardPipeline 元件代表一個流水線,與 Valve(閥)結合,用于處理請求。 StandardPipeline 中含有多個 Valve, 當需要處理請求時,會逐一調用 Valve 的 invoke 方法對 Request 和 Response 進行處理。特别的,其中有一個特殊的 Valve 叫 basicValve,每一個标準容器都有一個指定的 BasicValve,他們做的是最核心的工作。
  • StandardEngine 的是 StandardEngineValve,他用來将 Request 映射到指定的 Host;
  • StandardHost 的是 StandardHostValve, 他用來将 Request 映射到指定的 Context;
  • StandardContext 的是 StandardContextValve,它用來将 Request 映射到指定的 Wrapper;
  • StandardWrapper 的是 StandardWrapperValve,他用來加載 Rquest 所指定的 Servlet,并調用 Servlet 的 Service 方法。

Tomcat init

面試官:談談 Tomcat 架構及啟動過程,我一臉懵逼。。
  • 當通過 ./startup.sh 腳本或直接通過 java 指令來啟動 Bootstrap 時,Tomcat 的啟動過程就正式開始了,啟動的入口點就是 Bootstrap 類的 main 方法。
  • 啟動的過程分為兩步,分别是 init 和 start,本節主要介紹 init;
  • 初始化類加載器。
  1. 通過從 CatalinaProperties 類中擷取 common.loader 等屬性,獲得類加載器的掃描倉庫。CatalinaProperties 類在的靜态塊中調用了 loadProperties() 方法,從 conf/catalina.properties 檔案中加載了屬性.(即在類建立的時候屬性就已經加載好了)。
  2. 通過 ClassLoaderFactory 建立 URLClassLoader 的執行個體
  • 通過反射建立 Catalina 的執行個體并設定 parentClassLoader
  • setAwait(true)。設定 Catalina 的 await 屬性為 true。在 Start 階段尾部,若該屬性為 true,Tomcat 會在 main 線程中監聽 SHUTDOWN 指令,預設端口是 8005.當收到該指令後執行 Catalina 的 stop() 方法關閉 Tomcat 伺服器。
  • createStartDigester()。Catalina 的該方法用于建立一個 Digester 執行個體,并添加解析 conf/server.xml 的 RuleSet。Digester 原本是 Apache 的一個開源項目,專門解析 XML 檔案的,但我看 Tomcat-9.0.0.M22 中直接将這些類整合到 Tomcat 内部了,而不是引入 jar 檔案。
  • parse() 方法就是 Digester 處理 conf/server.xml 建立各個元件的過程。值的一提的是這些元件都是使用反射的方式來建立的。特别的,在建立 Digester 的時候,添加了一些特别的 rule Set,用于建立一些十分核心的元件,這些元件在 conf/server.xml 中沒有但是其作用都比較大,這裡做下簡單介紹,當 Start 時用到了再詳細說明:
  1. EngineConfig。LifecycleListener 的實作類,觸發 Engine 的生命周期事件後調用,這個監聽器沒有特别大的作用,就是列印一下日志
  2. HostConfig。LifecycleListener 的實作類,觸發 Host 的生命周期事件後調用。這個監聽器的作用就是部署應用程式,這包括 conf/// 目錄下所有的 Context xml 檔案 和 webapps 目錄下的應用程式,不管是 war 檔案還是已解壓的目錄。 另外背景程序對應用程式的熱部署也是由該監聽器負責的。
  3. ContextConfig。LifecycleListener 的實作類,觸發 Context 的生命周期事件時調用。這個監聽器的作用是配置應用程式,它會讀取并合并 conf/web.xml 和 應用程式的 web.xml,分析 /WEB-INF/classes/ 和 /WEB-INF/lib/*.jar中的 Class 檔案的注解,将其中所有的 Servlet、ServletMapping、Filter、FilterMapping、Listener 都配置到 StandardContext 中,以備後期使用。當然了 web.xml 中還有一些其他的應用程式參數,最後都會一并配置到 StandardContext 中。
  • reconfigureStartStopExecutor() 用于重新配置啟動和停止子容器的 Executor。預設是 1 個線程。我們可以配置 conf/server.xml 中 Engine 的 startStopThreads,來指定用于啟動和停止子容器的線程數量,如果配置 0 的話會使用 Runtime.getRuntime().availableProcessors() 作為線程數,若配置為負數的話會使用 Runtime.getRuntime().availableProcessors() + 配置值,若和小與 1 的話,使用 1 作為線程數。當線程數是 1 時,使用 InlineExecutorService 它直接使用目前線程來執行啟動停止操作,否則使用 ThreadPoolExecutor 來執行,其最大線程數為我們配置的值。
  • 需要注意的是 Host 的 init 操作是在 Start 階段來做的, StardardHost 建立好後其 state 屬性的預設值是 LifecycleState.NEW,是以在其調用 startInternal() 之前會進行一次初始化。

Tomcat Start[Deployment]

面試官:談談 Tomcat 架構及啟動過程,我一臉懵逼。。
  • 圖中從 StandardHost Start StandardContext 的這步其實在真正的執行流程中會直接跳過,因為 conf/server.xml 檔案中并沒有配置任何的 Context,是以在 findChildren() 查找子容器時會傳回空數組,是以之後周遊子容器來啟動子容器的 for 循環就直接跳過了。
  • 觸發 Host 的 BEFORE_START_EVENT 生命周期事件,HostConfig 調用其 beforeStart() 方法建立 CATALINA_BASE/webapps& $CATALINA_BASE/conf/// 目錄。
  • 觸發 Host 的 START_EVENT 生命周期事件,HostConfig 調用其 start() 方法開始部署已在 CATALINA_BASE/webapps & $CATALINA_BASE/conf/// 目錄下的應用程式。
  1. 解析 $CATALINA_BASE/conf/// 目錄下所有定義 Context 的 XML 檔案,并添加到 StandardHost。這些 XML 檔案稱為應用程式描述符。正因為如此,我們可以配置一個虛拟路徑來儲存應用程式中用到的圖檔,詳細的配置過程請參考 開發環境配置指南 – 6.3. 配置圖檔存放目錄
  2. 部署 $CATALINA_BASE/webapps 下所有的 WAR 檔案,并添加到 StandardHost。
  3. 部署 $CATALINA_BASE/webapps 下所有已解壓的目錄,并添加到 StandardHost。

特别的,添加到 StandardHost 時,會直接調用 StandardContext 的 start() 方法來啟動應用程式。啟動應用程式步驟請看 Context Start 一節。

  • 在 StandardEngine 和 StandardContext 啟動時都會調用各自的 threadStart() 方法,該方法會建立一個新的背景線程來處理該該容器和子容器及容器内各元件的背景事件。StandardEngine 會直接建立一個背景線程,StandardContext 預設是不建立的,和 StandardEngine 共用同一個。背景線程處理機制是周期調用元件的 backgroundProcess() 方法。詳情請看 Background process 一節。
  • MapperListener
  1. addListeners(engine) 方法會将該監聽器添加到 StandardEngine 和它的所有子容器中
  2. registerHost() 會注冊所有的 Host 和他們的子容器到 Mapper 中,友善後期請求處理時使用。
  3. 當有新的應用(StandardContext)添加進來後,會觸發 Host 的容器事件,然後通過 MapperListener 将新應用的映射注冊到 Mapper 中。
  • Start 工作都做完以後 Catalina 會建立一個 CatalinaShutdownHook 并注冊到 JVM。CatalinaShutdownHook 繼承了 Thread,是 Catalina 的内部類。其 run 方法中直接調用了 Catalina 的 stop() 方法來關閉整個伺服器。注冊該 Thread 到 JVM 的原因是防止使用者非正常終止 Tomcat,比如直接關閉指令視窗之類的。當直接關閉指令視窗時,作業系統會向 JVM 發送一個終止信号,然後 JVM 在退出前會逐一啟動已注冊的 ShutdownHook 來關閉相應資源。

Context Start

面試官:談談 Tomcat 架構及啟動過程,我一臉懵逼。。
  • StandRoot 類實作了 WebResourceRoot 接口,它容納了一個應用程式的所有資源,通俗的來說就是部署到 webapps 目錄下對應 Context 的目錄裡的所有資源。因為我對 Tomcat 的資源管理部分暫時不是很感興趣,是以資源管理相關類隻是做了簡單了解,并沒有深入研究源代碼。
  • resourceStart() 方法會對 StandardRoot 進行初始配置
  • postWorkDirectory() 用于建立對應的工作目錄 $CATALINA_BASE/work///, 該目錄用于存放臨時檔案。
  • StardardContext 隻是一個容器,而 ApplicationContext 則是一個應用程式真正的運作環境,相關類及操作會在請求處理流程看完以後進行補充。
  • StardardContext 觸發 CONFIGURE_START_EVENT 生命周期事件,ContextConfig 開始調用 configureStart() 對應用程式進行配置。
  1. 這個過程會解析并合并 conf/web.xml & conf///web.xml.default & webapps//WEB-INF/web.xml 中的配置。
  2. 配置配置檔案中的參數到 StandardContext, 其中主要的包括 Servlet、Filter、Listener。
  3. 因為從 Servlet3.0 以後是直接支援注解的,是以伺服器必須能夠處理加了注解的類。Tomcat 通過分析 WEB-INF/classes/ 中的 Class 檔案和 WEB-INF/lib/ 下的 jar 包将掃描到的 Servlet、Filter、Listerner 注冊到 StandardContext。
  4. setConfigured(true),是非常關鍵的一個操作,它辨別了 Context 的成功配置,若未設定該值為 true 的話,Context 會啟動失敗。

Background process

面試官:談談 Tomcat 架構及啟動過程,我一臉懵逼。。
  • 背景程序的作用就是處理一下 Servlet 引擎中的周期性事件,處理周期預設是 10s。
  • 特别的 StandardHost 的 backgroundProcess() 方法會觸發 Host 的 PERIODIC_EVENT 生命周期事件。然後 HostConfig 會調用其 check() 方法對已加載并進行過重新部署的應用程式進行 reload 或對新部署的應用程式進行熱部署。熱部署跟之前介紹的部署步驟一緻, reload() 過程隻是簡單的順序調用 setPause(true)、stop()、start()、setPause(false),其中 setPause(true) 的作用是暫時停止接受請求。

How to read excellent open source projects

真正的第一次閱讀開源項目源代碼,收獲還是很大的。讓我在架構設計、面向對象思想、設計模式、Clean Code等等各個方面都有了進步。閱讀優秀的開源項目其實是一件很爽的事,因為時不時的會發現一個新的設計思路,然後不由自主的感歎一聲居然還可以這樣!

當然了,讀的時候還是會有一些痛點的,比如說碰到一個變量,但是死活就是找不到初始化的位置,有時通過 Find Usage 工具可以找到,但有些找不到的隻能從頭開始再過一邊源碼。有時碰到一個設計思路死活都想不明白為什麼這樣設計等等,這種情況就隻能通過分析更高一層的架構來解決了等等。

下面我簡單分享一下我是如何閱讀開源項目源碼的。

  • 先找一些介紹該項目架構的書籍來看,項目架構是項目核心中的核心,讀架構讀的是高層次的設計思路,讀源碼讀的是低層次的實作細節。有了高層次的設計思路做指導,源碼讀起來才會得心應手,因為讀的時候心裡很清楚現在在讀的源碼在整個項目架構中處于什麼位置。我在讀 Tomcat 源碼之前先把 《How Tomcat works》 一書過了一邊,然後又看了一下 《Tomcat 架構解析》 的第二章,對 Tomcat 的架構有了初步了解。(PS:《How Tomcat works》一書是全英文的,但讀起來非常流暢,雖然它是基于 Tomcat 4 和 5 的,但 Tomcat 架構沒有非常大的變化,新版的 Tomcat 隻是增加了一些元件,如果你要學習 Tomcat 的話,首推這本書!)
  • 如果實在找不到講架構的書,那就自己動手畫類圖吧!一般來說,開源項目都是為了提供服務的,我們把提供服務的流程作為主線來分析源代碼,這樣目的性會更強一些,将該流程中涉及到的類畫到類圖中,最後得到的類圖就是架構!不過分析之前你要先找到流程的入口點,否則分析就無從開始。以 Tomcat 為例,他的主線流程大緻可以分為 3 個:啟動、部署、請求處理。他們的入口點就是 Bootstrap 類和 接受請求的 Acceptor 類!
  • 有了閱讀思路我們下面來說說工具吧。我使用的閱讀工具是 IntelliJ IDEA,一款十分強大的 IDE,可能比較重量級,如果你有其他更加輕量級的 Linux 平台源碼閱讀工具,可以推薦給我~
  1. Structure 欄目可以自定義列出類中的域、方法,然後還可以按照繼承結構對域和方法進行分組,這樣就可以直接看出來域和方法是在繼承結構中哪個類裡定義的。當你點選方法和域時,還可以自動滾動到源代碼等等。
  2. 在源代碼中 點選右鍵 -> Diagrams -> show Diagram 可以顯示類的繼承結構,圖中包含了該類所有的祖先和所有的接口。在該圖中選擇指定的父類和接口,點選右鍵 -> show Implementations, IDEA 會列出接口的實作類或該類的子類。
  3. FindUsage、Go To Declaration 等等就不再多說了。