Tomcat 7 中 web 應用載入原理(一)Context 構建

預流發表於2018-02-06

為什麼關心 Tomcat 中一個 web 應用的載入過程?在前面的文章中看過多次 Tomcat 的元件結構圖,這裡再貼出來回顧一下:

Tomcat 7 中 web 應用載入原理(一)Context 構建
之前的Tomcat 7 啟動分析系列文章中看到 Tomcat 啟動的時候將會解析 server.xml,根據裡面所配置的各個節點資訊逐一初始化和啟動相應元件(即分別呼叫它們的 init 和 start 方法),但瀏覽一下 Tomcat 7 原始碼中的 server.xml 的內容,裡面對應上圖中的已經預設配置的各級元件包括 Server、Service、Engine、Connector、Host、Valve 。上圖中的 Context 元件實際就是我們通常所說的一個 web 應用,有趣的是在 server.xml 中並沒有配置該元件,而我們預設啟動的時候實際上已經有好幾個 web 應用可以訪問了: 這個到底是怎麼回事?

看過前面的Tomcat 7 的一次請求分析系列文章的人應該知道,瀏覽器一次請求送到 Tomcat 伺服器之後,最終會根據瀏覽器中的 url 路徑找到相應的實際要訪問的 web 應用的 context 物件(預設即org.apache.catalina.core.StandardContext類的例項)。比如訪問的 url 為http://localhost:8080/,那麼將會送到上圖 ROOT 資料夾表示的 web 應用中,訪問的 url 為http://localhost:8080/docs,那麼將訪問 docs 資料夾表示的 web 應用。所以能夠猜到的是在 Tomcat 啟動完成後,必定容器內部已經構造好了表示相應web應用的各個 context 物件。

本文就對這個問題一探究竟。在Tomcat 7 伺服器關閉原理的開頭提到,預設的配置下 Tomcat 啟動完之後會看到後臺實際上總共有 6 個執行緒在執行:

Tomcat 7 中 web 應用載入原理(一)Context 構建
前面的幾篇文章中涉及了 mainhttp-bio-8080-Acceptor-0http-bio-8080-AsyncTimeoutajp-bio-8009-Acceptor-0ajp-bio-8009-AsyncTimeout,已經談到了這些執行緒的作用,它們是如何產生並響應請求的。但有一個執行緒沒有說,即ContainerBackgroundProcessor[StandardEngine[Catalina]],而本文要解答的問題奧祕就在這個執行緒之中。

先看看這個執行緒是如何產生的,其實從命名就可以看出一些端倪,它叫做容器後臺處理器,並且跟 StandardEngine 關聯起來,它的產生於作用也同樣如此。

Tomcat 7 中所有的預設容器元件( StandardEngine、StandardHost、StandardContext、StandardWrapper )都會繼承父類org.apache.catalina.core.ContainerBase,在這些容器元件啟動時將會呼叫自己內部的 startInternal 方法,在該方法內部一般會呼叫父類的 startInternal 方法( StandardContext 類的實現除外),比如org.apache.catalina.core.StandardEngine類中的 startInternal 方法:

Tomcat 7 中 web 應用載入原理(一)Context 構建
最後的super.startInternal()即呼叫父類org.apache.catalina.core.ContainerBase的startInternal方法,在該方法最後:
Tomcat 7 中 web 應用載入原理(一)Context 構建
第 6 行設定了LifecycleState.STARTING狀態(這樣將向容器釋出一個Lifecycle.START_EVENT事件),這一行的作用本文後面會提到,暫且按下不表。第 9 行呼叫 threadStart 方法,看看 threadStart 方法的程式碼:
Tomcat 7 中 web 應用載入原理(一)Context 構建
這裡可以看到如果兩個前置校驗條件通過的話將會啟動一個執行緒,並且執行緒的名字即以ContainerBackgroundProcessor[開頭,執行緒名字後面取的是物件的 toString 方法,以 StandardEngine 為例,看看org.apache.catalina.core.StandardEngine的 toString 方法實現:
Tomcat 7 中 web 應用載入原理(一)Context 構建
以上解釋了這個後臺執行緒的來歷。

但這裡有一個問題,既然 StandardEngine、StandardHost 都會呼叫super.startInternal()方法,按預設配置,後臺理應產生兩個後臺執行緒,實際為什麼只有一個?

回到org.apache.catalina.core.ContainerBase的 threadStart 方法,在啟動執行緒程式碼之前有兩個校驗條件:

Tomcat 7 中 web 應用載入原理(一)Context 構建
容器元件物件初始化時 thread 為null,backgroundProcessorDelay 是-1
Tomcat 7 中 web 應用載入原理(一)Context 構建
org.apache.catalina.core.StandardEngine在其自身建構函式中做了一點修改:
Tomcat 7 中 web 應用載入原理(一)Context 構建
建構函式最後將父類的 backgroundProcessorDelay 的值由-1改成了10,所以 Tomcat 啟動解析 xml 時碰到一個 Engine 節點就會對應產生一個後臺處理執行緒。

講完了這個後臺處理執行緒的產生,看看這個執行緒所作的事情,再看下這個執行緒的啟動程式碼:

Tomcat 7 中 web 應用載入原理(一)Context 構建
所以這個執行緒將會執行 ContainerBase 的內部類 ContainerBackgroundProcessor 的 run 方法,看下 ContainerBackgroundProcessor 的全部實現程式碼:
Tomcat 7 中 web 應用載入原理(一)Context 構建
Tomcat 7 中 web 應用載入原理(一)Context 構建
在它的 run 方法暫停一段時間之後會呼叫 processChildren 方法,而 processChildren 方法做了兩件事,一是呼叫容器元件自身的 backgroundProcess 方法,而是取出該容器元件的所有子容器元件並呼叫它們的 processChildren 方法。歸結起來這個執行緒的實現就是定期通過遞迴的方式呼叫當前容器及其所有子容器的 backgroundProcess 方法。

而這個 backgroundProcess 方法在 ContainerBase 內部已經給出了實現:

Tomcat 7 中 web 應用載入原理(一)Context 構建
Tomcat 7 中 web 應用載入原理(一)Context 構建
這段程式碼就不一一解釋了,概括起來說就是逐個呼叫與容器相關其它內部元件的 backgroundProcess 方法。最後註冊一個Lifecycle.PERIODIC_EVENT事件。

上面就是 Tomcat 7 的後臺處理執行緒所作的事情的概述,在 Tomcat 的早期版本中有一些後臺處理的事情原來是在各個元件內部分別自定義一個執行緒並啟動,在 Tomcat 5 中改成了所有後臺處理共享同一執行緒的方式。

回到本文要解答的問題,web 應用如何載入到容器中的?在 ContainerBase 類的 backgroundProcess 方法的最後:

fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);  
複製程式碼

向容器註冊了一個PERIODIC_EVENT事件。前面說道預設的ContainerBackgroundProcessor[StandardEngine[Catalina]]執行緒會定期(預設為 10 秒)執行 Engine、Host、Context、Wrapper 各容器元件及與它們相關的其它元件的 backgroundProcess 方法,所以也會定期向 Host 元件釋出一個PERIODIC_EVENT事件,這裡看下 StandardHost 都會關聯的一個監聽器org.apache.catalina.startup.HostConfig

在 Tomcat 啟動解析 xml 時org.apache.catalina.startup.Catalina類的 386 行:digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"))

在 HostRuleSet 類的 addRuleInstances 方法中:

Tomcat 7 中 web 應用載入原理(一)Context 構建

第 9 到 12 行看到,所有 Host 節點都會新增一個org.apache.catalina.startup.HostConfig物件作為org.apache.catalina.core.StandardHost物件的監聽器

在 HostConfig 的 lifecycleEvent 方法中可以看到如果 Host 元件收到了 Lifecycle.PERIODIC_EVENT 事件的釋出所作出的響應(如果對 Tomcat 7 的 Lifecycle 機制不清楚可以看下Tomcat 7 啟動分析(五)Lifecycle 機制和實現原理

Tomcat 7 中 web 應用載入原理(一)Context 構建
第 17 行,如果釋出的事件是PERIODIC_EVENT將會執行 check 方法。第 19 行,如果釋出的事件是START_EVENT則執行 start 方法。check 方法和 start 方法最後都會呼叫 deployApps() 方法,看下這方法的實現:
Tomcat 7 中 web 應用載入原理(一)Context 構建
這裡即各種不同方式釋出 web 應用的程式碼。

本文前面提到預設情況下元件啟動的時候會釋出一個Lifecycle.START_EVENT事件(在org.apache.catalina.core.ContainerBase類的 startInternal 方法倒數第二行),回到 HostConfig 的 lifecycleEvent 方法中,所以預設啟動時將會執行 HostConfig 的 start 方法,在該方法的最後:

if (host.getDeployOnStartup())  
    deployApps();
複製程式碼

因為預設配置 host.getDeployOnStartup() 返回 true ,這樣容器就會在啟動的時候直接載入相應的 web 應用。

當然,如果在 server.xml 中 Host 節點的 deployOnStartup 屬性設定為 false ,則容器啟動時不會載入應用,啟動完之後不能立即提供 web 應用的服務。但因為有上面提到的後臺處理執行緒在執行,會定期執行 HostConfig 的 check 方法:

Tomcat 7 中 web 應用載入原理(一)Context 構建
如果 Host 節點的 autoDeploy 屬性是 true(預設設定即為 true ),可以看到 check 方法最後同樣會載入 web 應用。

相關文章