面試官:談?wù)?Tomcat 架構(gòu)及啟動過程,我一臉懵逼。。
來源:https://github.com/c-rainstorm/blog/tree/master/tomcat
這個題目命的其實是很大的,寫的時候還是很忐忑的,但我盡可能把這個過程描述清楚。因為這是讀過源碼以后寫的總結(jié),在寫的過程中可能會忽略一些前提條件,如果有哪些比較突兀就出現(xiàn),或不好理解的地方可以給我提 Issue,我會盡快補充修訂相關(guān)內(nèi)容。
很多東西在時序圖中體現(xiàn)的已經(jīng)非常清楚了,沒有必要再一步一步的作介紹,所以本文以圖為主,然后對部分內(nèi)容加以簡單解釋。
繪制圖形使用的工具是 PlantUML + Visual Studio Code + PlantUML Extension
本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 為標(biāo)準(zhǔn)。
Overview
Bootstrap 作為 Tomcat 對外界的啟動類,在 $CATALINA_BASE/bin 目錄下,它通過反射創(chuàng)建 Catalina 的實例并對其進(jìn)行初始化及啟動。 Catalina 解析 $CATALINA_BASE/conf/server.xml 文件并創(chuàng)建 StandardServer、StandardService、StandardEngine、StandardHost 等 StandardServer 代表的是整個 Servlet 容器,他包含一個或多個 StandardService StandardService 包含一個或多個 Connector,和一個 Engine,Connector 和 Engine 都是在解析 conf/server.xml 文件時創(chuàng)建的,Engine 在 Tomcat 的標(biāo)準(zhǔn)實現(xiàn)是 StandardEngine MapperListener 實現(xiàn)了 LifecycleListener 和 ContainerListener 接口用于監(jiān)聽容器事件和生命周期事件。該監(jiān)聽器實例監(jiān)聽所有的容器,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,當(dāng)容器有變動時,注冊容器到 Mapper。 Mapper 維護了 URL 到容器的映射關(guān)系。當(dāng)請求到來時會根據(jù) Mapper 中的映射信息決定將請求映射到哪一個 Host、Context、Wrapper。 Http11NioProtocol 用于處理 HTTP/1.1 的請求 NioEndpoint 是連接的端點,在請求處理流程中該類是核心類,會重點介紹。 CoyoteAdapter 用于將請求從 Connctor 交給 Container 處理。使 Connctor 和 Container 解耦。 StandardEngine 代表的是 Servlet 引擎,用于處理 Connector 接受的 Request。包含一個或多個 Host(虛擬主機), Host 的標(biāo)準(zhǔn)實現(xiàn)是 StandardHost。 StandardHost 代表的是虛擬主機,用于部署該虛擬主機上的應(yīng)用程序。通常包含多個 Context (Context 在 Tomcat 中代表應(yīng)用程序)。Context 在 Tomcat 中的標(biāo)準(zhǔn)實現(xiàn)是 StandardContext。 StandardContext 代表一個獨立的應(yīng)用程序,通常包含多個 Wrapper,一個 Wrapper 容器封裝了一個 Servlet,Wrapper的標(biāo)準(zhǔn)實現(xiàn)是 StandardWrapper。 StandardPipeline 組件代表一個流水線,與 Valve(閥)結(jié)合,用于處理請求。StandardPipeline 中含有多個 Valve, 當(dāng)需要處理請求時,會逐一調(diào)用 Valve 的 invoke 方法對 Request 和 Response 進(jìn)行處理。特別的,其中有一個特殊的 Valve 叫 basicValve,每一個標(biāo)準(zhǔn)容器都有一個指定的 BasicValve,他們做的是最核心的工作。
StandardEngine 的是 StandardEngineValve,他用來將 Request 映射到指定的 Host; StandardHost 的是 StandardHostValve, 他用來將 Request 映射到指定的 Context; StandardContext 的是 StandardContextValve,它用來將 Request 映射到指定的 Wrapper; StandardWrapper 的是 StandardWrapperValve,他用來加載 Rquest 所指定的 Servlet,并調(diào)用 Servlet 的 Service 方法。
Tomcat init

當(dāng)通過 ./startup.sh 腳本或直接通過 java 命令來啟動 Bootstrap 時,Tomcat 的啟動過程就正式開始了,啟動的入口點就是 Bootstrap 類的 main 方法。 啟動的過程分為兩步,分別是 init 和 start,本節(jié)主要介紹 init; 初始化類加載器。
通過從 CatalinaProperties 類中獲取 common.loader 等屬性,獲得類加載器的掃描倉庫。CatalinaProperties 類在的靜態(tài)塊中調(diào)用了 loadProperties() 方法,從 conf/catalina.properties 文件中加載了屬性.(即在類創(chuàng)建的時候?qū)傩跃鸵呀?jīng)加載好了)。 通過 ClassLoaderFactory 創(chuàng)建 URLClassLoader 的實例
通過反射創(chuàng)建 Catalina 的實例并設(shè)置 parentClassLoader setAwait(true)。設(shè)置 Catalina 的 await 屬性為 true。在 Start 階段尾部,若該屬性為 true,Tomcat 會在 main 線程中監(jiān)聽 SHUTDOWN 命令,默認(rèn)端口是 8005.當(dāng)收到該命令后執(zhí)行 Catalina 的 stop() 方法關(guān)閉 Tomcat 服務(wù)器。 createStartDigester()。Catalina 的該方法用于創(chuàng)建一個 Digester 實例,并添加解析 conf/server.xml 的 RuleSet。Digester 原本是 Apache 的一個開源項目,專門解析 XML 文件的,但我看 Tomcat-9.0.0.M22 中直接將這些類整合到 Tomcat 內(nèi)部了,而不是引入 jar 文件。 parse() 方法就是 Digester 處理 conf/server.xml 創(chuàng)建各個組件的過程。值的一提的是這些組件都是使用反射的方式來創(chuàng)建的。特別的,在創(chuàng)建 Digester 的時候,添加了一些特別的 rule Set,用于創(chuàng)建一些十分核心的組件,這些組件在 conf/server.xml 中沒有但是其作用都比較大,這里做下簡單介紹,當(dāng) Start 時用到了再詳細(xì)說明:
EngineConfig。LifecycleListener 的實現(xiàn)類,觸發(fā) Engine 的生命周期事件后調(diào)用,這個監(jiān)聽器沒有特別大的作用,就是打印一下日志 HostConfig。LifecycleListener 的實現(xiàn)類,觸發(fā) Host 的生命周期事件后調(diào)用。這個監(jiān)聽器的作用就是部署應(yīng)用程序,這包括 conf/ / / 目錄下所有的 Context xml 文件 和 webapps 目錄下的應(yīng)用程序,不管是 war 文件還是已解壓的目錄。另外后臺進(jìn)程對應(yīng)用程序的熱部署也是由該監(jiān)聽器負(fù)責(zé)的。 ContextConfig。LifecycleListener 的實現(xiàn)類,觸發(fā) Context 的生命周期事件時調(diào)用。這個監(jiān)聽器的作用是配置應(yīng)用程序,它會讀取并合并 conf/web.xml 和 應(yīng)用程序的 web.xml,分析 /WEB-INF/classes/ 和 /WEB-INF/lib/*.jar中的 Class 文件的注解,將其中所有的 Servlet、ServletMapping、Filter、FilterMapping、Listener 都配置到 StandardContext 中,以備后期使用。當(dāng)然了 web.xml 中還有一些其他的應(yīng)用程序參數(shù),最后都會一并配置到 StandardContext 中。
reconfigureStartStopExecutor() 用于重新配置啟動和停止子容器的 Executor。默認(rèn)是 1 個線程。我們可以配置 conf/server.xml 中 Engine 的 startStopThreads,來指定用于啟動和停止子容器的線程數(shù)量,如果配置 0 的話會使用 Runtime.getRuntime().availableProcessors() 作為線程數(shù),若配置為負(fù)數(shù)的話會使用 Runtime.getRuntime().availableProcessors() + 配置值,若和小與 1 的話,使用 1 作為線程數(shù)。當(dāng)線程數(shù)是 1 時,使用 InlineExecutorService 它直接使用當(dāng)前線程來執(zhí)行啟動停止操作,否則使用 ThreadPoolExecutor 來執(zhí)行,其最大線程數(shù)為我們配置的值。 需要注意的是 Host 的 init 操作是在 Start 階段來做的, StardardHost 創(chuàng)建好后其 state 屬性的默認(rèn)值是 LifecycleState.NEW,所以在其調(diào)用 startInternal() 之前會進(jìn)行一次初始化。
Tomcat Start[Deployment]

圖中從 StandardHost Start StandardContext 的這步其實在真正的執(zhí)行流程中會直接跳過,因為 conf/server.xml 文件中并沒有配置任何的 Context,所以在 findChildren() 查找子容器時會返回空數(shù)組,所以之后遍歷子容器來啟動子容器的 for 循環(huán)就直接跳過了。 觸發(fā) Host 的 BEFORE_START_EVENT 生命周期事件,HostConfig 調(diào)用其 beforeStart() 方法創(chuàng)建 CATALINA_BASE/webapps& $CATALINA_BASE/conf/ / / 目錄。 觸發(fā) Host 的 START_EVENT 生命周期事件,HostConfig 調(diào)用其 start() 方法開始部署已在 CATALINA_BASE/webapps & $CATALINA_BASE/conf/ / / 目錄下的應(yīng)用程序。
解析 $CATALINA_BASE/conf/ / / 目錄下所有定義 Context 的 XML 文件,并添加到 StandardHost。這些 XML 文件稱為應(yīng)用程序描述符。正因為如此,我們可以配置一個虛擬路徑來保存應(yīng)用程序中用到的圖片,詳細(xì)的配置過程請參考 開發(fā)環(huán)境配置指南 – 6.3. 配置圖片存放目錄 部署 $CATALINA_BASE/webapps 下所有的 WAR 文件,并添加到 StandardHost。 部署 $CATALINA_BASE/webapps 下所有已解壓的目錄,并添加到 StandardHost。
特別的,添加到 StandardHost 時,會直接調(diào)用 StandardContext 的 start() 方法來啟動應(yīng)用程序。啟動應(yīng)用程序步驟請看 Context Start 一節(jié)。另外,MySQL 系列面試題和答案全部整理好了,微信搜索互聯(lián)網(wǎng)架構(gòu)師,在后臺發(fā)送:2T,可以在線閱讀。
在 StandardEngine 和 StandardContext 啟動時都會調(diào)用各自的 threadStart() 方法,該方法會創(chuàng)建一個新的后臺線程來處理該該容器和子容器及容器內(nèi)各組件的后臺事件。StandardEngine 會直接創(chuàng)建一個后臺線程,StandardContext 默認(rèn)是不創(chuàng)建的,和 StandardEngine 共用同一個。后臺線程處理機制是周期調(diào)用組件的 backgroundProcess() 方法。詳情請看 Background process 一節(jié)。 MapperListener
addListeners(engine) 方法會將該監(jiān)聽器添加到 StandardEngine 和它的所有子容器中 registerHost() 會注冊所有的 Host 和他們的子容器到 Mapper 中,方便后期請求處理時使用。 當(dāng)有新的應(yīng)用(StandardContext)添加進(jìn)來后,會觸發(fā) Host 的容器事件,然后通過 MapperListener 將新應(yīng)用的映射注冊到 Mapper 中。
Context Start

StandRoot 類實現(xiàn)了 WebResourceRoot 接口,它容納了一個應(yīng)用程序的所有資源,通俗的來說就是部署到 webapps 目錄下對應(yīng) Context 的目錄里的所有資源。因為我對 Tomcat 的資源管理部分暫時不是很感興趣,所以資源管理相關(guān)類只是做了簡單了解,并沒有深入研究源代碼。 resourceStart() 方法會對 StandardRoot 進(jìn)行初始配置 postWorkDirectory() 用于創(chuàng)建對應(yīng)的工作目錄 $CATALINA_BASE/work/ / / , 該目錄用于存放臨時文件。 StardardContext 只是一個容器,而 ApplicationContext 則是一個應(yīng)用程序真正的運行環(huán)境,相關(guān)類及操作會在請求處理流程看完以后進(jìn)行補充。 StardardContext 觸發(fā) CONFIGURE_START_EVENT 生命周期事件,ContextConfig 開始調(diào)用 configureStart() 對應(yīng)用程序進(jìn)行配置。
這個過程會解析并合并 conf/web.xml & conf/ / /web.xml.default & webapps/ /WEB-INF/web.xml 中的配置。 配置配置文件中的參數(shù)到 StandardContext, 其中主要的包括 Servlet、Filter、Listener。 因為從 Servlet3.0 以后是直接支持注解的,所以服務(wù)器必須能夠處理加了注解的類。Tomcat 通過分析 WEB-INF/classes/ 中的 Class 文件和 WEB-INF/lib/ 下的 jar 包將掃描到的 Servlet、Filter、Listerner 注冊到 StandardContext。 setConfigured(true),是非常關(guān)鍵的一個操作,它標(biāo)識了 Context 的成功配置,若未設(shè)置該值為 true 的話,Context 會啟動失敗。
Background process

后臺進(jìn)程的作用就是處理一下 Servlet 引擎中的周期性事件,處理周期默認(rèn)是 10s。 特別的 StandardHost 的 backgroundProcess() 方法會觸發(fā) Host 的 PERIODIC_EVENT 生命周期事件。然后 HostConfig 會調(diào)用其 check() 方法對已加載并進(jìn)行過重新部署的應(yīng)用程序進(jìn)行 reload 或?qū)π虏渴鸬膽?yīng)用程序進(jìn)行熱部署。熱部署跟之前介紹的部署步驟一致, reload() 過程只是簡單的順序調(diào)用 setPause(true)、stop()、start()、setPause(false),其中 setPause(true) 的作用是暫時停止接受請求。
How to read excellent open source projects
下面我簡單分享一下我是如何閱讀開源項目源碼的。
先找一些介紹該項目架構(gòu)的書籍來看,項目架構(gòu)是項目核心中的核心,讀架構(gòu)讀的是高層次的設(shè)計思路,讀源碼讀的是低層次的實現(xiàn)細(xì)節(jié)。有了高層次的設(shè)計思路做指導(dǎo),源碼讀起來才會得心應(yīng)手,因為讀的時候心里很清楚現(xiàn)在在讀的源碼在整個項目架構(gòu)中處于什么位置。我在讀 Tomcat 源碼之前先把 《How Tomcat works》 一書過了一邊,然后又看了一下 《Tomcat 架構(gòu)解析》 的第二章,對 Tomcat 的架構(gòu)有了初步了解。(PS:《How Tomcat works》一書是全英文的,但讀起來非常流暢,雖然它是基于 Tomcat 4 和 5 的,但 Tomcat 架構(gòu)沒有非常大的變化,新版的 Tomcat 只是增加了一些組件,如果你要學(xué)習(xí) Tomcat 的話,首推這本書!)
如果實在找不到講架構(gòu)的書,那就自己動手畫類圖吧!一般來說,開源項目都是為了提供服務(wù)的,我們把提供服務(wù)的流程作為主線來分析源代碼,這樣目的性會更強一些,將該流程中涉及到的類畫到類圖中,最后得到的類圖就是架構(gòu)!不過分析之前你要先找到流程的入口點,否則分析就無從開始。以 Tomcat 為例,他的主線流程大致可以分為 3 個:啟動、部署、請求處理。他們的入口點就是 Bootstrap 類和 接受請求的 Acceptor 類!
有了閱讀思路我們下面來說說工具吧。我使用的閱讀工具是 IntelliJ IDEA,一款十分強大的 IDE,可能比較重量級,如果你有其他更加輕量級的 Linux 平臺源碼閱讀工具,可以推薦給我~
Structure 欄目可以自定義列出類中的域、方法,然后還可以按照繼承結(jié)構(gòu)對域和方法進(jìn)行分組,這樣就可以直接看出來域和方法是在繼承結(jié)構(gòu)中哪個類里定義的。當(dāng)你點擊方法和域時,還可以自動滾動到源代碼等等。
在源代碼中 點擊右鍵 -> Diagrams -> show Diagram 可以顯示類的繼承結(jié)構(gòu),圖中包含了該類所有的祖先和所有的接口。在該圖中選擇指定的父類和接口,點擊右鍵 -> show Implementations, IDEA 會列出接口的實現(xiàn)類或該類的子類。
FindUsage、Go To Declaration 等等就不再多說了。
Reference
http://product.dangdang.com/25084132.html
https://tomcat.apache.org/tomcat-9.0-doc/index.html
http://www-eu.apache.org/dist/tomcat/tomcat-9/v9.0.0.M22/src/
正文結(jié)束
1.不認(rèn)命,從10年流水線工人,到谷歌上班的程序媛,一位湖南妹子的勵志故事
3.從零開始搭建創(chuàng)業(yè)公司后臺技術(shù)棧
5.37歲程序員被裁,120天沒找到工作,無奈去小公司,結(jié)果懵了...
一個人學(xué)習(xí)、工作很迷茫?
點擊「閱讀原文」加入我們的小圈子!

