Tomcat 通過 server.xml 配置文件裝配一系列組件,並且爲組件設計生命週期接口,在容器啓停時,協調控制組件的啓動、初始化和停止。容器通常使用腳本啓動,腳本主要是檢查 Java 環境、設置 JVM 參數,調用 Bootstrap.start 啓動。
Bootstrap 是 Catalina 的引導加載類,它構造了一個 commonLoader 類加載器,加載 ${catalina.base}/lib
目錄下的類,目的是與應用程序級的類隔離,接下來詳細分析每個過程。
在此之前可先了解一下,官網對啓動過程的描述,以及提供的啓動UML序列圖,Startup.txt&UML,這個圖囊括了啓動過程中類的調用,但我第一次看這個圖,也是一臉懵,以下描述的則是通過 DEBGUG 跟出來的,當回過頭再看這個圖,確實很有用。
初始化
初始化涉及到 server.xml 的解析,Tomcat 使用 Digester 解析,其工作原理理解了很簡單,使用 sax 解析,在元素開始和結束,藉助一個 ObjectStack 對象棧和一系列解析規則完成組件的初始化,它主要有三個基本規則:
- ObjectCreateRule:根據指定的 ClassName 創建一個實例,元素開始時,壓入對象棧,結束時,彈出對象棧
- SetPropertiesRule:元素開始時,根據其屬性,反射調用棧頂元素對應成員變量的 set 方法
- SetNextRule:元素結束時,使用set方法建立棧頂元素(child)和(top-1)元素(parent)的父子(組合)關係
各組件都是使用這三個規則進行配置解析,解析過程不再贅述,(這裏) 提供一份源碼註釋,可加斷點跟一下,着重關注棧內對象的入棧和出棧,值得注意的是在 GlobalResourcesLifecycleListener 初始化時會觸發加載解析 mbeans-descriptors.xml,這個過程太長,可使用斷點跳過。
調用 StandardServer.initialize 開始組件的初始化,觸發 INIT_EVENT 事件,詳細過程:
- 初始化 StandardServer:首先觸發各 Listener(具體有哪些,可查看xml的配置)的執行,然後初始化內部的 Services;
- Service 主要是初始化定義的 Connectors;
- 初始化 Connector:初始化 Adapter 和 ProtocolHandler,處理器有兩種不同實現:
- Http11NioProtocol 初始化:NioEndpoint 設置線程池名稱、設置ConnectionHandler、設置接收和發送 ByteBuffer 容量;SSL相關實現初始化:
- 初始化 NioEndpoint:初始化 ServerSocketChannel,設置成阻塞模式,綁定端口;設置 Acceptor、Poller 線程數目;初始化 SSL 信息;初始化 NioSelectorPool;
- Http11Protocol 初始化:JIoEndpoint 設置線程池名和ConnectionHandler,初始化 ServerSocketFactory:
- 初始化 JioEndpoint:設置 Acceptor 線程數;創建 ServerSocket 並綁定端口。
- Http11NioProtocol 初始化:NioEndpoint 設置線程池名稱、設置ConnectionHandler、設置接收和發送 ByteBuffer 容量;SSL相關實現初始化:
- 初始化完畢
以上就是在組件在init生命週期事件中完成的設置,注意在 Digester 解析過程中,也完成了一系列的設置。
啓動
調用 StandardServer.start 開啓組件的啓動過程,觸發 BEFORE_START_EVENT、START_EVENT、AFTER_START_EVENT 事件:
- 啓動 StandardServer:觸發執行各 Listener;啓動內部的 Services;
- Service 默認沒有 Listener,首先啓動 Engines 、接着啓動 Executors、最後啓動 Connectors;
- 啓動 Engine,它調用 super.start() 進行啓動或觸發以下的動作:
- 嘗試啓動 Manager、Cluster、Realm(LockOutRealm);
- 啓動子容器 Hosts;
- EngineConfig 監聽器的執行 - START_EVENT,STOP_EVENT;
- 啓動後臺線程,定期檢查會話超時。
- 啓動 Host:設置 ErrorReportValve,並添加到自己的 pipeline 中,觸發 ADD_VALVE_EVENT 容器事件,調用 super.start():
- 啓動子容器 Contexts;
- 啓動 pipeline 中實現 Lifecycle 的 Valve。
- 觸發 HostConfig 監聽器的執行:
- PERIODIC_EVENT:檢查所有Web應用程序的狀態;
- START_EVENT:創建 Context,啓動並部署 webapps & conf/Catalina/localhost/*.xml;
- STOP_EVENT:取消已部署的所有應用。
- 啓動 Context,部署 Web App
- 根據 context.xml(或默認的)使用 Digester 創建 StandardContext 對象,添加 ContextConfig 監聽器,通過 host.addChild 啓動 Context;
- 初始化用於解析 web.xml 的 Digester,創建設置 WebappLoader 且不使用"標準委託模型";
- 先解析默認的 conf/web.xml,然後處理 WEB-INF/web.xml,創建 StandardWrapper 封裝 Servlet。
- 啓動 Connector,無 Listener,它主要是啓動 ProtocolHandler:
- Http11NioProtocol - 啓動 NioEndpoint - 啓動 Poller 線程,啓動 Acceptor 線程;
- Http11Protocol - 啓動 JioEndpoint - 啓動 Acceptor 線程。
- 註冊 ShutdownHook,阻塞 main 線程,啓動完畢。
接下來就該處理請求了,這部分下一篇會介紹。
停止
當調用 Bootstrap.stop 或接收到 SHUTDOWN 指令或者捕捉到系統關閉信號(如ctrl+c,shutdown,logoff)時,開始調用 Catalina.stop 方法,關閉組件,觸發 BEFORE_STOP_EVENT、STOP_EVENT 事件:停止Server、Service,暫停 Connector,停止內部組件,停止 Connector,關閉線程池,打斷 awaitThread 即 main 線程,結束。
如果 stop 命令執行失敗,嘗試使用系統信號終止,首先使用 kill -15 ,進程收到後進行處理;如果還是失敗那麼,等待 5s 後,使用 kill -9 強制終止。
小結
當我看到 Web應用加載的時候,耐心有點不足,感覺捋順了整個過程,其實還有很多地方經不起推敲,爲了看得更透徹,那麼這個"簡單"的啓動過程,又有哪些值得思考的呢?
內部啓動的線程和線程池中的線程爲什麼設置爲守護線程(Daemon)?
Java 中有兩類線程:守護線程與用戶線程,區別是守護線程不會阻止 JVM 的退出。從 Tomcat 的停止流程來看,就算用非守護線程也不會出現什麼問題,沒有搜到官方對此的描述,這裏按照理解強行解釋一波 :)。
守護線程一般是服務提供者,運行系統代碼,比如 GC 線程,就像操作系統中也區分用戶線程和內核線程那樣,Tomcat 將內部線程設爲 daemon,也能很好的在語義上區分 Servlet 啓動的線程,並且對於 Servlet 來說 Tomcat 就是它的操作系統。
生命週期的設計有什麼好處?
保證組件啓動和停止的一致性,爲生命週期事件添加監聽器,這些監聽器處理其感興趣的事件,來做一些額外的操作。這是觀察者模式的應用。
多應用間如何實現隔離?
應用隔離的本質就是類隔離,主要防止類衝突。類是否相等是由其全限定名和類加載器共同決定的,容器就是通過自定義 ClassLoader 實現應用間隔離,Tomcat 類加載器結構:
當要求類加載器加載類時,它首先將請求委託給父加載器,然後在父加載器找不到所請求的類時,查找自己的存儲庫。而 Webapp 加載器略有不同,它首先會在自己的資源庫中搜索,而不是向上委託,打破了標準的委託機制,其類加載時按以下順序查找資源庫:
- Bootstrap 和 System 已加載的類
- /WEB-INF/classes 和 /WEB-INF/lib/*.jar
- Common 已加載的類
應用熱加載和熱部署?
當在啓動 Engine 時,會新建一個名爲 ContainerBackgroundProcessor[StandardEngine[Catalina]] 的線程,默認 10s 檢查是否要重新加載或重新部署,對應方法在 Loader 接口定義分別是 backgroundProcess 和 modified。
默認 web.xml 配置了什麼?
- 提供一個 DefaultServlet,用於處理靜態資源和未找到匹配 Servlet 的請求;
- 用於編譯和執行 JSP 的 JspServlet;
- Session 默認超時 30 minutes;
- 默認 MIME Type 映射和 Welcome Files
其他能夠想到的點
採用都是常用的數據結構,如 ArrayList、數組、HashMap等,Pipeline 採用鏈表實現,Digester 使用棧來解析。歡迎補充。