Tomcat 8.5下載地址
https://tomcat.apache.org/download-80.cgi
Tomcat啟動流程
Tomcat源碼目錄
catalina目錄
catalina包含所有的Servlet容器實現,以及涉及到安全、會話、集群、部署管理Servlet容器的各個方面,同時,它還包含了啟動入口。
coyote目錄
coyote是Tomcat鏈接器框架的名稱,是Tomcat服務器提供的客戶端訪問的外部接口,客戶端通過Coyote與服務器建立鏈接、發送請求並接收響應。
El目錄,提供java表達式語言
Jasper模塊提供JSP引擎
Naming模塊提供JNDI的服務
Juli提供服務器日志的服務
tomcat提供外部調用的接口api
Tomcat啟動流程分析
- 啟動流程解析:注意是標准的啟動,也就是從bin目錄下的啟動文件中啟動Tomcat

我們可以看到這個流程非常的清晰,同時注意到,Tomcat的啟動非常的標准,除去Boostrap和Catalin,我們可以對照一下Server.xml的配置文件。Server,service等等這些組件都是一一對照,同時又有先后順序。
基本的順序是先init方法,然后再start方法。
- 加入調試信息():注意是標准的啟動,也就是從bin目錄下的啟動文件中啟動Tomcat


可以看到,在源碼中加入調試的信息和流程圖是一致的。
我們可以看到,除了Bootstrap和catalina類,其他的Server,service等等之類的都只是一個接口,實現類均為StandardXXX類。
我們來看下StandardServer類,

問題來了,我們發現StandardServer類中沒有init方法,只有一個類似於init的initInternal方法,這個是為什么?
帶着問題我們進入下面的內容。
分析Tomcat請求過程
解耦:網絡協議與容器的解耦。
Connector鏈接器封裝了底層的網絡請求(Socket請求及相應處理),提供了統一的接口,使Container容器與具體的請求協議以及I/O方式解耦。
Connector將Socket輸入轉換成Request對象,交給Container容器進行處理,處理請求后,Container通過Connector提供的Response對象將結果寫入輸出流。
因為無論是Request對象還是Response對象都沒有實現Servlet規范對應的接口,Container會將它們進一步分裝成ServletRequest和ServletResponse.
問題來了,在Engine容器中,有四個級別的容器,他們的標准實現分別是StandardEngine、StandardHost、StandardContext、StandardWrapper。
組件的生命周期管理
各種組件如何統一管理
Tomcat的架構設計是清晰的、模塊化、它擁有很多組件,加入在啟動Tomcat時一個一個組件啟動,很容易遺漏組件,同時還會對后面的動態組件拓展帶來麻煩。如果采用我們傳統的方式的話,組件在啟動過程中如果發生異常,會很難管理,比如你的下一個組件調用了start方法,但是如果它的上級組件還沒有start甚至還沒有init的話,Tomcat的啟動會非常難管理,因此,Tomcat的設計者提出一個解決方案:用Lifecycle管理啟動,停止、關閉。
生命周期統一接口——Lifecycle
Tomcat內部架構中各個核心組件有包含與被包含關系,例如:Server包含了Service.Service又包含了Container和Connector,這個結構有一點像數據結構中的樹,樹的根結點沒有父節點,其他節點有且僅有一個父節點,每一個父節點有0至多個子節點。所以,我們可以通過父容器啟動它的子容器,這樣只要啟動根容器,就可以把其他所有的容器都啟動,從而達到了統一的啟動,停止、關閉的效果。
所有所有組件有一個統一的接口——Lifecycle,把所有的啟動、停止、關閉、生命周期相關的方法都組織到一起,就可以很方便管理Tomcat各個容器組件的生命周期。
Lifecycle其實就是定義了一些狀態常量和幾個方法,主要方法是init,start,stop三個方法。
例如:Tomcat的Server組件的init負責遍歷調用其包含所有的Service組件的init方法。
注意:Server只是一個接口,實現類為StandardServer,有意思的是,StandardServer沒有init方法,init方法是在哪里,其實是在它的父類LifecycleBase中,這個類就是統一的生命周期管理。



所以StandardServer最終只會調用到initInternal方法,這個方法會初始化子容器Service的init方法

為什么LifecycleBase這么玩,其實很多架構源碼都是這么玩的,包括JDK的容器源碼都是這么玩的,一個類,有一個接口,同時抽象一個抽象骨架類,把通用的實現放在抽象骨架類中,這樣設計就方便組件的管理,使用LifecycleBase骨架抽象類,在抽象方法中就可以進行統一的處理,具體的內容見下面。
抽象類LifecycleBase統一管理組件生命周期



具體實現類StandardXXX類調用initInternal方法實現具體的業務處理。

分析Tomcat請求過程
Host設計的目的
Tomcat誕生時,服務器資源很貴,所以一般一台服務器其實可以有多個域名映射,滿了滿足這種需求,比如,我的這台電腦,有一個localhost域名,同時在我的hosts文件中配置兩個域名,一個www.a.com 一個localhost。
Context設計的目的
container從上一個組件connector手上接過解析好的內部request,根據request來進行一系列的邏輯操作,直到調用到請求的servlet,然后組裝好response,返回給connecotr
先來看看container的分類吧:
Engine
Host
Context
Wrapper
它們各自的實現類分別是StandardEngine, StandardHost, StandardContext, and StandardWrapper,他們都在tomcat的org.apache.catalina.core包下。
它們之間的關系,可以查看tomcat的server.xml也能明白(根據節點父子關系),這么比喻吧:除了Wrapper最小,不能包含其他container外,Context內可以有零或多個Wrapper,Host可以擁有零或多個Host,Engine可以有零到多個Host。
Standard 的 container都是直接繼承抽象類:org.apache.catalina.core.ContainerBase:

Tomcat處理一個HTTP請求的過程
用戶點擊網頁內容,請求被發送到本機端口8080,被在那里監聽的Coyote HTTP/1.1 Connector獲得。
Connector把該請求交給它所在的Service的Engine來處理,並等待Engine的回應。
Engine獲得請求localhost/test/index.jsp,匹配所有的虛擬主機Host。
Engine匹配到名為localhost的Host(即使匹配不到也把請求交給該Host處理,因為該Host被定義為該Engine的默認主機),名為localhost的Host獲得請求/test/index.jsp,匹配它所擁有的所有的Context。Host匹配到路徑為/test的Context(如果匹配不到就把該請求交給路徑名為“ ”的Context去處理)。
path=“/test”的Context獲得請求/index.jsp,在它的mapping table中尋找出對應的Servlet。Context匹配到URL PATTERN為*.jsp的Servlet,對應於JspServlet類。
構造HttpServletRequest對象和HttpServletResponse對象,作為參數調用JspServlet的doGet()或doPost().執行業務邏輯、數據存儲等程序。
Context把執行完之后的HttpServletResponse對象返回給Host。
Host把HttpServletResponse對象返回給Engine。
Engine把HttpServletResponse對象返回Connector。
Connector把HttpServletResponse對象返回給客戶Browser。
管道模式
管道與閥門
在一個比較復雜的大型系統中,如果一個對象或數據流需要進行繁雜的邏輯處理,我們可以選擇在一個大的組件中直接處理這些繁雜的邏輯處理,這個方式雖然達到目的,但是拓展性和可重用性差。因為牽一發而動全身。
管道是就像一條管道把多個對象連接起來,整體看起來就像若干個閥門嵌套在管道中,而處理邏輯放在閥門上。
它的結構和實現是非常值得我們學習和借鑒的。
首先要了解的是每一種container都有一個自己的StandardValve
上面四個container對應的四個是:
StandardEngineValve
StandardHostValve
StandardContextValve
StandardWrapperValve
Pipeline就像一個工廠中的生產線,負責調配工人(valve)的位置,valve則是生產線上負責不同操作的工人。
一個生產線的完成需要兩步:
1,把原料運到工人邊上
2,工人完成自己負責的部分
而tomcat的Pipeline實現是這樣的:
1,在生產線上的第一個工人拿到生產原料后,二話不說就人給下一個工人,下一個工人模仿第一個工人那樣扔給下一個工人,直到最后一個工人,而最后一個工人被安排為上面提過的StandardValve,他要完成的工作居然是把生產資料運給自己包含的container的Pipeline上去。
2,四個container就相當於有四個生產線(Pipeline),四個Pipeline都這么干,直到最后的StandardWrapperValve拿到資源開始調用servlet。完成后返回來,一步一步的valve按照剛才丟生產原料是的順序的倒序一次執行。如此才完成了tomcat的Pipeline的機制。
手寫管道模式實現
我們了解到了,在管道中連接一個或者多個閥門,每一個閥門負責一部分邏輯處理,數據按照規定的順序往下流。此種模式分解了邏輯處理任務,可方便對某個任務單元進行安裝、拆卸,提高流程的可拓展性,可重用性,機動性,靈活性。
源碼分析
在CoyoteAdapter的service方法里,由下面這一句就進入Container的。
connector.getContainer().getPipeline().getFirst().invoke(request, response);
是的,這就是進入container迷宮的大門,歡迎來到Container。

一個StandardValve
來自org.apache.catalina.core.StandardEngineValve的invoke方法:

其他的類似StandardHostValve、StandardContextValve、StandardWrapperValve
Tomcat中定制閥門
管道機制給我們帶來了更好的拓展性,例如,你要添加一個額外的邏輯處理閥門是很容易的。
- 自定義個閥門PrintIPValve,只要繼承ValveBase並重寫invoke方法即可。注意在invoke方法中一定要執行調用下一個閥門的操作,否則會出現異常。
public class PrintIPValve extends ValveBase{
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("------自定義閥門PrintIPValve:"+request.getRemoteAddr());
getNext().invoke(request,response);
}
}
- 配置Tomcat的核心配置文件server.xml,這里把閥門配置到Engine容器下,作用范圍就是整個引擎,也可以根據作用范圍配置在Host或者是Context下
<Valve className="org.apache.catalina.valves.PrintIPValve"/>
- 源碼中是直接可以有效果,但是如果是運行版本,則可以將這個類導出成一個Jar包放入Tomcat/lib目錄下,也可以直接將.class文件打包進catalina.jar包中。
Tomcat中提供常用的閥門
AccessLogValve,請求訪問日志閥門,通過此閥門可以記錄所有客戶端的訪問日志,包括遠程主機IP,遠程主機名,請求方法,請求協議,會話ID,請求時間,處理時長,數據包大小等。它提供任意參數化的配置,可以通過任意組合來定制訪問日志的格式。
JDBCAccessLogValve,同樣是記錄訪問日志的閥門,但是它有助於將訪問日志通過JDBC持久化到數據庫中。
ErrorReportValve,這是一個講錯誤以HTML格式輸出的閥門
PersistentValve,這是對每一個請求的會話實現持久化的閥門
RemoteAddrValve,訪問控制閥門。可以通過配置決定哪些IP可以訪問WEB應用
RemoteHostValve,訪問控制閥門,通過配置覺得哪些主機名可以訪問WEB應用
RemoteIpValve,針對代理或者負載均衡處理的一個閥門,一般經過代理或者負載均衡轉發的請求都將自己的IP添加到請求頭”X-Forwarded-For”中,此時,通過閥門可以獲取訪問者真實的IP。
SemaphoreValve,這個是一個控制容器並發訪問的閥門,可以作用在不同容器上。
