一萬字深度剖析Tomcat源碼

整篇文章分為兩大部分平道,Tomcat 系統(tǒng)架構(gòu)設(shè)計和 Tomcat 源碼剖析。

Tomcat系統(tǒng)架構(gòu)設(shè)計

1.前言

很多人談到架構(gòu)感覺是一個非常高大尚的東西灼舍,覺得自己目前不太可能接觸到或者沒有實力接觸和學(xué)習(xí)它吼和。這其實是一個非常錯誤的認(rèn)識,事實上我們作為開發(fā)人員每天都在和架構(gòu)打交道骑素。比如當(dāng)你接到一個功能模塊的需求時炫乓,你首先要做的就是分析和設(shè)計,例如技術(shù)選型献丑、功能拆分末捣、設(shè)計合理的開發(fā)流程、類結(jié)構(gòu)關(guān)系梳理创橄、是否需要應(yīng)用設(shè)計模式等等塔粒。

所以其實架構(gòu)的本質(zhì)就是通過合理的內(nèi)部編排,保證整個系統(tǒng)高度有序筐摘、能夠不斷擴(kuò)展卒茬,滿足業(yè)務(wù)和技術(shù)的不斷變化。

架構(gòu)是屬于設(shè)計層面咖熟,而源碼則是對設(shè)計的實現(xiàn)圃酵,在源碼分析前如果能夠?qū)φw框架有一個全面的認(rèn)知,那么后續(xù)解讀源碼將會事半功倍馍管。當(dāng)然了由于 Tomcat 代碼不像 Spring 那么龐大郭赐,因此我們是有這個實力去分析 Tomcat 的整體架構(gòu)設(shè)計的,并且它的架構(gòu)設(shè)計比較獨特确沸,屬于俄羅斯套娃式架構(gòu)設(shè)計捌锭,這種設(shè)計方式也非常值的我們?nèi)ヌ剿骱蛯W(xué)習(xí)它俘陷。

接下來我們以 Tomcat 的設(shè)計者身份來揭開 Tomcat 架構(gòu)設(shè)計的神秘面紗。

2.Tomcat 功能分析

既然要設(shè)計一個系統(tǒng)观谦,我們自然是先要了解需求拉盾。也就是我們希望 Tomcat 能干什么?

HTTP 服務(wù)器

首先想到的肯定是希望 Tomcat 能夠接受并且處理 http 請求豁状,也就是作為一個 HTTP 服務(wù)器捉偏。
http 請求意味著使用的是 HTTP 協(xié)議進(jìn)行數(shù)據(jù)傳送,HTTP 協(xié)議是應(yīng)用層協(xié)議泻红,其本質(zhì)就是一種瀏覽器與服務(wù)器之間約定好的通信格式夭禽。
那么瀏覽器和服務(wù)器針對一次 http 請求的處理過程大致是怎樣的呢?如下圖所示

從圖中你可以看到整個處理過程分為了11步:

  1. 用戶通過瀏覽器進(jìn)行了一個操作谊路,這個操作可以是輸入url地址并回車讹躯,或者是點擊超鏈接,或者是在搜索框中輸入關(guān)鍵字進(jìn)行搜索缠劝,接著瀏覽器就捕獲到了這個事件
  2. 由于 HTTP 協(xié)議底層具體的數(shù)據(jù)傳輸使用的是 TCP/IP 協(xié)議蜀撑,因此瀏覽器需要向服務(wù)端發(fā)出 TCP 連接請求
  3. 服務(wù)器接受瀏覽器的連接請求,并經(jīng)過 TCP 三次握手建立連接
  4. 瀏覽器將請求數(shù)據(jù)打包成一個 HTTP 協(xié)議格式的數(shù)據(jù)包
  5. 瀏覽器將打包好的數(shù)據(jù)包推入網(wǎng)絡(luò)剩彬,經(jīng)過網(wǎng)絡(luò)傳輸最終到達(dá)服務(wù)器指定程序
  6. 服務(wù)端程序拿到數(shù)據(jù)包后酷麦,根據(jù) HTTP 協(xié)議格式進(jìn)行解包,獲取到客戶端的意圖
  7. 得知客戶端意圖后進(jìn)行處理喉恋,比如提供靜態(tài)文件或者調(diào)用服務(wù)端程序獲得動態(tài)結(jié)果
  8. 服務(wù)器將響應(yīng)結(jié)果按照 HTTP 協(xié)議格式打包
  9. 服務(wù)器將響應(yīng)數(shù)據(jù)包推入網(wǎng)絡(luò)沃饶,數(shù)據(jù)包經(jīng)過網(wǎng)絡(luò)傳輸最終達(dá)到到瀏覽器
  10. 瀏覽器拿到數(shù)據(jù)包后,按照 HTTP 協(xié)議的格式解包轻黑,然后對數(shù)據(jù)進(jìn)行解析
  11. 瀏覽器將解析后的靜態(tài)數(shù)據(jù)(如html糊肤、圖片)展示給用戶

那么在這里 Tomcat 作為一個 HTTP 服務(wù)器,主要需要完成的功能是接受連接氓鄙、解析請求數(shù)據(jù)馆揉、處理請求和發(fā)送響應(yīng)這幾個步驟。

我們重點關(guān)注處理請求部分抖拦,當(dāng)我們使用瀏覽器向某一個網(wǎng)站發(fā)起一個 HTTP 格式的請求升酣,那么作為 HTTP 服務(wù)器接收到這個請求之后,會調(diào)用具體的程序(Java類)進(jìn)行處理态罪,往往不同的請求由不同的 Java 類完成處理噩茄。那么問題來了,HTTP 服務(wù)器怎么知道要調(diào)用哪個 Java 類的哪個方法呢复颈。當(dāng)然我們可以在 HTTP 服務(wù)器代碼里直接進(jìn)行處理绩聘,也就是根據(jù)請求路徑等信息進(jìn)行一堆的 if else 邏輯判斷,但是這樣做缺陷很明顯,因為 HTTP 服務(wù)器的代碼跟后端業(yè)務(wù)處理邏輯耦合在一起了凿菩,不符合我們軟件開發(fā)設(shè)計的原則机杜,并且也非常不利于系統(tǒng)的擴(kuò)展性。

Servlet 容器

針對這種耦合問題最好的解決辦法自然是面向接口編程衅谷,于是我們可以定義一個 Servlet 接口椒拗,所有的業(yè)務(wù)類都必須實現(xiàn)這個接口。到這里就完了嗎会喝?當(dāng)然沒有,因為我們還是沒能解決 Servlet 定位問題玩郊,即一個請求到來時 HTTP 服務(wù)器如何知道該由哪個 Servlet 來處理呢肢执,并且這些自定義的 Servlet 也需要進(jìn)行加載和管理。于是 Servlet 容器就被發(fā)明出來了译红,Servlet 容器作為 HTTP 服務(wù)器和具體業(yè)務(wù)類進(jìn)行交互的橋梁预茄,HTTP 服務(wù)器將請求交由 Servlet 容器去處理,而 Servlet 容器則負(fù)責(zé)將請求轉(zhuǎn)發(fā)到具體的 Servlet侦厚,并且調(diào)用 Servlet 的方法進(jìn)行業(yè)務(wù)處理耻陕,它們之間的調(diào)用通過 Servlet 接口進(jìn)行解耦。

其實 Servlet 接口和 Servlet 容器并不是 Tomcat 發(fā)明的刨沦,而是在 JAVAEE API 中定義的诗宣,我們也把這一整套內(nèi)容叫做 Servlet 規(guī)范。有了這套規(guī)范之后想诅,如果我們要實現(xiàn)新的業(yè)務(wù)功能召庞,只需要實現(xiàn)一個 Servlet,并把它注冊到 Servlet 容器中来破,剩下的事情就需要由 Tomcat 幫我們處理了篮灼。因此對于 Tomcat 而言就需要按照 Serlvet 規(guī)范的要求去實現(xiàn)一個 Servlet 容器。

Tomcat Servlet 容器工作流程

當(dāng)用戶請求某個URL資源時:

  1. HTTP 服務(wù)器會把請求信息使用 ServletRequest 對象封裝起來
  2. 進(jìn)一步去調(diào)用 Servlet 容器中某個具體的 Servlet
  3. 在第二步中當(dāng) Servlet 容器拿到請求后徘禁,會根據(jù) URL 和 Servlet 的映射關(guān)系诅诱,找到相應(yīng)的 Servlet
  4. 如果 Servlet 還沒有被加載,就使用反射機(jī)制創(chuàng)建這個 Servlet送朱,并調(diào)用 Servlet 的 init 方法來完成初始化
  5. 接著調(diào)用這個具體 Servlet 的 service 方法來處理請求娘荡,請求處理結(jié)果使用 ServletResponse 對象封裝
  6. 把 ServletResponse 對象返回給 HTTP 服務(wù)器,HTTP 服務(wù)器會把響應(yīng)發(fā)送給客戶端
Web 服務(wù)器

根據(jù)上述分析驶沼,我們知道了 Tomcat 要實現(xiàn)成 “HTTP 服務(wù)器 + Servlet 容器”它改,也就是所謂的 Web 服務(wù)器。
而作為一個 Web 服務(wù)器商乎,Tomcat 要實現(xiàn)兩個非常核心的功能:

  • Http 服務(wù)器功能:進(jìn)行 Socket 通信(基于 TCP/IP)央拖,解析 HTTP 報文
  • Servlet 容器功能:加載和管理 Servlet,由 Servlet 具體負(fù)責(zé)處理 Request 請求

3.Tomcat組件設(shè)計

連接器和容器

為了完成上述兩個功能,我們設(shè)計了兩個核心組件連接器(Connector)和容器(Container)來分別做這兩件事情鲜戒。連接器負(fù)責(zé)對外交流(完成 Http 服務(wù)器功能)专控,容器負(fù)責(zé)內(nèi)部處理(完成 Servlet 容器功能)。

連接器既然負(fù)責(zé)對外交流那就免不了進(jìn)行 socket 通信遏餐,說到 socket 通信伦腐,就涉及到了網(wǎng)絡(luò)IO模型,那么網(wǎng)絡(luò)IO模型是可變的失都、多種多樣的柏蘑,因此一個容器可能對接多個連接器,這就好比一個房間有多個門粹庞。但是單獨的連接器或者容器都不能對外提供服務(wù)咳焚,需要把它們組裝起來才能工作,組裝后這個整體我們將其命名為 Service 組件庞溜。

Service 設(shè)計一個還是多個革半?考慮一下這種場景,當(dāng)我們有2個或以上網(wǎng)站都需要能夠部署和管理自己的應(yīng)用并且彼此不會相互影響(端口隔離)流码,而此時又不想安裝多個tomcat避免造成資源的浪費又官,那么就需要多個 Service 了。因此我們在一個 Tomcat 中讓它可以配置多個 Service漫试,這種設(shè)計也是出于系統(tǒng)的靈活性考慮六敬,雖然現(xiàn)在大多數(shù)情況下我們不會用到。

至此我們畫出了如下的架構(gòu)圖:

對圖中的組件分別進(jìn)行說明:

  • Server
    這里的 Server 就代表了一個 Tomcat 實例驾荣,包含了 Servlet 容器以及其他組件觉阅,負(fù)責(zé)組裝并啟動 Servlet 引擎、Tomcat 連接器
  • Service
    服務(wù)是 Server 內(nèi)部的組件秘车,一個Server包括多個Service典勇。它將若干個 Connector 組件綁定到一個 Container
  • Container
    容器,負(fù)責(zé)處理用戶的 servlet 請求叮趴,并返回對象給 web 用戶的模塊

這里還需注意的是連接器和容器兩者之間是通過標(biāo)準(zhǔn)的 ServletRequest 和 ServletResponse 通信割笙,這樣連接器對 Servlet 容器就屏蔽了網(wǎng)絡(luò)協(xié)議以及 I/O 模型等的區(qū)別。
有了總體架構(gòu)后眯亦,我們就進(jìn)一步看看連接器和容器分別應(yīng)該怎么設(shè)計伤溉。

連接器的設(shè)計

首先我們分析出連接器主要需要完成以下三個核心功能:

  • socket 通信,也就是網(wǎng)絡(luò)編程
  • 解析處理應(yīng)用層協(xié)議妻率,封裝成一個 Request 對象
  • 將 Request 轉(zhuǎn)換為 ServletRequest乱顾,將 Response 轉(zhuǎn)換為 ServletResponse

我們設(shè)計了三個組件 EndPoint、Processor宫静、Adapter 來對應(yīng)完成上述三項功能走净。這三個組件之間通過抽象接口進(jìn)行交互券时。從一個請求的正向流程來看, Endpoint 負(fù)責(zé)提供請求字節(jié)流給 Processor伏伯,Processor 負(fù)責(zé)提供 Tomcat 定義的 Request 對象給 Adapter橘洞,Adapter 負(fù)責(zé)提供標(biāo)準(zhǔn)的 ServletRequest 對象給 Servlet 容器。

首先來說一下 Adapter 組件说搅,連接器需要對接的是標(biāo)準(zhǔn)的 Servlet 容器炸枣,既然是 Servlet 容器,那就應(yīng)該遵循 Servlet 規(guī)范弄唧,也就是說在 Servlet 的 service方法中只能接收標(biāo)準(zhǔn)的 ServletRequest 對象和 ServletResponse對象适肠,而連接器負(fù)責(zé)對外交流,只能將基礎(chǔ)的請求信息封裝成一個 Request 對象候引,這時候我們就需要一個轉(zhuǎn)換器侯养,遇到這種需求我們通常會采用適配器模式。因此我們設(shè)計了一個 CoyoteAdapter 類背伴,并提供一個 service 方法供連接器調(diào)用沸毁,內(nèi)部則調(diào)用容器的 service 方法峰髓。

然后是 EndPoint 組件 和 Processor 組件傻寂,這兩者一個負(fù)責(zé)對接 I/O 模型,一個負(fù)責(zé)對接應(yīng)用層協(xié)議携兵,都是會變化的疾掰,并且可以自由組合。因此在這里我們規(guī)定一下 Tomcat 能夠支持的 I/O 模型和應(yīng)用層協(xié)議徐紧,如下圖所示静檬。

針對這樣一個組合的場景我們可以這么來設(shè)計,首先這兩者其實是可以作為一個整體的并级,最終目的就是將請求信息轉(zhuǎn)為一個統(tǒng)一的 Request 對象拂檩,當(dāng)然了還要負(fù)責(zé)響應(yīng)信息的輸出。因此我們設(shè)計一個 ProtocolHandler 的接口來封裝這兩種變化點嘲碧。

除了這些變化點稻励,這兩個組件也存在一些相對穩(wěn)定的部分或者是一些通用的處理邏輯,這些穩(wěn)定的部分我們通常使用抽象類來封裝愈涩,我們可以定義一個抽象基類 AbstractProtocol 讓它實現(xiàn) ProtocolHandler 接口望抽,然后針對每一種應(yīng)用層協(xié)議也定義一個自己的抽象類,它們繼承自 AbstractProtocol 抽象基類履婉,擴(kuò)展了具體協(xié)議相關(guān)的內(nèi)容煤篙。

最后我們針對具體的應(yīng)用層協(xié)議和 I/O 模型組合定義具體的實現(xiàn)類,例如:Http11NioProtocol 就是對 HTTP/1.1 協(xié)議 和 NIO 模型的實現(xiàn)毁腿。它們的類關(guān)系圖如下:

在這里可能大家會有一個疑問辑奈,不是說 ProtocolHandler 是對 EndPoint 組件 和 Processor 組件的封裝嗎苛茂?為什么從源碼中完全看不出來,很好的一個問題身害,有關(guān)于 EndPoint 組件和 Processor 組件的設(shè)計細(xì)節(jié)以及它們的交互過程我們會在源碼部分給出答案味悄。
連接器組件總體設(shè)計如下:

容器的設(shè)計

容器部分我們設(shè)計了4種容器,分別是Engine塌鸯、Host侍瑟、Context、Wrapper丙猬。這四種容器是父子關(guān)系涨颜,整體形成一個分層結(jié)構(gòu),如下圖所示茧球。

首先說明一下這四種容器的作用:

  • Engine
    表示整個 Catalina 的 Servlet 引擎庭瑰,用來管理多個虛擬站點,一個 Service 最多只能有一個 Engine抢埋,但是一個引擎可包含多個 Host
  • Host
    代表一個虛擬主機(jī)弹灭,或者說一個站點,可以給 Tomcat 配置多個虛擬主機(jī)地址揪垄,而一個虛擬主機(jī)下可包含多個 Context
  • Context
    表示一個 Web 應(yīng)用程序穷吮,一個Web應(yīng)用可包含多個 Wrapper
  • Wrapper
    表示一個Servlet,負(fù)責(zé)管理整個 Servlet 的生命周期饥努,包括裝載捡鱼、初始化、資源回收等

通過這種分層的架構(gòu)設(shè)計酷愧,使得 Servlet 容器具有很好的靈活性驾诈,同時功能也更加強(qiáng)大。

4.Tomcat架構(gòu)匯總

至此 Tomcat 的核心組件就分析的差不多了溶浴,可以看出為了實現(xiàn)兩項功能乍迄,Tomcat 進(jìn)行了很多的封裝設(shè)計,封裝出了很多的組件士败,而這些組件之間呈現(xiàn)出了明顯的層級關(guān)系闯两,一層套著一層,這就是經(jīng)典的套娃式架構(gòu)設(shè)計拱烁。

配置文件與 Catalina 組件

其實這些組件的設(shè)計更多是為了使用者能夠靈活的進(jìn)行 web 項目部署配置生蚁,因此我們將其抽取成一個配置文件,名為 server.xml戏自,如下圖所示邦投,在配置文件中你也能很清晰的對應(yīng)上這些層級關(guān)系。

在這里需要做一個補充:不知道你在前面有沒有注意到在講解 Engine 組件時提到了 Catalina擅笔。其實 Catalina 也是 Tomcat 中的一個組件志衣,它負(fù)責(zé)的是解析 Tomcat 的配置文件(server.xml)旷偿,以此來創(chuàng)建服務(wù)器 Server 組件并進(jìn)行管理铝噩。

因此也可以認(rèn)為整個 Tomcat 就是一個 Catalina 實例,Tomcat 啟動的時候會初始化這個實例,Catalina 實例通過加載server.xml 完成其他實例的創(chuàng)建沸柔,創(chuàng)建并管理一個 Server倾哺,Server 創(chuàng)建并管理多個服務(wù)璃搜, 每個服務(wù)又可以有多個Connector 和一個 Container铸史。

套娃式架構(gòu)設(shè)計的好處:

  • 一層套一層的方式,組件關(guān)系清晰明了假勿,也便于后期組件生命周期管理
  • 與 xml 這種層級的數(shù)據(jù)格式非常吻合借嗽,整體的配置也非常靈活
  • 便于子組件繼承父組件的一些配置
Tomcat 模塊分層結(jié)構(gòu)

Tomcat 是一個由一系列可配置的組件構(gòu)成的 Web 容器,在實現(xiàn)時根據(jù)不同的功能 Tomcat 內(nèi)部進(jìn)行了模塊分層转培,其中 Catalina 模塊作為 Tomcat 的 servlet 容器實現(xiàn)恶导,它是 Tomcat 的核心模塊。因為從另一個角度來說浸须,Tomcat 本質(zhì)上就是一款 Servlet 容器惨寿。而其他模塊的設(shè)計都是為 Catalina 提供支撐的。
相關(guān)模塊的功能說明如下:

整體模塊分層結(jié)構(gòu)圖如下:

Tomcat總體架構(gòu)

Tomcat 核心組件架構(gòu)圖如下所示:

這里有部分組件并沒有分析到删窒,我們對其進(jìn)行簡單的說明:

  • Listener 組件
    可以在 Tomcat 生命周期中完成某些容器相關(guān)的監(jiān)聽器
  • JNDI
    JNDI是 Java 命名與目錄接口裂垦,是屬于 J2EE 規(guī)范的,Tomcat 對其進(jìn)行了實現(xiàn)易稠。JNDI 在 J2EE 中的角色就是“交換機(jī)”缸废,即 J2EE 組件在運行時間接地查找其他組件包蓝、資源或服務(wù)的通用機(jī)制(你可以簡單理解為給資源取個名字驶社,再根據(jù)名字來找資源)
  • Cluster 組件
    提供了集群功能,可以將對應(yīng)容器需要共享的數(shù)據(jù)同步到集群中的其他 Tomcat 實例中
  • Realm 組件
    提供了容器級別的用戶-密碼-權(quán)限的數(shù)據(jù)對象测萎,配合資源認(rèn)證模塊使用
  • Loader 組件
    Web 應(yīng)用加載器亡电,用于加載 Web 應(yīng)用的資源,它要保證不同 Web 應(yīng)用之間的資源隔離
  • Manager 組件
    Servlet 映射器硅瞧,它屬于 Context 內(nèi)部的路由映射器份乒,只負(fù)責(zé)該 Context 容器的路由導(dǎo)航

Tomcat源碼剖析

1.Tomcat 源碼構(gòu)建

下載源碼(8.5.50)

網(wǎng)址:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.50/src/

如果需要下載其他版本的源碼可以在tomcat官網(wǎng):http://tomcat.apache.org/ 點擊左側(cè)相應(yīng)的大版本,然后在右側(cè)點擊 Archives (存檔)找到所有的子版本源碼腕唧。

源碼導(dǎo)入IDE之前準(zhǔn)備工作
  • 解壓源碼包或辖,進(jìn)入 apache-tomcat-8.5.50-src 目錄
  • 在當(dāng)前目錄中創(chuàng)建 source 文件夾,然后將 conf枣接、webapps 目錄移動到 source 文件夾中
  • 在當(dāng)前目錄下創(chuàng)建 pom.xml颂暇,文件內(nèi)容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>apache-tomcat-8.5.50-src</artifactId>
    <name>Tomcat8.5</name>
    <version>8.5</version>
    <build>
        <!--指定源目錄-->
        <finalName>Tomcat8.5</finalName>
        <sourceDirectory>java</sourceDirectory>
        <resources>
            <resource>
                <directory>java</directory>
            </resource>
        </resources>
        <plugins>
            <!--引入編譯插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <!--tomcat依賴的基礎(chǔ)包-->
    <dependencies>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>javax.xml</groupId>
            <artifactId>jaxrpc</artifactId>
            <version>1.1</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.5.1</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.soap</groupId>
            <artifactId>javax.xml.soap-api</artifactId>
            <version>1.4.0</version>
        </dependency>
    </dependencies>
</project>
導(dǎo)入源碼工程到IDE并進(jìn)行配置

打開IDEA,新建一個空的工程如下圖所示但惶,命名為tomcatsource耳鸯。

然后選擇【File】—>【New】—>【Module from Existing Sources】湿蛔,選擇我們解壓好的tomcat源碼目錄。
下一步選擇Maven县爬,點擊Finish即可阳啥。

項目構(gòu)建完成后,在當(dāng)前項目中搜索 Bootstrap 類财喳,找到 main 方法察迟,然后運行,這時候發(fā)現(xiàn)控制臺報了一些錯耳高。

我們雙擊錯誤定位到錯誤具體位置卷拘,根據(jù)提示進(jìn)行修復(fù),如下圖所示祝高。

然后給 tomcat 的源碼程序啟動類 Bootstrap 配置VM參數(shù)(注意路徑需要修改為自己的項目位置)栗弟,因為 tomcat 源碼運行也需要加載配置文件等。

-Dcatalina.home=D:/IdeaProjects/apache-tomcat-8.5.50-src/source
-Dcatalina.base=D:/IdeaProjects/apache-tomcat-8.5.50-src/source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=D:/IdeaProjects/apache-tomcat-8.5.50-src/source/conf/logging.properties

打開運行設(shè)置界面工闺,在如下位置添加

最后還需要在 ContextConfig 類中的 configureStart 方法中增加一行代碼將 Jsp 引擎初始化乍赫,如下

重啟 tomcat,看到如下界面就說明訪問正常了陆蟆,至此 Tomcat 源碼構(gòu)建完畢雷厂。

2.Tomcat 啟動流程分析

啟動入口

首先我們需要定位啟動入口在哪里。由于 Tomcat 是 Java 編寫的叠殷,因此啟動方法肯定是在某個類的 main 方法中改鲫,我們第一步就先來找到這個類。

通常啟動 tomcat 都是直接雙擊運行 startup.sh 或者 startup.bat 腳本林束,以 startup.sh 腳本為例像棘,文件核心內(nèi)容如下:

可以看出在該腳本中執(zhí)行了 catalina.sh,我們打開 catalina.sh 文件壶冒,找到核心內(nèi)容如下:

在 catalina.sh 中缕题,執(zhí)行了 Bootstrap 類,當(dāng)然在前面附加了很多參數(shù)比如 JVM 相關(guān)參數(shù)等胖腾。
接下來就來到了 Bootstrap 類的 main 方法烟零,也就是 tomcat 的啟動入口處。

逐級初始化

首先在 main 方法中調(diào)用了自身的 init 方法

進(jìn)入 init 方法咸作,主要邏輯如下:

然后回到 main 方法锨阿,往下調(diào)用了自身的 load 方法和 start 方法,我們先來看 load 方法的執(zhí)行過程记罚。

在 load 方法中實際是使用反射調(diào)用了 Catalina.load() 方法

進(jìn)入 Catalina 類的 load 方法墅诡,里面代碼有點多,前面其實是讀取和解析配置文件的過程毫胜,我們后面再來分析书斜,直接來到方法的末尾處诬辈,在這里 getServer() 返回的是一個 StandardServer 對象,分別對該對象設(shè)置了Catalina 對象荐吉、catalinaHome 和 catalinaBase 屬性焙糟,catalinahome 是咱們 tomcat 的安裝目錄,catalinabase 是工作目錄样屠,其實這兩個都是我們在構(gòu)建源碼時配置的 tomcat 源碼下的 source 目錄穿撮,最后關(guān)鍵的一步是調(diào)用了 server 的 init 方法,即 server 組件的初始化痪欲。

進(jìn)入 init 方法悦穿,實際調(diào)用的是 LifecycleBase 的 init 方法,該類是一個抽象基類业踢,實現(xiàn)了 Lifecycle 接口栗柒,方法主要邏輯如下:

下一步進(jìn)入到了 LifecycleBase 的子類,也就是 StandardServer 的 initInternal() 方法知举,該方法除了對自身的一些初始化操作之外瞬沦,最后進(jìn)行了 service 組件的初始化。

當(dāng)我們進(jìn)入 service 的 init 方法發(fā)現(xiàn)又來到了 LifecycleBase 的 init 方法雇锡,因此我們直接跳過逛钻,下一步來到 StandardService 的 initInternal() 方法,該方法的重點依然是對各個子組件的初始化锰提,其實到這你會發(fā)現(xiàn)這些組件都有一個共性曙痘,就是都繼承了 LifecycleBase 抽象類并實現(xiàn)了其中的抽象方法,因此我們不在一一 debug了立肘,最后看一個 connector 組件的初始化边坤。

來到 Connector 類的 initInternal() 方法,該方法執(zhí)行了 protocolHandler 組件的初始化赛不。

下一步來到了 AbstractHttp11Protocol 類的 init 方法惩嘉,該方法又調(diào)用了父類的初始化方法罢洲。

進(jìn)入父類即 AbstractProtocol 的 init 方法踢故,該方法最后完成了 endpoint 組件的初始化。

看一下 endpoint 的初始化過程惹苗,進(jìn)入 AbstractEndpoint 的 init 方法殿较,核心邏輯是這個 bind 方法。

進(jìn)入 NioEndpoint 的 bind 方法桩蓉,最后其實是使用了 java nio 的 API 創(chuàng)建了相關(guān)的對象及對象初始化工作淋纲。

到此各個組件的初始化過程就分析完畢了。

逐級啟動

回到 Bootstrap 類的 main 方法院究,分析一下 start 方法的執(zhí)行流程洽瞬。該方法最終也是調(diào)用的 Catalina 類 的 start 方法本涕。

進(jìn)入 Catalina 的 start 方法,該方法調(diào)用了 server 的 start 方法伙窃,即啟動 server 組件菩颖。

首先還是來到 LifecycleBase 的 start 方法,設(shè)置組件的狀態(tài)为障,然后調(diào)用子類的 startInternal 方法晦闰。

來到 StandardServer 的 startInternal() 方法,該方法最后啟動了 service 組件鳍怨。

相信分析到這后面的步驟大家都能知道呻右,和 load 方法的執(zhí)行流程一樣,也是一個逐級啟動的過程鞋喇,我們最后
看一下 endpoint 組件啟動時完成的工作声滥。來到 NioEndpoint 的 startInternal 方法。主要邏輯如下:

其中 Acceptor 線程是負(fù)責(zé)服務(wù)監(jiān)聽端口的請求接入的侦香,我們可以看下在 NioEndPoint 中的內(nèi)部類 Acceptor 的 run 方法醒串。

Tomcat 啟動流程總結(jié)

我們用一張時序圖來描述上述的調(diào)用過程,整體分為兩大部分鄙皇,組件的逐級初始化和逐級啟動芜赌。

3.優(yōu)雅的 Lifecycle 接口

通過對 tomcat 啟動流程的分析,你會發(fā)現(xiàn)這些組件都有 init 方法和 start方法伴逸,而這兩個方法其實都是在 Lifecycle 接口中定義的缠沈。那么Lifecycle 接口是怎么被設(shè)計出來的?

生命周期機(jī)制

設(shè)計其實就是要找到系統(tǒng)的變化點和不變點错蝴,然后遵循設(shè)計原則洲愤,選擇合適的設(shè)計模式進(jìn)行具體的設(shè)計。

對于這些組件來說不變點就是每個組件的生命周期是一致的顷锰,即它們都要經(jīng)歷創(chuàng)建柬赐、初始化、啟動官紫、停止和銷毀這幾個過程肛宋,在這個過程中組件的狀態(tài)和狀態(tài)之間的轉(zhuǎn)化也是不變的。而其中的變化點則是某個具體的組件在執(zhí)行某個過程時是有所差異的束世。

因此針對這些組件的生命周期酝陈,我們抽取出一個接口,這個接口就是 Lifecycle毁涉。在該接口中沉帮,我們需要定義這么幾個方法:init、start、stop 和 destroy穆壕,讓每個組件去實現(xiàn)這些方法待牵。

在 tomcat 中組件是有層級關(guān)系的,因此父組件需要在自己的生命周期方法比如 init 方法里創(chuàng)建子組件并且調(diào)用子組件的 init 方法喇勋,最終像一個鏈條一樣層層調(diào)用下去洲敢,這其實就是設(shè)計模式中組合模式的經(jīng)典實用。最終實現(xiàn)的效果就是在啟動入口我們只需要調(diào)用最頂層組件的 init 方法 和 start 方法茄蚯,整個 tomcat 就被啟動起來了压彭。

事件監(jiān)聽機(jī)制

tomcat 作為一個框架,尤其是作為一個 Web 容器框架渗常,監(jiān)聽機(jī)制和過濾機(jī)制是我們設(shè)計時必須要考慮的一個點壮不,也就是我們通常說的監(jiān)聽器和過濾器。由于在這里并沒有涉及到請求的處理皱碘,因此我們只需要考慮監(jiān)聽器的設(shè)計询一。

在啟動階段各個組件會涉及到生命周期方法的組合調(diào)用,因此我們可以把組件的生命周期定義成相應(yīng)的狀態(tài)癌椿,把狀態(tài)的轉(zhuǎn)變看作是一個事件健蕊。有了事件就需要有監(jiān)聽器,在監(jiān)聽器里可以實現(xiàn)一些內(nèi)部邏輯踢俄,并且監(jiān)聽器也可以方便的添加和刪除缩功,這就是典型的觀察者模式的應(yīng)用。

對于組件的狀態(tài)我們可以定義一個枚舉類 LifecycleState 來表示都办,針對監(jiān)聽器的添加和刪除我們在 Lifecycle 接口中新增兩個方法嫡锌。類圖關(guān)系如下:

抽象基類設(shè)計

在設(shè)計了 Lifecycle 接口之后,我們通常需要做這么一個考慮琳钉,在接口的這些眾多方法中势木,不同實現(xiàn)類實現(xiàn)它們是否包含一些通用的邏輯或者流程,如果有我們就需要定義一個抽象基類來實現(xiàn)這部分共同的邏輯歌懒,而其中有差異的地方我們將其抽取成抽象方法啦桌,供子類實現(xiàn)。

因此我們可以定義一個抽象基類 LifecycleBase 讓它實現(xiàn) Lifecycle 接口并重寫所有的方法及皂,針對方法中的通用邏輯我們可以定義私有的方法自行實現(xiàn)甫男,例如狀態(tài)的轉(zhuǎn)換和事件監(jiān)聽的處理我們可以定義了 setStateInternal 方法,而非通用邏輯定義相應(yīng)的抽象方法交給具體子類去實現(xiàn)躲庄,例如子類的初始化邏輯定義抽象方法 initInternal查剖,這就是典型的模板設(shè)計模式的使用,其中的抽象方法也稱為模版方法噪窘。具體類圖關(guān)系如下:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子倔监,更是在濱河造成了極大的恐慌直砂,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浩习,死亡現(xiàn)場離奇詭異静暂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谱秽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門洽蛀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人疟赊,你說我怎么就攤上這事郊供。” “怎么了近哟?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵驮审,是天一觀的道長。 經(jīng)常有香客問我吉执,道長疯淫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任戳玫,我火速辦了婚禮熙掺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘咕宿。我一直安慰自己适掰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布荠列。 她就那樣靜靜地躺著类浪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肌似。 梳的紋絲不亂的頭發(fā)上费就,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音川队,去河邊找鬼力细。 笑死,一個胖子當(dāng)著我的面吹牛固额,可吹牛的內(nèi)容都是我干的眠蚂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼斗躏,長吁一口氣:“原來是場噩夢啊……” “哼逝慧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤笛臣,失蹤者是張志新(化名)和其女友劉穎云稚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沈堡,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡静陈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了诞丽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲸拥。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖僧免,靈堂內(nèi)的尸體忽然破棺而出刑赶,到底是詐尸還是另有隱情,我是刑警寧澤猬膨,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布角撞,位于F島的核電站,受9級特大地震影響勃痴,放射性物質(zhì)發(fā)生泄漏谒所。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一沛申、第九天 我趴在偏房一處隱蔽的房頂上張望劣领。 院中可真熱鬧,春花似錦铁材、人聲如沸尖淘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽村生。三九已至,卻和暖如春饼丘,著一層夾襖步出監(jiān)牢的瞬間趁桃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工肄鸽, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留卫病,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓典徘,卻偏偏與公主長得像蟀苛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子逮诲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345