Tomcat 架構(gòu)原理解析到架構(gòu)設(shè)計借鑒
Tomcat 發(fā)展這么多年,已經(jīng)比較成熟穩(wěn)定。在如今『追新求快』的時代,Tomcat 作為 JAVA Web 開發(fā)必備的工具似乎變成了『熟悉的陌生人』,難道說如今就沒有必要深入學習它了么?學習它我們又有什么收獲呢?
靜下心來,細細品味經(jīng)典的開源作品 。提升我們的「內(nèi)功」,具體來說就是學習大牛們?nèi)绾卧O(shè)計、架構(gòu)一個中間件系統(tǒng),并且讓這些經(jīng)驗為我所用。
美好的事物往往是整潔而優(yōu)雅的。但這并不等于簡單,而是要將復(fù)雜的系統(tǒng)分解成一個個小模塊,并且各個模塊的職責劃分也要清晰合理。
與此相反的是凌亂無序,比如你看到城中村一堆互相糾纏在一起的電線,可能會感到不適。維護的代碼一個類幾千行、一個方法好幾百行。方法之間相互耦合糅雜在一起,你可能會說 what the f*k!

學習目的
掌握 Tomcat 架構(gòu)設(shè)計與原理提高內(nèi)功
宏觀上看
Tomcat 作為一個 「Http 服務(wù)器 + Servlet 容器」,對我們屏蔽了應(yīng)用層協(xié)議和網(wǎng)絡(luò)通信細節(jié),給我們的是標準的 Request 和 Response 對象;對于具體的業(yè)務(wù)邏輯則作為變化點,交給我們來實現(xiàn)。我們使用了SpringMVC 之類的框架,可是卻從來不需要考慮 TCP 連接、 Http 協(xié)議的數(shù)據(jù)處理與響應(yīng)。就是因為 Tomcat 已經(jīng)為我們做好了這些,我們只需要關(guān)注每個請求的具體業(yè)務(wù)邏輯。
微觀上看
Tomcat 內(nèi)部也隔離了變化點與不變點,使用了組件化設(shè)計,目的就是為了實現(xiàn)「俄羅斯套娃式」的高度定制化(組合模式),而每個組件的生命周期管理又有一些共性的東西,則被提取出來成為接口和抽象類,讓具體子類實現(xiàn)變化點,也就是模板方法設(shè)計模式。
當今流行的微服務(wù)也是這個思路,按照功能將單體應(yīng)用拆成「微服務(wù)」,拆分過程要將共性提取出來,而這些共性就會成為核心的基礎(chǔ)服務(wù)或者通用庫。「中臺」思想亦是如此。
設(shè)計模式往往就是封裝變化的一把利器,合理的運用設(shè)計模式能讓我們的代碼與系統(tǒng)設(shè)計變得優(yōu)雅且整潔。
這就是學習優(yōu)秀開源軟件能獲得的「內(nèi)功」,從不會過時,其中的設(shè)計思想與哲學才是根本之道。從中借鑒設(shè)計經(jīng)驗,合理運用設(shè)計模式封裝變與不變,更能從它們的源碼中汲取經(jīng)驗,提升自己的系統(tǒng)設(shè)計能力。
宏觀理解一個請求如何與 Spring 聯(lián)系起來
在工作過程中,我們對 Java 語法已經(jīng)很熟悉了,甚至「背」過一些設(shè)計模式,用過很多 Web 框架,但是很少有機會將他們用到實際項目中,讓自己獨立設(shè)計一個系統(tǒng)似乎也是根據(jù)需求一個個 Service 實現(xiàn)而已。腦子里似乎沒有一張 Java Web 開發(fā)全景圖,比如我并不知道瀏覽器的請求是怎么跟 Spring 中的代碼聯(lián)系起來的。
為了突破這個瓶頸,為何不站在巨人的肩膀上學習優(yōu)秀的開源系統(tǒng),看大牛們是如何思考這些問題。
學習 Tomcat 的原理,我發(fā)現(xiàn) Servlet 技術(shù)是 Web 開發(fā)的原點,幾乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet 的封裝,Spring 應(yīng)用本身就是一個 Servlet(DispatchSevlet),而 Tomcat 和 Jetty 這樣的 Web 容器,負責加載和運行 Servlet。如圖所示:

提升自己的系統(tǒng)設(shè)計能力
學習 Tomcat ,我還發(fā)現(xiàn)用到不少 Java 高級技術(shù),比如 Java 多線程并發(fā)編程、Socket 網(wǎng)絡(luò)編程以及反射等。之前也只是了解這些技術(shù),為了面試也背過一些題。但是總感覺「知道」與會用之間存在一道溝壑,通過對 Tomcat 源碼學習,我學會了什么場景去使用這些技術(shù)。
還有就是系統(tǒng)設(shè)計能力,比如面向接口編程、組件化組合模式、骨架抽象類、一鍵式啟停、對象池技術(shù)以及各種設(shè)計模式,比如模板方法、觀察者模式、責任鏈模式等,之后我也開始模仿它們并把這些設(shè)計思想運用到實際的工作中。
整體架構(gòu)設(shè)計
今天咱們就來一步一步分析 Tomcat 的設(shè)計思路,一方面我們可以學到 Tomcat 的總體架構(gòu),學會從宏觀上怎么去設(shè)計一個復(fù)雜系統(tǒng),怎么設(shè)計頂層模塊,以及模塊之間的關(guān)系;另一方面也為我們深入學習 Tomcat 的工作原理打下基礎(chǔ)。
Tomcat 啟動流程:startup.sh -> catalina.sh start ->java -jar org.Apache.catalina.startup.Bootstrap.main()
Tomcat 實現(xiàn)的 2 個核心功能:
- 處理 Socket 連接,負責網(wǎng)絡(luò)字節(jié)流與 Request 和 Response 對象的轉(zhuǎn)化。
- 加載并管理 Servlet ,以及處理具體的 Request 請求。
所以 Tomcat 設(shè)計了兩個核心組件連接器(Connector)和容器(Container)。連接器負責對外交流,容器負責內(nèi)部 處理
Tomcat為了實現(xiàn)支持多種 I/O 模型和應(yīng)用層協(xié)議,一個容器可能對接多個連接器,就好比一個房間有多個門。

Tomcat整體架構(gòu)
- Server 對應(yīng)的就是一個 Tomcat 實例。
- Service 默認只有一個,也就是一個 Tomcat 實例默認一個 Service。
- Connector:一個 Service 可能多個 連接器,接受不同連接協(xié)議。
- Container: 多個連接器對應(yīng)一個容器,頂層容器其實就是 Engine。
每個組件都有對應(yīng)的生命周期,需要啟動,同時還要啟動自己內(nèi)部的子組件,比如一個 Tomcat 實例包含一個 Service,一個 Service 包含多個連接器和一個容器。而一個容器包含多個 Host, Host 內(nèi)部可能有多個 Contex t 容器,而一個 Context 也會包含多個 Servlet,所以 Tomcat 利用組合模式管理組件每個組件,對待過個也想對待單個組一樣對待。整體上每個組件設(shè)計就像是「俄羅斯套娃」一樣。
連接器
在開始講連接器前,我先鋪墊一下 Tomcat支持的多種 I/O 模型和應(yīng)用層協(xié)議。
Tomcat支持的 I/O 模型有:
- NIO:非阻塞 I/O,采用 Java NIO 類庫實現(xiàn)。
- NIO2:異步I/O,采用 JDK 7 最新的 NIO2 類庫實現(xiàn)。
- APR:采用 Apache可移植運行庫實現(xiàn),是 C/C++ 編寫的本地庫。
Tomcat 支持的應(yīng)用層協(xié)議有:
- HTTP/1.1:這是大部分 Web 應(yīng)用采用的訪問協(xié)議。
- AJP:用于和 Web 服務(wù)器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
所以一個容器可能對接多個連接器。連接器對 Servlet 容器屏蔽了網(wǎng)絡(luò)協(xié)議與 I/O 模型的區(qū)別,無論是 Http 還是 AJP,在容器中獲取到的都是一個標準的 ServletRequest 對象。
細化連接器的功能需求就是:
- 監(jiān)聽網(wǎng)絡(luò)端口。
- 接受網(wǎng)絡(luò)連接請求。
- 讀取請求網(wǎng)絡(luò)字節(jié)流。
- 根據(jù)具體應(yīng)用層協(xié)議(HTTP/AJP)解析字節(jié)流,生成統(tǒng)一的 Tomcat Request 對象。
- 將 Tomcat Request 對象轉(zhuǎn)成標準的 ServletRequest。
- 調(diào)用 Servlet容器,得到 ServletResponse。
- 將 ServletResponse轉(zhuǎn)成 Tomcat Response 對象。
- 將 Tomcat Response 轉(zhuǎn)成網(wǎng)絡(luò)字節(jié)流。
- 將響應(yīng)字節(jié)流寫回給瀏覽器。
需求列清楚后,我們要考慮的下一個問題是,連接器應(yīng)該有哪些子模塊?優(yōu)秀的模塊化設(shè)計應(yīng)該考慮高內(nèi)聚、低耦合。
- 高內(nèi)聚是指相關(guān)度比較高的功能要盡可能集中,不要分散。
- 低耦合是指兩個相關(guān)的模塊要盡可能減少依賴的部分和降低依賴的程度,不要讓兩個模塊產(chǎn)生強依賴。
我們發(fā)現(xiàn)連接器需要完成 3 個高內(nèi)聚的功能:
- 網(wǎng)絡(luò)通信。
- 應(yīng)用層協(xié)議解析。
- Tomcat Request/Response 與 ServletRequest/ServletResponse 的轉(zhuǎn)化。
因此 Tomcat 的設(shè)計者設(shè)計了 3 個組件來實現(xiàn)這 3 個功能,分別是 EndPoint、Processor 和 Adapter。
網(wǎng)絡(luò)通信的 I/O 模型是變化的, 應(yīng)用層協(xié)議也是變化的,但是整體的處理邏輯是不變的,EndPoint 負責提供字節(jié)流給 Processor,Processor負責提供 Tomcat Request 對象給 Adapter,Adapter負責提供 ServletRequest對象給容器。
封裝變與不變
因此 Tomcat 設(shè)計了一系列抽象基類來封裝這些穩(wěn)定的部分,抽象基類 AbstractProtocol實現(xiàn)了 ProtocolHandler接口。每一種應(yīng)用層協(xié)議有自己的抽象基類,比如 AbstractAjpProtocol和 AbstractHttp11Protocol,具體協(xié)議的實現(xiàn)類擴展了協(xié)議層抽象基類。
這就是模板方法設(shè)計模式的運用。

應(yīng)用層協(xié)議抽象
總結(jié)下來,連接器的三個核心組件 Endpoint、Processor和 Adapter來分別做三件事情,其中 Endpoint和 Processor放在一起抽象成了 ProtocolHandler組件,它們的關(guān)系如下圖所示。

連接器
ProtocolHandler 組件
主要處理 網(wǎng)絡(luò)連接 和 應(yīng)用層協(xié)議 ,包含了兩個重要部件 EndPoint 和 Processor,兩個組件組合形成 ProtocoHandler,下面我來詳細介紹它們的工作原理。
EndPoint
EndPoint是通信端點,即通信監(jiān)聽的接口,是具體的 Socket 接收和發(fā)送處理器,是對傳輸層的抽象,因此 EndPoint是用來實現(xiàn) TCP/IP 協(xié)議數(shù)據(jù)讀寫的,本質(zhì)調(diào)用操作系統(tǒng)的 socket 接口。
EndPoint是一個接口,對應(yīng)的抽象實現(xiàn)類是 AbstractEndpoint,而 AbstractEndpoint的具體子類,比如在 NioEndpoint和 Nio2Endpoint中,有兩個重要的子組件:Acceptor和 SocketProcessor。
其中 Acceptor 用于監(jiān)聽 Socket 連接請求。SocketProcessor用于處理 Acceptor 接收到的 Socket請求,它實現(xiàn) Runnable接口,在 Run方法里調(diào)用應(yīng)用層協(xié)議處理組件 Processor 進行處理。為了提高處理能力,SocketProcessor被提交到線程池來執(zhí)行。
我們知道,對于 Java 的多路復(fù)用器的使用,無非是兩步:
- 創(chuàng)建一個 Seletor,在它身上注冊各種感興趣的事件,然后調(diào)用 select 方法,等待感興趣的事情發(fā)生。
- 感興趣的事情發(fā)生了,比如可以讀了,這時便創(chuàng)建一個新的線程從 Channel 中讀數(shù)據(jù)。
在 Tomcat 中 NioEndpoint 則是 AbstractEndpoint 的具體實現(xiàn),里面組件雖然很多,但是處理邏輯還是前面兩步。它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor和Executor 共 5 個組件,分別分工合作實現(xiàn)整個 TCP/IP 協(xié)議的處理。
- LimitLatch 是連接控制器,它負責控制最大連接數(shù),NIO 模式下默認是 10000,達到這個閾值后,連接請求被拒絕。
- Acceptor跑在一個單獨的線程里,它在一個死循環(huán)里調(diào)用 accept方法來接收新連接,一旦有新的連接請求到來,accept方法返回一個 Channel 對象,接著把 Channel對象交給 Poller 去處理。
- Poller 的本質(zhì)是一個 Selector,也跑在單獨線程里。Poller在內(nèi)部維護一個Channel數(shù)組,它在一個死循環(huán)里不斷檢測 Channel的數(shù)據(jù)就緒狀態(tài),一旦有 Channel可讀,就生成一個 SocketProcessor任務(wù)對象扔給 Executor去處理。
- SocketProcessor 實現(xiàn)了 Runnable 接口,其中 run 方法中的 getHandler().process(socketWrApper, SocketEvent.CONNECT_FAIL); 代碼則是獲取 handler 并執(zhí)行處理 socketWrapper,最后通過 socket 獲取合適應(yīng)用層協(xié)議處理器,也就是調(diào)用 Http11Processor 組件來處理請求。Http11Processor 讀取 Channel 的數(shù)據(jù)來生成 ServletRequest 對象,Http11Processor 并不是直接讀取 Channel 的。這是因為 Tomcat 支持同步非阻塞 I/O 模型和異步 I/O 模型,在 Java API 中,相應(yīng)的 Channel 類也是不一樣的,比如有 AsynchronousSocketChannel 和 SocketChannel,為了對 Http11Processor 屏蔽這些差異,Tomcat 設(shè)計了一個包裝類叫作 SocketWrapper,Http11Processor 只調(diào)用 SocketWrapper 的方法去讀寫數(shù)據(jù)。
- Executor就是線程池,負責運行 SocketProcessor任務(wù)類,SocketProcessor 的 run方法會調(diào)用 Http11Processor 來讀取和解析請求數(shù)據(jù)。我們知道,Http11Processor是應(yīng)用層協(xié)議的封裝,它會調(diào)用容器獲得響應(yīng),再把響應(yīng)通過 Channel寫出。
工作流程如下所示:

NioEndPoint
Processor
Processor 用來實現(xiàn) HTTP 協(xié)議,Processor 接收來自 EndPoint 的 Socket,讀取字節(jié)流解析成 Tomcat Request 和 Response 對象,并通過 Adapter 將其提交到容器處理,Processor 是對應(yīng)用層協(xié)議的抽象。

從圖中我們看到,EndPoint 接收到 Socket 連接后,生成一個 SocketProcessor 任務(wù)提交到線程池去處理,SocketProcessor 的 Run 方法會調(diào)用 HttpProcessor 組件去解析應(yīng)用層協(xié)議,Processor 通過解析生成 Request 對象后,會調(diào)用 Adapter 的 Service 方法,方法內(nèi)部通過 以下代碼將請求傳遞到容器中。
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Adapter 組件
由于協(xié)議的不同,Tomcat 定義了自己的 Request 類來存放請求信息,這里其實體現(xiàn)了面向?qū)ο蟮乃季S。但是這個 Request 不是標準的 ServletRequest ,所以不能直接使用 Tomcat 定義 Request 作為參數(shù)直接容器。
Tomcat 設(shè)計者的解決方案是引入 CoyoteAdapter,這是適配器模式的經(jīng)典運用,連接器調(diào)用 CoyoteAdapter 的 Sevice 方法,傳入的是 Tomcat Request 對象,CoyoteAdapter負責將 Tomcat Request 轉(zhuǎn)成 ServletRequest,再調(diào)用容器的 Service方法。
容器
連接器負責外部交流,容器負責內(nèi)部處理。具體來說就是,連接器處理 Socket 通信和應(yīng)用層協(xié)議的解析,得到 Servlet請求;而容器則負責處理 Servlet請求。
容器:顧名思義就是拿來裝東西的, 所以 Tomcat 容器就是拿來裝載 Servlet。
Tomcat 設(shè)計了 4 種容器,分別是 Engine、Host、Context和 Wrapper。Server 代表 Tomcat 實例。
要注意的是這 4 種容器不是平行關(guān)系,屬于父子關(guān)系,如下圖所示:

容器
你可能會問,為啥要設(shè)計這么多層次的容器,這不是增加復(fù)雜度么?其實這背后的考慮是,Tomcat 通過一種分層的架構(gòu),使得 Servlet 容器具有很好的靈活性。因為這里正好符合一個 Host 多個 Context, 一個 Context 也包含多個 Servlet,而每個組件都需要統(tǒng)一生命周期管理,所以組合模式設(shè)計這些容器
Wrapper 表示一個 Servlet ,Context 表示一個 Web 應(yīng)用程序,而一個 Web 程序可能有多個 Servlet ;Host 表示一個虛擬主機,或者說一個站點,一個 Tomcat 可以配置多個站點(Host);一個站點( Host) 可以部署多個 Web 應(yīng)用;Engine 代表 引擎,用于管理多個站點(Host),一個 Service 只能有 一個 Engine。
可通過 Tomcat 配置文件加深對其層次關(guān)系理解。
<Server port="8005" shutdown="SHUTDOWN"> // 頂層組件,可包含多個 Service,代表一個 Tomcat 實例
<Service name="Catalina"> // 頂層組件,包含一個 Engine ,多個連接器
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> // 連接器
// 容器組件:一個 Engine 處理 Service 所有請求,包含多個 Host
<Engine name="Catalina" defaultHost="localhost">
// 容器組件:處理指定Host下的客戶端請求, 可包含多個 Context
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
// 容器組件:處理特定 Context Web應(yīng)用的所有客戶端請求
<Context></Context>
</Host>
</Engine>
</Service>
</Server>
如何管理這些容器?我們發(fā)現(xiàn)容器之間具有父子關(guān)系,形成一個樹形結(jié)構(gòu),是不是想到了設(shè)計模式中的 組合模式 。
Tomcat 就是用組合模式來管理這些容器的。具體實現(xiàn)方法是,所有容器組件都實現(xiàn)了 Container接口,因此組合模式可以使得用戶對單容器對象和組合容器對象的使用具有一致性。這里單容器對象指的是最底層的 Wrapper,組合容器對象指的是上面的 Context、Host或者Engine。Container 接口定義如下:
public interface Container extends Lifecycle {
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}
我們看到了getParent、SetParent、addChild和 removeChild等方法,這里正好驗證了我們說的組合模式。我們還看到 Container接口拓展了 Lifecycle ,Tomcat 就是通過 Lifecycle 統(tǒng)一管理所有容器的組件的生命周期。通過組合模式管理所有容器,拓展 Lifecycle 實現(xiàn)對每個組件的生命周期管理 ,Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()。
請求定位 Servlet 的過程
一個請求是如何定位到讓哪個 Wrapper 的 Servlet 處理的?答案是,Tomcat 是用 Mapper 組件來完成這個任務(wù)的。
Mapper 組件的功能就是將用戶請求的 URL 定位到一個 Servlet,它的工作原理是:Mapper組件里保存了 Web 應(yīng)用的配置信息,其實就是容器組件與訪問路徑的映射關(guān)系,比如 Host容器里配置的域名、Context容器里的 Web應(yīng)用路徑,以及 Wrapper容器里 Servlet 映射的路徑,你可以想象這些配置信息就是一個多層次的 Map。
當一個請求到來時,Mapper 組件通過解析請求 URL 里的域名和路徑,再到自己保存的 Map 里去查找,就能定位到一個 Servlet。請你注意,一個請求 URL 最后只會定位到一個 Wrapper容器,也就是一個 Servlet。

假如有用戶訪問一個 URL,比如圖中的http://user.shopping.com:8080/order/buy,Tomcat 如何將這個 URL 定位到一個 Servlet 呢?
- 首先根據(jù)協(xié)議和端口號確定 Service 和 Engine。Tomcat 默認的 HTTP 連接器監(jiān)聽 8080 端口、默認的 AJP 連接器監(jiān)聽 8009 端口。上面例子中的 URL 訪問的是 8080 端口,因此這個請求會被 HTTP 連接器接收,而一個連接器是屬于一個 Service 組件的,這樣 Service 組件就確定了。我們還知道一個 Service 組件里除了有多個連接器,還有一個容器組件,具體來說就是一個 Engine 容器,因此 Service 確定了也就意味著 Engine 也確定了。
- 根據(jù)域名選定 Host。 Service 和 Engine 確定后,Mapper 組件通過 URL 中的域名去查找相應(yīng)的 Host 容器,比如例子中的 URL 訪問的域名是user.shopping.com,因此 Mapper 會找到 Host2 這個容器。
- 根據(jù) URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據(jù) URL 的路徑來匹配相應(yīng)的 Web 應(yīng)用的路徑,比如例子中訪問的是 /order,因此找到了 Context4 這個 Context 容器。
- 根據(jù) URL 路徑找到 Wrapper(Servlet)。 Context 確定后,Mapper 再根據(jù) web.xml 中配置的 Servlet 映射路徑來找到具體的 Wrapper 和 Servlet。
連接器中的 Adapter 會調(diào)用容器的 Service 方法來執(zhí)行 Servlet,最先拿到請求的是 Engine 容器,Engine 容器對請求做一些處理后,會把請求傳給自己子容器 Host 繼續(xù)處理,依次類推,最后這個請求會傳給 Wrapper 容器,Wrapper 會調(diào)用最終的 Servlet 來處理。那么這個調(diào)用過程具體是怎么實現(xiàn)的呢?答案是使用 Pipeline-Valve 管道。
Pipeline-Valve 是責任鏈模式,責任鏈模式是指在一個請求處理的過程中有很多處理者依次對請求進行處理,每個處理者負責做自己相應(yīng)的處理,處理完之后將再調(diào)用下一個處理者繼續(xù)處理,Valve 表示一個處理點(也就是一個處理閥門),因此 invoke方法就是來處理請求的。
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response)
}
繼續(xù)看 Pipeline 接口
public interface Pipeline {
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}
Pipeline中有 addValve方法。Pipeline 中維護了 Valve鏈表,Valve可以插入到Pipeline中,對請求做某些處理。我們還發(fā)現(xiàn) Pipeline 中沒有 invoke 方法,因為整個調(diào)用鏈的觸發(fā)是 Valve 來完成的,Valve完成自己的處理后,調(diào)用 getNext.invoke() 來觸發(fā)下一個 Valve 調(diào)用。
其實每個容器都有一個 Pipeline 對象,只要觸發(fā)了這個 Pipeline 的第一個 Valve,這個容器里Pipeline中的 Valve 就都會被調(diào)用到。但是,不同容器的 Pipeline 是怎么鏈式觸發(fā)的呢,比如 Engine 中 Pipeline 需要調(diào)用下層容器 Host 中的 Pipeline。
這是因為 Pipeline中還有個 getBasic方法。這個 BasicValve處于 Valve鏈表的末端,它是 Pipeline中必不可少的一個 Valve,負責調(diào)用下層容器的 Pipeline 里的第一個 Valve。

整個過程分是通過連接器中的 CoyoteAdapter 觸發(fā),它會調(diào)用 Engine 的第一個 Valve:
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
// 省略其他代碼
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
...
}
Wrapper 容器的最后一個 Valve 會創(chuàng)建一個 Filter 鏈,并調(diào)用 doFilter() 方法,最終會調(diào)到 Servlet的 service方法。
前面我們不是講到了 Filter,似乎也有相似的功能,那 Valve 和 Filter有什么區(qū)別嗎?它們的區(qū)別是:
- Valve是 Tomcat的私有機制,與 Tomcat 的基礎(chǔ)架構(gòu) API是緊耦合的。Servlet API是公有的標準,所有的 Web 容器包括 Jetty 都支持 Filter 機制。
- 另一個重要的區(qū)別是 Valve工作在 Web 容器級別,攔截所有應(yīng)用的請求;而 Servlet Filter 工作在應(yīng)用級別,只能攔截某個 Web 應(yīng)用的所有請求。如果想做整個 Web容器的攔截器,必須通過 Valve來實現(xiàn)。
Lifecycle 生命周期
前面我們看到 Container容器 繼承了 Lifecycle 生命周期。如果想讓一個系統(tǒng)能夠?qū)ν馓峁┓?wù),我們需要創(chuàng)建、組裝并啟動這些組件;在服務(wù)停止的時候,我們還需要釋放資源,銷毀這些組件,因此這是一個動態(tài)的過程。也就是說,Tomcat 需要動態(tài)地管理這些組件的生命周期。
如何統(tǒng)一管理組件的創(chuàng)建、初始化、啟動、停止和銷毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動和停止不遺漏、不重復(fù)?
一鍵式啟停:LifeCycle 接口
設(shè)計就是要找到系統(tǒng)的變化點和不變點。這里的不變點就是每個組件都要經(jīng)歷創(chuàng)建、初始化、啟動這幾個過程,這些狀態(tài)以及狀態(tài)的轉(zhuǎn)化是不變的。而變化點是每個具體組件的初始化方法,也就是啟動方法是不一樣的。
因此,Tomcat 把不變點抽象出來成為一個接口,這個接口跟生命周期有關(guān),叫作 LifeCycle。LifeCycle 接口里定義這么幾個方法:init()、start()、stop() 和 destroy(),每個具體的組件(也就是容器)去實現(xiàn)這些方法。
在父組件的 init() 方法里需要創(chuàng)建子組件并調(diào)用子組件的 init() 方法。同樣,在父組件的 start()方法里也需要調(diào)用子組件的 start() 方法,因此調(diào)用者可以無差別的調(diào)用各組件的 init() 方法和 start() 方法,這就是組合模式的使用,并且只要調(diào)用最頂層組件,也就是 Server 組件的 init()和start() 方法,整個 Tomcat 就被啟動起來了。所以 Tomcat 采取組合模式管理容器,容器繼承 LifeCycle 接口,這樣就可以向針對單個對象一樣一鍵管理各個容器的生命周期,整個 Tomcat 就啟動起來。
可擴展性:LifeCycle 事件
我們再來考慮另一個問題,那就是系統(tǒng)的可擴展性。因為各個組件init() 和 start() 方法的具體實現(xiàn)是復(fù)雜多變的,比如在 Host 容器的啟動方法里需要掃描 webapps 目錄下的 Web 應(yīng)用,創(chuàng)建相應(yīng)的 Context 容器,如果將來需要增加新的邏輯,直接修改start() 方法?這樣會違反開閉原則,那如何解決這個問題呢?開閉原則說的是為了擴展系統(tǒng)的功能,你不能直接修改系統(tǒng)中已有的類,但是你可以定義新的類。
組件的 init() 和 start() 調(diào)用是由它的父組件的狀態(tài)變化觸發(fā)的,上層組件的初始化會觸發(fā)子組件的初始化,上層組件的啟動會觸發(fā)子組件的啟動,因此我們把組件的生命周期定義成一個個狀態(tài),把狀態(tài)的轉(zhuǎn)變看作是一個事件。而事件是有監(jiān)聽器的,在監(jiān)聽器里可以實現(xiàn)一些邏輯,并且監(jiān)聽器也可以方便的添加和刪除,這就是典型的觀察者模式。
以下就是 Lyfecycle 接口的定義:

Lyfecycle
重用性:LifeCycleBase 抽象基類
再次看到抽象模板設(shè)計模式。
有了接口,我們就要用類去實現(xiàn)接口。一般來說實現(xiàn)類不止一個,不同的類在實現(xiàn)接口時往往會有一些相同的邏輯,如果讓各個子類都去實現(xiàn)一遍,就會有重復(fù)代碼。那子類如何重用這部分邏輯呢?其實就是定義一個基類來實現(xiàn)共同的邏輯,然后讓各個子類去繼承它,就達到了重用的目的。
Tomcat 定義一個基類 LifeCycleBase 來實現(xiàn) LifeCycle 接口,把一些公共的邏輯放到基類中去,比如生命狀態(tài)的轉(zhuǎn)變與維護、生命事件的觸發(fā)以及監(jiān)聽器的添加和刪除等,而子類就負責實現(xiàn)自己的初始化、啟動和停止等方法。
public abstract class LifecycleBase implements Lifecycle{
// 持有所有的觀察者
private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
/**
* 發(fā)布事件
*
* @param type Event type
* @param data Data associated with event.
*/
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
// 模板方法定義整個啟動流程,啟動所有容器
@Override
public final synchronized void init() throws LifecycleException {
//1. 狀態(tài)檢查
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
//2. 觸發(fā) INITIALIZING 事件的監(jiān)聽器
setStateInternal(LifecycleState.INITIALIZING, null, false);
// 3. 調(diào)用具體子類的初始化方法
initInternal();
// 4. 觸發(fā) INITIALIZED 事件的監(jiān)聽器
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(
sm.getString("lifecycleBase.initFail",toString()), t);
}
}
}
Tomcat 為了實現(xiàn)一鍵式啟停以及優(yōu)雅的生命周期管理,并考慮到了可擴展性和可重用性,將面向?qū)ο笏枷牒驮O(shè)計模式發(fā)揮到了極致,Containaer接口維護了容器的父子關(guān)系,Lifecycle 組合模式實現(xiàn)組件的生命周期維護,生命周期每個組件有變與不變的點,運用模板方法模式。分別運用了組合模式、觀察者模式、骨架抽象類和模板方法。
如果你需要維護一堆具有父子關(guān)系的實體,可以考慮使用組合模式。
觀察者模式聽起來 “高大上”,其實就是當一個事件發(fā)生后,需要執(zhí)行一連串更新操作。實現(xiàn)了低耦合、非侵入式的通知與更新機制。

Container 繼承了 LifeCycle,StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應(yīng)容器組件的具體實現(xiàn)類,因為它們都是容器,所以繼承了 ContainerBase 抽象基類,而 ContainerBase 實現(xiàn)了 Container 接口,也繼承了 LifeCycleBase 類,它們的生命周期管理接口和功能接口是分開的,這也符合設(shè)計中接口分離的原則。
Tomcat 為何打破雙親委派機制
雙親委派
我們知道 JVM的類加載器加載 Class 的時候基于雙親委派機制,也就是會將加載交給自己的父加載器加載,如果 父加載器為空則查找Bootstrap 是否加載過,當無法加載的時候才讓自己加載。JDK 提供一個抽象類 ClassLoader,這個抽象類中定義了三個關(guān)鍵方法。對外使用loadClass(String name) 用于子類重寫打破雙親委派:loadClass(String name, boolean resolve)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 查找該 class 是否已經(jīng)被加載過
Class<?> c = findLoadedClass(name);
// 如果沒有加載過
if (c == null) {
// 委托給父加載器去加載,遞歸調(diào)用
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加載器為空,查找 Bootstrap 是否加載過
c = findBootstrapClassOrNull(name);
}
// 若果依然加載不到,則調(diào)用自己的 findClass 去加載
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name){
//1. 根據(jù)傳入的類名 name,到在特定目錄下去尋找類文件,把.class 文件讀入內(nèi)存
...
//2. 調(diào)用 defineClass 將字節(jié)數(shù)組轉(zhuǎn)成 Class 對象
return defineClass(buf, off, len);
}
// 將字節(jié)碼數(shù)組解析成一個 Class 對象,用 native 方法實現(xiàn)
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
JDK 中有 3 個類加載器,另外你也可以自定義類加載器,它們的關(guān)系如下圖所示。

類加載器
- BootstrapClassLoader是啟動類加載器,由 C 語言實現(xiàn),用來加載 JVM啟動時所需要的核心類,比如rt.jar、resources.jar等。
- ExtClassLoader是擴展類加載器,用來加載jrelibext目錄下 JAR 包。
- AppClassLoader是系統(tǒng)類加載器,用來加載 classpath下的類,應(yīng)用程序默認用它來加載類。
- 自定義類加載器,用來加載自定義路徑下的類。
這些類加載器的工作原理是一樣的,區(qū)別是它們的加載路徑不同,也就是說 findClass這個方法查找的路徑不同。雙親委托機制是為了保證一個 Java 類在 JVM 中是唯一的,假如你不小心寫了一個與 JRE 核心類同名的類,比如 Object類,雙親委托機制能保證加載的是 JRE里的那個 Object類,而不是你寫的 Object類。這是因為 AppClassLoader在加載你的 Object 類時,會委托給 ExtClassLoader去加載,而 ExtClassLoader又會委托給 BootstrapClassLoader,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載過了 Object類,會直接返回,不會去加載你寫的 Object類。我們最多只能 獲取到 ExtClassLoader這里注意下。
Tomcat 熱加載
Tomcat 本質(zhì)是通過一個后臺線程做周期性的任務(wù),定期檢測類文件的變化,如果有變化就重新加載類。我們來看 ContainerBackgroundProcessor具體是如何實現(xiàn)的。
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
// 請注意這里傳入的參數(shù)是 " 宿主類 " 的實例
processChildren(ContainerBase.this);
}
protected void processChildren(Container container) {
try {
//1. 調(diào)用當前容器的 backgroundProcess 方法。
container.backgroundProcess();
//2. 遍歷所有的子容器,遞歸調(diào)用 processChildren,
// 這樣當前容器的子孫都會被處理
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
// 這里請你注意,容器基類有個變量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后臺線程,無需父容器來調(diào)用它的 processChildren 方法。
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i]);
}
}
} catch (Throwable t) { ... }
Tomcat 的熱加載就是在 Context 容器實現(xiàn),主要是調(diào)用了 Context 容器的 reload 方法。拋開細節(jié)從宏觀上看主要完成以下任務(wù):
- 停止和銷毀 Context 容器及其所有子容器,子容器其實就是 Wrapper,也就是說 Wrapper 里面 Servlet 實例也被銷毀了。
- 停止和銷毀 Context 容器關(guān)聯(lián)的 Listener 和 Filter。
- 停止和銷毀 Context 下的 Pipeline 和各種 Valve。
- 停止和銷毀 Context 的類加載器,以及類加載器加載的類文件資源。
- 啟動 Context 容器,在這個過程中會重新創(chuàng)建前面四步被銷毀的資源。
在這個過程中,類加載器發(fā)揮著關(guān)鍵作用。一個 Context 容器對應(yīng)一個類加載器,類加載器在銷毀的過程中會把它加載的所有類也全部銷毀。Context 容器在啟動過程中,會創(chuàng)建一個新的類加載器來加載新的類文件。
Tomcat 的類加載器
Tomcat 的自定義類加載器 WebAppClassLoader打破了雙親委托機制,它首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。具體實現(xiàn)就是重寫 ClassLoader的兩個方法:findClass和 loadClass。
findClass 方法
org.apache.catalina.loader.WebappClassLoaderBase#findClass;為了方便理解和閱讀,我去掉了一些細節(jié):
public Class<?> findClass(String name) throws ClassNotFoundException {
...
Class<?> clazz = null;
try {
//1. 先在 Web 應(yīng)用目錄下查找類
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2. 如果在本地目錄沒有找到,交給父加載器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3. 如果父類也沒找到,拋出 ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
- 先在 Web 應(yīng)用本地目錄下查找要加載的類。
- 如果沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類加載器 AppClassLoader。
- 如何父加載器也沒找到這個類,拋出 ClassNotFound異常。
loadClass 方法
再來看 Tomcat 類加載器的 loadClass方法的實現(xiàn),同樣我也去掉了一些細節(jié):
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地 cache 查找該類是否已經(jīng)加載過
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 從系統(tǒng)類加載器的 cache 中查找是否加載過
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 嘗試用 ExtClassLoader 類加載器類加載,為什么?
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 嘗試在本地目錄搜索 class 并加載
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 嘗試用系統(tǒng)類加載器 (也就是 AppClassLoader) 來加載
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述過程都加載失敗,拋出異常
throw new ClassNotFoundException(name);
}
主要有六個步驟:
- 先在本地 Cache 查找該類是否已經(jīng)加載過,也就是說 Tomcat 的類加載器是否已經(jīng)加載過這個類。
- 如果 Tomcat 類加載器沒有加載過這個類,再看看系統(tǒng)類加載器是否加載過。
- 如果都沒有,就讓ExtClassLoader去加載,這一步比較關(guān)鍵,目的 防止 Web 應(yīng)用自己的類覆蓋 JRE 的核心類。因為 Tomcat 需要打破雙親委托機制,假如 Web 應(yīng)用里自定義了一個叫 Object 的類,如果先加載這個 Object 類,就會覆蓋 JRE 里面的那個 Object 類,這就是為什么 Tomcat 的類加載器會優(yōu)先嘗試用 ExtClassLoader去加載,因為 ExtClassLoader會委托給 BootstrapClassLoader去加載,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載了 Object 類,直接返回給 Tomcat 的類加載器,這樣 Tomcat 的類加載器就不會去加載 Web 應(yīng)用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題。
- 如果 ExtClassLoader加載器加載失敗,也就是說 JRE核心類中沒有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載。
- 如果本地目錄下沒有這個類,說明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載。這里請你注意,Web 應(yīng)用是通過Class.forName調(diào)用交給系統(tǒng)類加載器的,因為Class.forName的默認加載器就是系統(tǒng)類加載器。
- 如果上述加載過程全部失敗,拋出 ClassNotFound異常。
Tomcat 類加載器層次
Tomcat 作為 Servlet容器,它負責加載我們的 Servlet類,此外它還負責加載 Servlet所依賴的 JAR 包。并且 Tomcat本身也是也是一個 Java 程序,因此它需要加載自己的類和依賴的 JAR 包。首先讓我們思考這一下這幾個問題:
- 假如我們在 Tomcat 中運行了兩個 Web 應(yīng)用程序,兩個 Web 應(yīng)用中有同名的 Servlet,但是功能不同,Tomcat 需要同時加載和管理這兩個同名的 Servlet類,保證它們不會沖突,因此 Web 應(yīng)用之間的類需要隔離。
- 假如兩個 Web 應(yīng)用都依賴同一個第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加載到內(nèi)存后,Tomcat要保證這兩個 Web 應(yīng)用能夠共享,也就是說 Spring的 JAR 包只被加載一次,否則隨著依賴的第三方 JAR 包增多,JVM的內(nèi)存會膨脹。
- 跟 JVM 一樣,我們需要隔離 Tomcat 本身的類和 Web 應(yīng)用的類。

1. WebAppClassLoader
Tomcat 的解決方案是自定義一個類加載器 WebAppClassLoader, 并且給每個 Web 應(yīng)用創(chuàng)建一個類加載器實例。我們知道,Context 容器組件對應(yīng)一個 Web 應(yīng)用,因此,每個 Context容器負責創(chuàng)建和維護一個 WebAppClassLoader加載器實例。這背后的原理是,不同的加載器實例加載的類被認為是不同的類,即使它們的類名相同。這就相當于在 Java 虛擬機內(nèi)部創(chuàng)建了一個個相互隔離的 Java 類空間,每一個 Web 應(yīng)用都有自己的類空間,Web 應(yīng)用之間通過各自的類加載器互相隔離。
2.SharedClassLoader
本質(zhì)需求是兩個 Web 應(yīng)用之間怎么共享庫類,并且不能重復(fù)加載相同的類。在雙親委托機制里,各個子加載器都可以通過父加載器去加載類,那么把需要共享的類放到父加載器的加載路徑下不就行了嗎。
因此 Tomcat 的設(shè)計者又加了一個類加載器 SharedClassLoader,作為 WebAppClassLoader的父加載器,專門來加載 Web 應(yīng)用之間共享的類。如果 WebAppClassLoader自己沒有加載到某個類,就會委托父加載器 SharedClassLoader去加載這個類,SharedClassLoader會在指定目錄下加載共享類,之后返回給 WebAppClassLoader,這樣共享的問題就解決了。
3. CatalinaClassloader
如何隔離 Tomcat 本身的類和 Web 應(yīng)用的類?
要共享可以通過父子關(guān)系,要隔離那就需要兄弟關(guān)系了。兄弟關(guān)系就是指兩個類加載器是平行的,它們可能擁有同一個父加載器,基于此 Tomcat 又設(shè)計一個類加載器CatalinaClassloader,專門來加載 Tomcat 自身的類。
這樣設(shè)計有個問題,那 Tomcat 和各 Web 應(yīng)用之間需要共享一些類時該怎么辦呢?
老辦法,還是再增加一個 CommonClassLoader,作為 CatalinaClassloader和 SharedClassLoader的父加載器。CommonClassLoader能加載的類都可以被 CatalinaClassLoader和 SharedClassLoader使用
整體架構(gòu)設(shè)計解析收獲總結(jié)
通過前面對 Tomcat 整體架構(gòu)的學習,知道了 Tomcat 有哪些核心組件,組件之間的關(guān)系。以及 Tomcat 是怎么處理一個 HTTP 請求的。下面我們通過一張簡化的類圖來回顧一下,從圖上你可以看到各種組件的層次關(guān)系,圖中的虛線表示一個請求在 Tomcat 中流轉(zhuǎn)的過程。

Tomcat 整體組件關(guān)系
連接器
Tomcat 的整體架構(gòu)包含了兩個核心組件連接器和容器。連接器負責對外交流,容器負責內(nèi)部處理。連接器用 ProtocolHandler接口來封裝通信協(xié)議和 I/O模型的差異,ProtocolHandler內(nèi)部又分為 EndPoint和 Processor模塊,EndPoint負責底層Socket通信,Proccesor負責應(yīng)用層協(xié)議解析。連接器通過適配器 Adapter調(diào)用容器。
對 Tomcat 整體架構(gòu)的學習,我們可以得到一些設(shè)計復(fù)雜系統(tǒng)的基本思路。首先要分析需求,根據(jù)高內(nèi)聚低耦合的原則確定子模塊,然后找出子模塊中的變化點和不變點,用接口和抽象基類去封裝不變點,在抽象基類中定義模板方法,讓子類自行實現(xiàn)抽象方法,也就是具體子類去實現(xiàn)變化點。
容器
運用了組合模式 管理容器、通過 觀察者模式 發(fā)布啟動事件達到解耦、開閉原則。骨架抽象類和模板方法抽象變與不變,變化的交給子類實現(xiàn),從而實現(xiàn)代碼復(fù)用,以及靈活的拓展。使用責任鏈的方式處理請求,比如記錄日志等。
類加載器
Tomcat 的自定義類加載器 WebAppClassLoader為了隔離 Web 應(yīng)用打破了雙親委托機制,它首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。防止 Web 應(yīng)用自己的類覆蓋 JRE 的核心類,使用 ExtClassLoader 去加載,這樣即打破了雙親委派,又能安全加載。
如何閱讀源碼持續(xù)學習
學習是一個反人類的過程,是比較痛苦的。尤其學習我們常用的優(yōu)秀技術(shù)框架本身比較龐大,設(shè)計比較復(fù)雜,在學習初期很容易遇到 “挫折感”,debug 跳來跳去陷入恐怖細節(jié)之中無法自拔,往往就會放棄。
找到適合自己的學習方法非常重要,同樣關(guān)鍵的是要保持學習的興趣和動力,并且得到學習反饋效果。
學習優(yōu)秀源碼,我們收獲的就是架構(gòu)設(shè)計能力,遇到復(fù)雜需求我們學習到可以利用合理模式與組件抽象設(shè)計了可拓展性強的代碼能力。
如何閱讀
比如我最初在學習 Spring 框架的時候,一開始就鉆進某個模塊啃起來。然而由于 Spring 太龐大,模塊之間也有聯(lián)系,根本不明白為啥要這么寫,只覺得為啥設(shè)計這么 “繞”。
錯誤方式
- 陷入細節(jié),不看全局:我還沒弄清楚森林長啥樣,就盯著葉子看 ,看不到全貌和整體設(shè)計思路。所以閱讀源碼學習的時候不要一開始就進入細節(jié),而是宏觀看待整體架構(gòu)設(shè)計思想,模塊之間的關(guān)系。
- 還沒學會用就研究如何設(shè)計:首先基本上框架都運用了設(shè)計模式,我們最起碼也要了解常用的設(shè)計模式,即使是“背”,也得了然于胸。在學習一門技術(shù),我推薦先看官方文檔,看看有哪些模塊、整體設(shè)計思想。然后下載示例跑一遍,最后才是看源碼。
- 看源碼深究細節(jié):到了看具體某個模塊源碼的時候也要下意識的不要去深入細節(jié),重要的是學習設(shè)計思路,而不是具體一個方法實現(xiàn)邏輯。除非自己要基于源碼做二次開發(fā)。
正確方式
- 定焦原則:抓主線(抓住一個核心流程去分析,不要漫無目的的到處閱讀)。
- 宏觀思維:從全局的視角去看待,上帝視角理出主要核心架構(gòu)設(shè)計,先森林后樹葉。切勿不要試圖去搞明白每一行代碼。
- 斷點:合理運用調(diào)用棧(觀察調(diào)用過程上下文)。
帶著目標去學
比如某些知識點是面試的熱點,那學習目標就是徹底理解和掌握它,當被問到相關(guān)問題時,你的回答能夠使得面試官對你刮目相看,有時候往往憑著某一個亮點就能影響最后的錄用結(jié)果。
又或者接到一個稍微復(fù)雜的需求,學習從優(yōu)秀源碼中借鑒設(shè)計思路與優(yōu)化技巧。
最后就是動手實踐,將所學運用在工作項目中。只有動手實踐才會讓我們對技術(shù)有最直觀的感受。有時候我們聽別人講經(jīng)驗和理論,感覺似乎懂了,但是過一段時間便又忘記了。
實際場景運用
簡單的分析了 Tomcat 整體架構(gòu)設(shè)計,從 【連接器】 到 【容器】,并且分別細說了一些組件的設(shè)計思想以及設(shè)計模式。接下來就是如何學以致用,借鑒優(yōu)雅的設(shè)計運用到實際工作開發(fā)中。學習,從模仿開始。
責任鏈模式
在工作中,有這么一個需求,用戶可以輸入一些信息并可以選擇查驗該企業(yè)的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個或者多個模塊,而且模塊之間還有一些公共的東西是要各個模塊復(fù)用。
這里就像一個請求,會被多個模塊去處理。所以每個查詢模塊我們可以抽象為 處理閥門,使用一個 List 將這些 閥門保存起來,這樣新增模塊我們只需要新增一個閥門即可,實現(xiàn)了開閉原則,同時將一堆查驗的代碼解耦到不同的具體閥門中,使用抽象類提取 “不變的”功能。

具體示例代碼如下所示:
首先抽象我們的處理閥門, NetCheckDTO是請求信息
/**
* 責任鏈模式:處理每個模塊閥門
*/
public interface Valve {
/**
* 調(diào)用
* @param netCheckDTO
*/
void invoke(NetCheckDTO netCheckDTO);
}
定義抽象基類,復(fù)用代碼。
public abstract class AbstractCheckValve implements Valve {
public final AnalysisReportLogDO getLatestHistoryData(NetCheckDTO netCheckDTO, NetCheckDataTypeEnum checkDataTypeEnum){
// 獲取歷史記錄,省略代碼邏輯
}
// 獲取查驗數(shù)據(jù)源配置
public final String getModuleSource(String querySource, ModuleEnum moduleEnum){
// 省略代碼邏輯
}
}
定義具體每個模塊處理的業(yè)務(wù)邏輯,比如 【百度負面新聞】對應(yīng)的處理
@Slf4j
@Service
public class BaiduNegativeValve extends AbstractCheckValve {
@Override
public void invoke(NetCheckDTO netCheckDTO) {
}
}
最后就是管理用戶選擇要查驗的模塊,我們通過 List 保存。用于觸發(fā)所需要的查驗?zāi)K
@Slf4j
@Service
public class NetCheckService {
// 注入所有的閥門
@Autowired
private Map<String, Valve> valveMap;
/**
* 發(fā)送查驗請求
*
* @param netCheckDTO
*/
@Async("asyncExecutor")
public void sendCheckRequest(NetCheckDTO netCheckDTO) {
// 用于保存客戶選擇處理的模塊閥門
List<Valve> valves = new ArrayList<>();
CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig();
// 將用戶選擇查驗的模塊添加到 閥門鏈條中
if (checkModuleConfig.getBaiduNegative()) {
valves.add(valveMap.get("baiduNegativeValve"));
}
// 省略部分代碼.......
if (CollectionUtils.isEmpty(valves)) {
log.info("網(wǎng)查查驗?zāi)K為空,沒有需要查驗的任務(wù)");
return;
}
// 觸發(fā)處理
valves.forEach(valve -> valve.invoke(netCheckDTO));
}
}
模板方法模式
需求是這樣的,可根據(jù)客戶錄入的財報 Excel 數(shù)據(jù)或者企業(yè)名稱執(zhí)行財報分析。
對于非上市的則解析 excel -> 校驗數(shù)據(jù)是否合法->執(zhí)行計算。
上市企業(yè):判斷名稱是否存在 ,不存在則發(fā)送郵件并中止計算-> 從數(shù)據(jù)庫拉取財報數(shù)據(jù),初始化查驗日志、生成一條報告記錄,觸發(fā)計算-> 根據(jù)失敗與成功修改任務(wù)狀態(tài) 。

重要的 ”變“ 與 ”不變“,
- 不變的是整個流程是初始化查驗日志、初始化一條報告、前期校驗數(shù)據(jù)(若是上市公司校驗不通過還需要構(gòu)建郵件數(shù)據(jù)并發(fā)送)、從不同來源拉取財報數(shù)據(jù)并且適配通用數(shù)據(jù)、然后觸發(fā)計算,任務(wù)異常與成功都需要修改狀態(tài)。
- 變化的是上市與非上市校驗規(guī)則不一樣,獲取財報數(shù)據(jù)方式不一樣,兩種方式的財報數(shù)據(jù)需要適配
整個算法流程是固定的模板,但是需要將算法內(nèi)部變化的部分具體實現(xiàn)延遲到不同子類實現(xiàn),這正是模板方法模式的最佳場景。
public abstract class AbstractAnalysisTemplate {
/**
* 提交財報分析模板方法,定義骨架流程
* @param reportAnalysisRequest
* @return
*/
public final FinancialAnalysisResultDTO doProcess(FinancialReportAnalysisRequest reportAnalysisRequest) {
FinancialAnalysisResultDTO analysisDTO = new FinancialAnalysisResultDTO();
// 抽象方法:提交查驗的合法校驗
boolean prepareValidate = prepareValidate(reportAnalysisRequest, analysisDTO);
log.info("prepareValidate 校驗結(jié)果 = {} ", prepareValidate);
if (!prepareValidate) {
// 抽象方法:構(gòu)建通知郵件所需要的數(shù)據(jù)
buildEmailData(analysisDTO);
log.info("構(gòu)建郵件信息,data = {}", JSON.toJSONString(analysisDTO));
return analysisDTO;
}
String reportNo = FINANCIAL_REPORT_NO_PREFIX + reportAnalysisRequest.getUserId() + SerialNumGenerator.getFixLenthSerialNumber();
// 生成分析日志
initFinancialAnalysisLog(reportAnalysisRequest, reportNo);
// 生成分析記錄
initAnalysisReport(reportAnalysisRequest, reportNo);
try {
// 抽象方法:拉取財報數(shù)據(jù),不同子類實現(xiàn)
FinancialDataDTO financialData = pullFinancialData(reportAnalysisRequest);
log.info("拉取財報數(shù)據(jù)完成, 準備執(zhí)行計算");
// 測算指標
financialCalcContext.calc(reportAnalysisRequest, financialData, reportNo);
// 設(shè)置分析日志為成功
successCalc(reportNo);
} catch (Exception e) {
log.error("財報計算子任務(wù)出現(xiàn)異常", e);
// 設(shè)置分析日志失敗
failCalc(reportNo);
throw e;
}
return analysisDTO;
}
}
最后新建兩個子類繼承該模板,并實現(xiàn)抽象方法。這樣就將上市與非上市兩種類型的處理邏輯解耦,同時又復(fù)用了代碼。
策略模式
需求是這樣,要做一個萬能識別銀行流水的 excel 接口,假設(shè)標準流水包含【交易時間、收入、支出、交易余額、付款人賬號、付款人名字、收款人名稱、收款人賬號】等字段。現(xiàn)在我們解析出來每個必要字段所在 excel 表頭的下標。但是流水有多種情況:
- 一種就是包含所有標準字段。
- 收入、支出下標是同一列,通過正負來區(qū)分收入與支出。
- 收入與支出是同一列,有一個交易類型的字段來區(qū)分。
- 特殊銀行的特殊處理。
也就是我們要根據(jù)解析對應(yīng)的下標找到對應(yīng)的處理邏輯算法,我們可能在一個方法里面寫超多 if else 的代碼,整個流水處理都偶合在一起,假如未來再來一種新的流水類型,還要繼續(xù)改老代碼。最后可能出現(xiàn) “又臭又長,難以維護” 的代碼復(fù)雜度。
這個時候我們可以用到策略模式,將不同模板的流水使用不同的處理器處理,根據(jù)模板找到對應(yīng)的策略算法去處理。即使未來再加一種類型,我們只要新加一種處理器即可,高內(nèi)聚低耦合,且可拓展。

定義處理器接口,不同處理器去實現(xiàn)處理邏輯。將所有的處理器注入到BankFlowDataHandler 的data_processor_map中,根據(jù)不同的場景取出對已經(jīng)的處理器處理流水。
public interface DataProcessor {
/**
* 處理流水數(shù)據(jù)
* @param bankFlowTemplateDO 流水下標數(shù)據(jù)
* @param row
* @return
*/
BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO, List<String> row);
/**
* 是否支持處理該模板,不同類型的流水策略根據(jù)模板數(shù)據(jù)判斷是否支持解析
* @return
*/
boolean isSupport(BankFlowTemplateDO bankFlowTemplateDO);
}
// 處理器的上下文
@Service
@Slf4j
public class BankFlowDataContext {
// 將所有處理器注入到 map 中
@Autowired
private List<DataProcessor> processors;
// 找對對應(yīng)的處理器處理流水
public void process() {
DataProcessor processor = getProcessor(bankFlowTemplateDO);
for(DataProcessor processor :processors) {
if (processor.isSupport(bankFlowTemplateDO)) {
// row 就是一行流水數(shù)據(jù)
processor.doProcess(bankFlowTemplateDO, row);
break;
}
}
}
}
定義默認處理器,處理正常模板,新增模板只要新增處理器實現(xiàn) DataProcessor即可。
/**
* 默認處理器:正對規(guī)范流水模板
*
*/
@Component("defaultDataProcessor")
@Slf4j
public class DefaultDataProcessor implements DataProcessor {
@Override
public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) {
// 省略處理邏輯細節(jié)
return bankTransactionFlowDO;
}
@Override
public String strategy(BankFlowTemplateDO bankFlowTemplateDO) {
// 省略判斷是否支持解析該流水
boolean isDefault = true;
return isDefault;
}
}
通過策略模式,我們將不同處理邏輯分配到不同的處理類中,這樣完全解耦,便于拓展。
使用內(nèi)嵌 Tomcat 方式調(diào)試源代碼:GitHub: https://github.com/UniqueDong/tomcat-embedded