Tomcat 架構(gòu)原理解析到架構(gòu)設(shè)計(jì)借鑒
Tomcat 架構(gòu)原理解析到架構(gòu)設(shè)計(jì)借鑒
Tomcat 發(fā)展這么多年,已經(jīng)比較成熟穩(wěn)定。在如今『追新求快』的時(shí)代,Tomcat 作為 Java Web 開(kāi)發(fā)必備的工具似乎變成了『熟悉的陌生人』,難道說(shuō)如今就沒(méi)有必要深入學(xué)習(xí)它了么?學(xué)習(xí)它我們又有什么收獲呢?
靜下心來(lái),細(xì)細(xì)品味經(jīng)典的開(kāi)源作品 。提升我們的「內(nèi)功」,具體來(lái)說(shuō)就是學(xué)習(xí)大牛們?nèi)绾卧O(shè)計(jì)、架構(gòu)一個(gè)中間件系統(tǒng),并且讓這些經(jīng)驗(yàn)為我所用。
美好的事物往往是整潔而優(yōu)雅的。但這并不等于簡(jiǎn)單,而是要將復(fù)雜的系統(tǒng)分解成一個(gè)個(gè)小模塊,并且各個(gè)模塊的職責(zé)劃分也要清晰合理。
與此相反的是凌亂無(wú)序,比如你看到城中村一堆互相糾纏在一起的電線,可能會(huì)感到不適。維護(hù)的代碼一個(gè)類幾千行、一個(gè)方法好幾百行。方法之間相互耦合糅雜在一起,你可能會(huì)說(shuō) what the f*k!
學(xué)習(xí)目的
掌握 Tomcat 架構(gòu)設(shè)計(jì)與原理提高內(nèi)功
宏觀上看
Tomcat 作為一個(gè) 「Http 服務(wù)器 + Servlet 容器」,對(duì)我們屏蔽了應(yīng)用層協(xié)議和網(wǎng)絡(luò)通信細(xì)節(jié),給我們的是標(biāo)準(zhǔn)的 Request 和 Response 對(duì)象;對(duì)于具體的業(yè)務(wù)邏輯則作為變化點(diǎn),交給我們來(lái)實(shí)現(xiàn)。我們使用了SpringMVC 之類的框架,可是卻從來(lái)不需要考慮 TCP 連接、 Http 協(xié)議的數(shù)據(jù)處理與響應(yīng)。就是因?yàn)?Tomcat 已經(jīng)為我們做好了這些,我們只需要關(guān)注每個(gè)請(qǐng)求的具體業(yè)務(wù)邏輯。
微觀上看
Tomcat 內(nèi)部也隔離了變化點(diǎn)與不變點(diǎn),使用了組件化設(shè)計(jì),目的就是為了實(shí)現(xiàn)「俄羅斯套娃式」的高度定制化(組合模式),而每個(gè)組件的生命周期管理又有一些共性的東西,則被提取出來(lái)成為接口和抽象類,讓具體子類實(shí)現(xiàn)變化點(diǎn),也就是模板方法設(shè)計(jì)模式。
當(dāng)今流行的微服務(wù)也是這個(gè)思路,按照功能將單體應(yīng)用拆成「微服務(wù)」,拆分過(guò)程要將共性提取出來(lái),而這些共性就會(huì)成為核心的基礎(chǔ)服務(wù)或者通用庫(kù)?!钢信_(tái)」思想亦是如此。
設(shè)計(jì)模式往往就是封裝變化的一把利器,合理的運(yùn)用設(shè)計(jì)模式能讓我們的代碼與系統(tǒng)設(shè)計(jì)變得優(yōu)雅且整潔。
這就是學(xué)習(xí)優(yōu)秀開(kāi)源軟件能獲得的「內(nèi)功」,從不會(huì)過(guò)時(shí),其中的設(shè)計(jì)思想與哲學(xué)才是根本之道。從中借鑒設(shè)計(jì)經(jīng)驗(yàn),合理運(yùn)用設(shè)計(jì)模式封裝變與不變,更能從它們的源碼中汲取經(jīng)驗(yàn),提升自己的系統(tǒng)設(shè)計(jì)能力。
宏觀理解一個(gè)請(qǐng)求如何與 Spring 聯(lián)系起來(lái)
在工作過(guò)程中,我們對(duì) Java 語(yǔ)法已經(jīng)很熟悉了,甚至「背」過(guò)一些設(shè)計(jì)模式,用過(guò)很多 Web 框架,但是很少有機(jī)會(huì)將他們用到實(shí)際項(xiàng)目中,讓自己獨(dú)立設(shè)計(jì)一個(gè)系統(tǒng)似乎也是根據(jù)需求一個(gè)個(gè) Service 實(shí)現(xiàn)而已。腦子里似乎沒(méi)有一張 Java Web 開(kāi)發(fā)全景圖,比如我并不知道瀏覽器的請(qǐng)求是怎么跟 Spring 中的代碼聯(lián)系起來(lái)的。
為了突破這個(gè)瓶頸,為何不站在巨人的肩膀上學(xué)習(xí)優(yōu)秀的開(kāi)源系統(tǒng),看大牛們是如何思考這些問(wèn)題。
學(xué)習(xí) Tomcat 的原理,我發(fā)現(xiàn) Servlet 技術(shù)是 Web 開(kāi)發(fā)的原點(diǎn),幾乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet 的封裝,Spring 應(yīng)用本身就是一個(gè) Servlet(DispatchSevlet),而 Tomcat 和 Jetty 這樣的 Web 容器,負(fù)責(zé)加載和運(yùn)行 Servlet。如圖所示:
提升自己的系統(tǒng)設(shè)計(jì)能力
學(xué)習(xí) Tomcat ,我還發(fā)現(xiàn)用到不少 Java 高級(jí)技術(shù),比如 Java 多線程并發(fā)編程、Socket 網(wǎng)絡(luò)編程以及反射等。之前也只是了解這些技術(shù),為了面試也背過(guò)一些題。但是總感覺(jué)「知道」與會(huì)用之間存在一道溝壑,通過(guò)對(duì) Tomcat 源碼學(xué)習(xí),我學(xué)會(huì)了什么場(chǎng)景去使用這些技術(shù)。
還有就是系統(tǒng)設(shè)計(jì)能力,比如面向接口編程、組件化組合模式、骨架抽象類、一鍵式啟停、對(duì)象池技術(shù)以及各種設(shè)計(jì)模式,比如模板方法、觀察者模式、責(zé)任鏈模式等,之后我也開(kāi)始模仿它們并把這些設(shè)計(jì)思想運(yùn)用到實(shí)際的工作中。
整體架構(gòu)設(shè)計(jì)
今天咱們就來(lái)一步一步分析 Tomcat 的設(shè)計(jì)思路,一方面我們可以學(xué)到 Tomcat 的總體架構(gòu),學(xué)會(huì)從宏觀上怎么去設(shè)計(jì)一個(gè)復(fù)雜系統(tǒng),怎么設(shè)計(jì)頂層模塊,以及模塊之間的關(guān)系;另一方面也為我們深入學(xué)習(xí) Tomcat 的工作原理打下基礎(chǔ)。
Tomcat 啟動(dòng)流程:startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()
Tomcat 實(shí)現(xiàn)的 2 個(gè)核心功能:
- 處理 Socket 連接,負(fù)責(zé)網(wǎng)絡(luò)字節(jié)流與 Request 和 Response 對(duì)象的轉(zhuǎn)化。
- 加載并管理 Servlet ,以及處理具體的 Request 請(qǐng)求。
所以 Tomcat 設(shè)計(jì)了兩個(gè)核心組件連接器(Connector)和容器(Container)。連接器負(fù)責(zé)對(duì)外交流,容器負(fù)責(zé)內(nèi)部 處理
Tomcat為了實(shí)現(xiàn)支持多種 I/O 模型和應(yīng)用層協(xié)議,一個(gè)容器可能對(duì)接多個(gè)連接器,就好比一個(gè)房間有多個(gè)門(mén)。
Tomcat整體架構(gòu)
- Server 對(duì)應(yīng)的就是一個(gè) Tomcat 實(shí)例。
- Service 默認(rèn)只有一個(gè),也就是一個(gè) Tomcat 實(shí)例默認(rèn)一個(gè) Service。
- Connector:一個(gè) Service 可能多個(gè) 連接器,接受不同連接協(xié)議。
- Container: 多個(gè)連接器對(duì)應(yīng)一個(gè)容器,頂層容器其實(shí)就是 Engine。
每個(gè)組件都有對(duì)應(yīng)的生命周期,需要啟動(dòng),同時(shí)還要啟動(dòng)自己內(nèi)部的子組件,比如一個(gè) Tomcat 實(shí)例包含一個(gè) Service,一個(gè) Service 包含多個(gè)連接器和一個(gè)容器。而一個(gè)容器包含多個(gè) Host, Host 內(nèi)部可能有多個(gè) Contex t 容器,而一個(gè) Context 也會(huì)包含多個(gè) Servlet,所以 Tomcat 利用組合模式管理組件每個(gè)組件,對(duì)待過(guò)個(gè)也想對(duì)待單個(gè)組一樣對(duì)待。整體上每個(gè)組件設(shè)計(jì)就像是「俄羅斯套娃」一樣。
連接器
在開(kāi)始講連接器前,我先鋪墊一下 Tomcat支持的多種 I/O 模型和應(yīng)用層協(xié)議。
Tomcat支持的 I/O 模型有:
- NIO:非阻塞 I/O,采用 Java NIO 類庫(kù)實(shí)現(xiàn)。
- NIO2:異步I/O,采用 JDK 7 最新的 NIO2 類庫(kù)實(shí)現(xiàn)。
- APR:采用 Apache可移植運(yùn)行庫(kù)實(shí)現(xiàn),是 C/C++ 編寫(xiě)的本地庫(kù)。
Tomcat 支持的應(yīng)用層協(xié)議有:
- HTTP/1.1:這是大部分 Web 應(yīng)用采用的訪問(wèn)協(xié)議。
- AJP:用于和 Web 服務(wù)器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
所以一個(gè)容器可能對(duì)接多個(gè)連接器。連接器對(duì) Servlet 容器屏蔽了網(wǎng)絡(luò)協(xié)議與 I/O 模型的區(qū)別,無(wú)論是 Http 還是 AJP,在容器中獲取到的都是一個(gè)標(biāo)準(zhǔn)的 ServletRequest 對(duì)象。
細(xì)化連接器的功能需求就是:
- 監(jiān)聽(tīng)網(wǎng)絡(luò)端口。
- 接受網(wǎng)絡(luò)連接請(qǐng)求。
- 讀取請(qǐng)求網(wǎng)絡(luò)字節(jié)流。
- 根據(jù)具體應(yīng)用層協(xié)議(HTTP/AJP)解析字節(jié)流,生成統(tǒng)一的 Tomcat Request 對(duì)象。
- 將 Tomcat Request 對(duì)象轉(zhuǎn)成標(biāo)準(zhǔn)的 ServletRequest。
- 調(diào)用 Servlet容器,得到 ServletResponse。
- 將 ServletResponse轉(zhuǎn)成 Tomcat Response 對(duì)象。
- 將 Tomcat Response 轉(zhuǎn)成網(wǎng)絡(luò)字節(jié)流。
- 將響應(yīng)字節(jié)流寫(xiě)回給瀏覽器。
需求列清楚后,我們要考慮的下一個(gè)問(wèn)題是,連接器應(yīng)該有哪些子模塊??jī)?yōu)秀的模塊化設(shè)計(jì)應(yīng)該考慮高內(nèi)聚、低耦合。
- 高內(nèi)聚是指相關(guān)度比較高的功能要盡可能集中,不要分散。
- 低耦合是指兩個(gè)相關(guān)的模塊要盡可能減少依賴的部分和降低依賴的程度,不要讓兩個(gè)模塊產(chǎn)生強(qiáng)依賴。
我們發(fā)現(xiàn)連接器需要完成 3 個(gè)高內(nèi)聚的功能:
- 網(wǎng)絡(luò)通信。
- 應(yīng)用層協(xié)議解析。
- Tomcat Request/Response 與 ServletRequest/ServletResponse 的轉(zhuǎn)化。
因此 Tomcat 的設(shè)計(jì)者設(shè)計(jì)了 3 個(gè)組件來(lái)實(shí)現(xiàn)這 3 個(gè)功能,分別是 EndPoint、Processor 和 Adapter。
網(wǎng)絡(luò)通信的 I/O 模型是變化的, 應(yīng)用層協(xié)議也是變化的,但是整體的處理邏輯是不變的,EndPoint 負(fù)責(zé)提供字節(jié)流給 Processor,Processor負(fù)責(zé)提供 Tomcat Request 對(duì)象給 Adapter,Adapter負(fù)責(zé)提供 ServletRequest對(duì)象給容器。
封裝變與不變
因此 Tomcat 設(shè)計(jì)了一系列抽象基類來(lái)封裝這些穩(wěn)定的部分,抽象基類 AbstractProtocol實(shí)現(xiàn)了 ProtocolHandler接口。每一種應(yīng)用層協(xié)議有自己的抽象基類,比如 AbstractAjpProtocol和 AbstractHttp11Protocol,具體協(xié)議的實(shí)現(xiàn)類擴(kuò)展了協(xié)議層抽象基類。
這就是模板方法設(shè)計(jì)模式的運(yùn)用。
應(yīng)用層協(xié)議抽象
總結(jié)下來(lái),連接器的三個(gè)核心組件 Endpoint、Processor和 Adapter來(lái)分別做三件事情,其中 Endpoint和 Processor放在一起抽象成了 ProtocolHandler組件,它們的關(guān)系如下圖所示。
連接器
ProtocolHandler 組件
主要處理 網(wǎng)絡(luò)連接 和 應(yīng)用層協(xié)議 ,包含了兩個(gè)重要部件 EndPoint 和 Processor,兩個(gè)組件組合形成 ProtocoHandler,下面我來(lái)詳細(xì)介紹它們的工作原理。
EndPoint
EndPoint是通信端點(diǎn),即通信監(jiān)聽(tīng)的接口,是具體的 Socket 接收和發(fā)送處理器,是對(duì)傳輸層的抽象,因此 EndPoint是用來(lái)實(shí)現(xiàn) TCP/IP 協(xié)議數(shù)據(jù)讀寫(xiě)的,本質(zhì)調(diào)用操作系統(tǒng)的 socket 接口。
EndPoint是一個(gè)接口,對(duì)應(yīng)的抽象實(shí)現(xiàn)類是 AbstractEndpoint,而 AbstractEndpoint的具體子類,比如在 NioEndpoint和 Nio2Endpoint中,有兩個(gè)重要的子組件:Acceptor和 SocketProcessor。
其中 Acceptor 用于監(jiān)聽(tīng) Socket 連接請(qǐng)求。SocketProcessor用于處理 Acceptor 接收到的 Socket請(qǐng)求,它實(shí)現(xiàn) Runnable接口,在 Run方法里調(diào)用應(yīng)用層協(xié)議處理組件 Processor 進(jìn)行處理。為了提高處理能力,SocketProcessor被提交到線程池來(lái)執(zhí)行。
我們知道,對(duì)于 Java 的多路復(fù)用器的使用,無(wú)非是兩步:
- 創(chuàng)建一個(gè) Seletor,在它身上注冊(cè)各種感興趣的事件,然后調(diào)用 select 方法,等待感興趣的事情發(fā)生。
- 感興趣的事情發(fā)生了,比如可以讀了,這時(shí)便創(chuàng)建一個(gè)新的線程從 Channel 中讀數(shù)據(jù)。
在 Tomcat 中 NioEndpoint 則是 AbstractEndpoint 的具體實(shí)現(xiàn),里面組件雖然很多,但是處理邏輯還是前面兩步。它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor和 Executor 共 5 個(gè)組件,分別分工合作實(shí)現(xiàn)整個(gè) TCP/IP 協(xié)議的處理。
- LimitLatch 是連接控制器,它負(fù)責(zé)控制最大連接數(shù),NIO 模式下默認(rèn)是 10000,達(dá)到這個(gè)閾值后,連接請(qǐng)求被拒絕。
- Acceptor跑在一個(gè)單獨(dú)的線程里,它在一個(gè)死循環(huán)里調(diào)用 accept方法來(lái)接收新連接,一旦有新的連接請(qǐng)求到來(lái),accept方法返回一個(gè) Channel 對(duì)象,接著把 Channel對(duì)象交給 Poller 去處理。
- Poller 的本質(zhì)是一個(gè) Selector,也跑在單獨(dú)線程里。Poller在內(nèi)部維護(hù)一個(gè) Channel數(shù)組,它在一個(gè)死循環(huán)里不斷檢測(cè) Channel的數(shù)據(jù)就緒狀態(tài),一旦有 Channel可讀,就生成一個(gè) SocketProcessor任務(wù)對(duì)象扔給 Executor去處理。
- SocketProcessor 實(shí)現(xiàn)了 Runnable 接口,其中 run 方法中的 getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); 代碼則是獲取 handler 并執(zhí)行處理 socketWrapper,最后通過(guò) socket 獲取合適應(yīng)用層協(xié)議處理器,也就是調(diào)用 Http11Processor 組件來(lái)處理請(qǐng)求。Http11Processor 讀取 Channel 的數(shù)據(jù)來(lái)生成 ServletRequest 對(duì)象,Http11Processor 并不是直接讀取 Channel 的。這是因?yàn)?Tomcat 支持同步非阻塞 I/O 模型和異步 I/O 模型,在 Java API 中,相應(yīng)的 Channel 類也是不一樣的,比如有 AsynchronousSocketChannel 和 SocketChannel,為了對(duì) Http11Processor 屏蔽這些差異,Tomcat 設(shè)計(jì)了一個(gè)包裝類叫作 SocketWrapper,Http11Processor 只調(diào)用 SocketWrapper 的方法去讀寫(xiě)數(shù)據(jù)。
- Executor就是線程池,負(fù)責(zé)運(yùn)行 SocketProcessor任務(wù)類,SocketProcessor 的 run方法會(huì)調(diào)用 Http11Processor 來(lái)讀取和解析請(qǐng)求數(shù)據(jù)。我們知道,Http11Processor是應(yīng)用層協(xié)議的封裝,它會(huì)調(diào)用容器獲得響應(yīng),再把響應(yīng)通過(guò) Channel寫(xiě)出。
工作流程如下所示:
NioEndPoint
Processor
Processor 用來(lái)實(shí)現(xiàn) HTTP 協(xié)議,Processor 接收來(lái)自 EndPoint 的 Socket,讀取字節(jié)流解析成 Tomcat Request 和 Response 對(duì)象,并通過(guò) Adapter 將其提交到容器處理,Processor 是對(duì)應(yīng)用層協(xié)議的抽象。
從圖中我們看到,EndPoint 接收到 Socket 連接后,生成一個(gè) SocketProcessor 任務(wù)提交到線程池去處理,SocketProcessor 的 Run 方法會(huì)調(diào)用 HttpProcessor 組件去解析應(yīng)用層協(xié)議,Processor 通過(guò)解析生成 Request 對(duì)象后,會(huì)調(diào)用 Adapter 的 Service 方法,方法內(nèi)部通過(guò) 以下代碼將請(qǐng)求傳遞到容器中。
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Adapter 組件
由于協(xié)議的不同,Tomcat 定義了自己的 Request 類來(lái)存放請(qǐng)求信息,這里其實(shí)體現(xiàn)了面向?qū)ο蟮乃季S。但是這個(gè) Request 不是標(biāo)準(zhǔn)的 ServletRequest ,所以不能直接使用 Tomcat 定義 Request 作為參數(shù)直接容器。
Tomcat 設(shè)計(jì)者的解決方案是引入 CoyoteAdapter,這是適配器模式的經(jīng)典運(yùn)用,連接器調(diào)用 CoyoteAdapter 的 Sevice 方法,傳入的是 Tomcat Request 對(duì)象,CoyoteAdapter負(fù)責(zé)將 Tomcat Request 轉(zhuǎn)成 ServletRequest,再調(diào)用容器的 Service方法。
容器
連接器負(fù)責(zé)外部交流,容器負(fù)責(zé)內(nèi)部處理。具體來(lái)說(shuō)就是,連接器處理 Socket 通信和應(yīng)用層協(xié)議的解析,得到 Servlet請(qǐng)求;而容器則負(fù)責(zé)處理 Servlet請(qǐng)求。
容器:顧名思義就是拿來(lái)裝東西的, 所以 Tomcat 容器就是拿來(lái)裝載 Servlet。
Tomcat 設(shè)計(jì)了 4 種容器,分別是 Engine、Host、Context和 Wrapper。Server 代表 Tomcat 實(shí)例。
要注意的是這 4 種容器不是平行關(guān)系,屬于父子關(guān)系,如下圖所示:
容器
你可能會(huì)問(wèn),為啥要設(shè)計(jì)這么多層次的容器,這不是增加復(fù)雜度么?其實(shí)這背后的考慮是,Tomcat 通過(guò)一種分層的架構(gòu),使得 Servlet 容器具有很好的靈活性。因?yàn)檫@里正好符合一個(gè) Host 多個(gè) Context, 一個(gè) Context 也包含多個(gè) Servlet,而每個(gè)組件都需要統(tǒng)一生命周期管理,所以組合模式設(shè)計(jì)這些容器
Wrapper 表示一個(gè) Servlet ,Context 表示一個(gè) Web 應(yīng)用程序,而一個(gè) Web 程序可能有多個(gè) Servlet ;Host 表示一個(gè)虛擬主機(jī),或者說(shuō)一個(gè)站點(diǎn),一個(gè) Tomcat 可以配置多個(gè)站點(diǎn)(Host);一個(gè)站點(diǎn)( Host) 可以部署多個(gè) Web 應(yīng)用;Engine 代表 引擎,用于管理多個(gè)站點(diǎn)(Host),一個(gè) Service 只能有 一個(gè) Engine。
可通過(guò) Tomcat 配置文件加深對(duì)其層次關(guān)系理解。
<Server port="8005" shutdown="SHUTDOWN"> // 頂層組件,可包含多個(gè) Service,代表一個(gè) Tomcat 實(shí)例
<Service name="Catalina"> // 頂層組件,包含一個(gè) Engine ,多個(gè)連接器
<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" /> // 連接器
// 容器組件:一個(gè) Engine 處理 Service 所有請(qǐng)求,包含多個(gè) Host
<Engine name="Catalina" defaultHost="localhost">
// 容器組件:處理指定Host下的客戶端請(qǐng)求, 可包含多個(gè) Context
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
// 容器組件:處理特定 Context Web應(yīng)用的所有客戶端請(qǐng)求
<Context></Context>
</Host>
</Engine>
</Service>
</Server>
如何管理這些容器?我們發(fā)現(xiàn)容器之間具有父子關(guān)系,形成一個(gè)樹(shù)形結(jié)構(gòu),是不是想到了設(shè)計(jì)模式中的 組合模式 。
Tomcat 就是用組合模式來(lái)管理這些容器的。具體實(shí)現(xiàn)方法是,所有容器組件都實(shí)現(xiàn)了 Container接口,因此組合模式可以使得用戶對(duì)單容器對(duì)象和組合容器對(duì)象的使用具有一致性。這里單容器對(duì)象指的是最底層的 Wrapper,組合容器對(duì)象指的是上面的 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等方法,這里正好驗(yàn)證了我們說(shuō)的組合模式。我們還看到 Container接口拓展了 Lifecycle ,Tomcat 就是通過(guò) Lifecycle 統(tǒng)一管理所有容器的組件的生命周期。通過(guò)組合模式管理所有容器,拓展 Lifecycle 實(shí)現(xiàn)對(duì)每個(gè)組件的生命周期管理 ,Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()。
請(qǐng)求定位 Servlet 的過(guò)程
一個(gè)請(qǐng)求是如何定位到讓哪個(gè) Wrapper 的 Servlet 處理的?答案是,Tomcat 是用 Mapper 組件來(lái)完成這個(gè)任務(wù)的。
Mapper 組件的功能就是將用戶請(qǐng)求的 URL 定位到一個(gè) Servlet,它的工作原理是:Mapper組件里保存了 Web 應(yīng)用的配置信息,其實(shí)就是容器組件與訪問(wèn)路徑的映射關(guān)系,比如 Host容器里配置的域名、Context容器里的 Web應(yīng)用路徑,以及 Wrapper容器里 Servlet 映射的路徑,你可以想象這些配置信息就是一個(gè)多層次的 Map。
當(dāng)一個(gè)請(qǐng)求到來(lái)時(shí),Mapper 組件通過(guò)解析請(qǐng)求 URL 里的域名和路徑,再到自己保存的 Map 里去查找,就能定位到一個(gè) Servlet。請(qǐng)你注意,一個(gè)請(qǐng)求 URL 最后只會(huì)定位到一個(gè) Wrapper容器,也就是一個(gè) Servlet。
假如有用戶訪問(wèn)一個(gè) URL,比如圖中的http://user.shopping.com:8080/order/buy,Tomcat 如何將這個(gè) URL 定位到一個(gè) Servlet 呢?
- 首先根據(jù)協(xié)議和端口號(hào)確定 Service 和 Engine。Tomcat 默認(rèn)的 HTTP 連接器監(jiān)聽(tīng) 8080 端口、默認(rèn)的 AJP 連接器監(jiān)聽(tīng) 8009 端口。上面例子中的 URL 訪問(wèn)的是 8080 端口,因此這個(gè)請(qǐng)求會(huì)被 HTTP 連接器接收,而一個(gè)連接器是屬于一個(gè) Service 組件的,這樣 Service 組件就確定了。我們還知道一個(gè) Service 組件里除了有多個(gè)連接器,還有一個(gè)容器組件,具體來(lái)說(shuō)就是一個(gè) Engine 容器,因此 Service 確定了也就意味著 Engine 也確定了。
- 根據(jù)域名選定 Host。 Service 和 Engine 確定后,Mapper 組件通過(guò) URL 中的域名去查找相應(yīng)的 Host 容器,比如例子中的 URL 訪問(wèn)的域名是
user.shopping.com
,因此 Mapper 會(huì)找到 Host2 這個(gè)容器。 - 根據(jù) URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據(jù) URL 的路徑來(lái)匹配相應(yīng)的 Web 應(yīng)用的路徑,比如例子中訪問(wèn)的是 /order,因此找到了 Context4 這個(gè) Context 容器。
- 根據(jù) URL 路徑找到 Wrapper(Servlet)。 Context 確定后,Mapper 再根據(jù) web.xml 中配置的 Servlet 映射路徑來(lái)找到具體的 Wrapper 和 Servlet。
連接器中的 Adapter 會(huì)調(diào)用容器的 Service 方法來(lái)執(zhí)行 Servlet,最先拿到請(qǐng)求的是 Engine 容器,Engine 容器對(duì)請(qǐng)求做一些處理后,會(huì)把請(qǐng)求傳給自己子容器 Host 繼續(xù)處理,依次類推,最后這個(gè)請(qǐng)求會(huì)傳給 Wrapper 容器,Wrapper 會(huì)調(diào)用最終的 Servlet 來(lái)處理。那么這個(gè)調(diào)用過(guò)程具體是怎么實(shí)現(xiàn)的呢?答案是使用 Pipeline-Valve 管道。
Pipeline-Valve 是責(zé)任鏈模式,責(zé)任鏈模式是指在一個(gè)請(qǐng)求處理的過(guò)程中有很多處理者依次對(duì)請(qǐng)求進(jìn)行處理,每個(gè)處理者負(fù)責(zé)做自己相應(yīng)的處理,處理完之后將再調(diào)用下一個(gè)處理者繼續(xù)處理,Valve 表示一個(gè)處理點(diǎn)(也就是一個(gè)處理閥門(mén)),因此 invoke方法就是來(lái)處理請(qǐng)求的。
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 中維護(hù)了 Valve鏈表,Valve可以插入到 Pipeline中,對(duì)請(qǐng)求做某些處理。我們還發(fā)現(xiàn) Pipeline 中沒(méi)有 invoke 方法,因?yàn)檎麄€(gè)調(diào)用鏈的觸發(fā)是 Valve 來(lái)完成的,Valve完成自己的處理后,調(diào)用 getNext.invoke() 來(lái)觸發(fā)下一個(gè) Valve 調(diào)用。
其實(shí)每個(gè)容器都有一個(gè) Pipeline 對(duì)象,只要觸發(fā)了這個(gè) Pipeline 的第一個(gè) Valve,這個(gè)容器里 Pipeline中的 Valve 就都會(huì)被調(diào)用到。但是,不同容器的 Pipeline 是怎么鏈?zhǔn)接|發(fā)的呢,比如 Engine 中 Pipeline 需要調(diào)用下層容器 Host 中的 Pipeline。
這是因?yàn)?nbsp;Pipeline中還有個(gè) getBasic方法。這個(gè) BasicValve處于 Valve鏈表的末端,它是 Pipeline中必不可少的一個(gè) Valve,負(fù)責(zé)調(diào)用下層容器的 Pipeline 里的第一個(gè) Valve。
整個(gè)過(guò)程分是通過(guò)連接器中的 CoyoteAdapter 觸發(fā),它會(huì)調(diào)用 Engine 的第一個(gè) 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 容器的最后一個(gè) Valve 會(huì)創(chuàng)建一個(gè) Filter 鏈,并調(diào)用 doFilter() 方法,最終會(huì)調(diào)到 Servlet的 service方法。
前面我們不是講到了 Filter,似乎也有相似的功能,那 Valve 和 Filter有什么區(qū)別嗎?它們的區(qū)別是:
- Valve是 Tomcat的私有機(jī)制,與 Tomcat 的基礎(chǔ)架構(gòu) API是緊耦合的。Servlet API是公有的標(biāo)準(zhǔn),所有的 Web 容器包括 Jetty 都支持 Filter 機(jī)制。
- 另一個(gè)重要的區(qū)別是 Valve工作在 Web 容器級(jí)別,攔截所有應(yīng)用的請(qǐng)求;而 Servlet Filter 工作在應(yīng)用級(jí)別,只能攔截某個(gè) Web 應(yīng)用的所有請(qǐng)求。如果想做整個(gè) Web容器的攔截器,必須通過(guò) Valve來(lái)實(shí)現(xiàn)。
Lifecycle 生命周期
前面我們看到 Container容器 繼承了 Lifecycle 生命周期。如果想讓一個(gè)系統(tǒng)能夠?qū)ν馓峁┓?wù),我們需要?jiǎng)?chuàng)建、組裝并啟動(dòng)這些組件;在服務(wù)停止的時(shí)候,我們還需要釋放資源,銷毀這些組件,因此這是一個(gè)動(dòng)態(tài)的過(guò)程。也就是說(shuō),Tomcat 需要?jiǎng)討B(tài)地管理這些組件的生命周期。
如何統(tǒng)一管理組件的創(chuàng)建、初始化、啟動(dòng)、停止和銷毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動(dòng)和停止不遺漏、不重復(fù)?
一鍵式啟停:LifeCycle 接口
設(shè)計(jì)就是要找到系統(tǒng)的變化點(diǎn)和不變點(diǎn)。這里的不變點(diǎn)就是每個(gè)組件都要經(jīng)歷創(chuàng)建、初始化、啟動(dòng)這幾個(gè)過(guò)程,這些狀態(tài)以及狀態(tài)的轉(zhuǎn)化是不變的。而變化點(diǎn)是每個(gè)具體組件的初始化方法,也就是啟動(dòng)方法是不一樣的。
因此,Tomcat 把不變點(diǎn)抽象出來(lái)成為一個(gè)接口,這個(gè)接口跟生命周期有關(guān),叫作 LifeCycle。LifeCycle 接口里定義這么幾個(gè)方法:init()、start()、stop() 和 destroy(),每個(gè)具體的組件(也就是容器)去實(shí)現(xiàn)這些方法。
在父組件的 init() 方法里需要?jiǎng)?chuàng)建子組件并調(diào)用子組件的 init() 方法。同樣,在父組件的 start()方法里也需要調(diào)用子組件的 start() 方法,因此調(diào)用者可以無(wú)差別的調(diào)用各組件的 init() 方法和 start() 方法,這就是組合模式的使用,并且只要調(diào)用最頂層組件,也就是 Server 組件的 init()和start() 方法,整個(gè) Tomcat 就被啟動(dòng)起來(lái)了。所以 Tomcat 采取組合模式管理容器,容器繼承 LifeCycle 接口,這樣就可以向針對(duì)單個(gè)對(duì)象一樣一鍵管理各個(gè)容器的生命周期,整個(gè) Tomcat 就啟動(dòng)起來(lái)。
可擴(kuò)展性:LifeCycle 事件
我們?cè)賮?lái)考慮另一個(gè)問(wèn)題,那就是系統(tǒng)的可擴(kuò)展性。因?yàn)楦鱾€(gè)組件init() 和 start() 方法的具體實(shí)現(xiàn)是復(fù)雜多變的,比如在 Host 容器的啟動(dòng)方法里需要掃描 webapps 目錄下的 Web 應(yīng)用,創(chuàng)建相應(yīng)的 Context 容器,如果將來(lái)需要增加新的邏輯,直接修改start() 方法?這樣會(huì)違反開(kāi)閉原則,那如何解決這個(gè)問(wèn)題呢?開(kāi)閉原則說(shuō)的是為了擴(kuò)展系統(tǒng)的功能,你不能直接修改系統(tǒng)中已有的類,但是你可以定義新的類。
組件的 init() 和 start() 調(diào)用是由它的父組件的狀態(tài)變化觸發(fā)的,上層組件的初始化會(huì)觸發(fā)子組件的初始化,上層組件的啟動(dòng)會(huì)觸發(fā)子組件的啟動(dòng),因此我們把組件的生命周期定義成一個(gè)個(gè)狀態(tài),把狀態(tài)的轉(zhuǎn)變看作是一個(gè)事件。而事件是有監(jiān)聽(tīng)器的,在監(jiān)聽(tīng)器里可以實(shí)現(xiàn)一些邏輯,并且監(jiān)聽(tīng)器也可以方便的添加和刪除,這就是典型的觀察者模式。
以下就是 Lyfecycle 接口的定義:
Lyfecycle
重用性:LifeCycleBase 抽象基類
再次看到抽象模板設(shè)計(jì)模式。
有了接口,我們就要用類去實(shí)現(xiàn)接口。一般來(lái)說(shuō)實(shí)現(xiàn)類不止一個(gè),不同的類在實(shí)現(xiàn)接口時(shí)往往會(huì)有一些相同的邏輯,如果讓各個(gè)子類都去實(shí)現(xiàn)一遍,就會(huì)有重復(fù)代碼。那子類如何重用這部分邏輯呢?其實(shí)就是定義一個(gè)基類來(lái)實(shí)現(xiàn)共同的邏輯,然后讓各個(gè)子類去繼承它,就達(dá)到了重用的目的。
Tomcat 定義一個(gè)基類 LifeCycleBase 來(lái)實(shí)現(xiàn) LifeCycle 接口,把一些公共的邏輯放到基類中去,比如生命狀態(tài)的轉(zhuǎn)變與維護(hù)、生命事件的觸發(fā)以及監(jiān)聽(tīng)器的添加和刪除等,而子類就負(fù)責(zé)實(shí)現(xiàn)自己的初始化、啟動(dòng)和停止等方法。
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);
}
}
// 模板方法定義整個(gè)啟動(dòng)流程,啟動(dòng)所有容器
@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)聽(tīng)器
setStateInternal(LifecycleState.INITIALIZING, null, false);
// 3. 調(diào)用具體子類的初始化方法
initInternal();
// 4. 觸發(fā) INITIALIZED 事件的監(jiān)聽(tīng)器
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 為了實(shí)現(xiàn)一鍵式啟停以及優(yōu)雅的生命周期管理,并考慮到了可擴(kuò)展性和可重用性,將面向?qū)ο笏枷牒驮O(shè)計(jì)模式發(fā)揮到了極致,Containaer接口維護(hù)了容器的父子關(guān)系,Lifecycle 組合模式實(shí)現(xiàn)組件的生命周期維護(hù),生命周期每個(gè)組件有變與不變的點(diǎn),運(yùn)用模板方法模式。分別運(yùn)用了組合模式、觀察者模式、骨架抽象類和模板方法。
如果你需要維護(hù)一堆具有父子關(guān)系的實(shí)體,可以考慮使用組合模式。
觀察者模式聽(tīng)起來(lái) “高大上”,其實(shí)就是當(dāng)一個(gè)事件發(fā)生后,需要執(zhí)行一連串更新操作。實(shí)現(xiàn)了低耦合、非侵入式的通知與更新機(jī)制。
Container 繼承了 LifeCycle,StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應(yīng)容器組件的具體實(shí)現(xiàn)類,因?yàn)樗鼈兌际侨萜鳎岳^承了 ContainerBase 抽象基類,而 ContainerBase 實(shí)現(xiàn)了 Container 接口,也繼承了 LifeCycleBase 類,它們的生命周期管理接口和功能接口是分開(kāi)的,這也符合設(shè)計(jì)中接口分離的原則。
Tomcat 為何打破雙親委派機(jī)制
雙親委派
我們知道 JVM的類加載器加載 Class 的時(shí)候基于雙親委派機(jī)制,也就是會(huì)將加載交給自己的父加載器加載,如果 父加載器為空則查找Bootstrap 是否加載過(guò),當(dāng)無(wú)法加載的時(shí)候才讓自己加載。JDK 提供一個(gè)抽象類 ClassLoader,這個(gè)抽象類中定義了三個(gè)關(guān)鍵方法。對(duì)外使用loadClass(String name) 用于子類重寫(xiě)打破雙親委派: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)被加載過(guò)
Class<?> c = findLoadedClass(name);
// 如果沒(méi)有加載過(guò)
if (c == null) {
// 委托給父加載器去加載,遞歸調(diào)用
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加載器為空,查找 Bootstrap 是否加載過(guò)
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 對(duì)象
return defineClass(buf, off, len);
}
// 將字節(jié)碼數(shù)組解析成一個(gè) Class 對(duì)象,用 native 方法實(shí)現(xiàn)
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
JDK 中有 3 個(gè)類加載器,另外你也可以自定義類加載器,它們的關(guān)系如下圖所示。
類加載器
- BootstrapClassLoader是啟動(dòng)類加載器,由 C 語(yǔ)言實(shí)現(xiàn),用來(lái)加載 JVM啟動(dòng)時(shí)所需要的核心類,比如rt.jar、resources.jar等。
- ExtClassLoader是擴(kuò)展類加載器,用來(lái)加載\jre\lib\ext目錄下 JAR 包。
- AppClassLoader是系統(tǒng)類加載器,用來(lái)加載 classpath下的類,應(yīng)用程序默認(rèn)用它來(lái)加載類。
- 自定義類加載器,用來(lái)加載自定義路徑下的類。
這些類加載器的工作原理是一樣的,區(qū)別是它們的加載路徑不同,也就是說(shuō) findClass這個(gè)方法查找的路徑不同。雙親委托機(jī)制是為了保證一個(gè) Java 類在 JVM 中是唯一的,假如你不小心寫(xiě)了一個(gè)與 JRE 核心類同名的類,比如 Object類,雙親委托機(jī)制能保證加載的是 JRE里的那個(gè) Object類,而不是你寫(xiě)的 Object類。這是因?yàn)?nbsp;AppClassLoader在加載你的 Object 類時(shí),會(huì)委托給 ExtClassLoader去加載,而 ExtClassLoader又會(huì)委托給 BootstrapClassLoader,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載過(guò)了 Object類,會(huì)直接返回,不會(huì)去加載你寫(xiě)的 Object類。我們最多只能 獲取到 ExtClassLoader這里注意下。
Tomcat 熱加載
Tomcat 本質(zhì)是通過(guò)一個(gè)后臺(tái)線程做周期性的任務(wù),定期檢測(cè)類文件的變化,如果有變化就重新加載類。我們來(lái)看 ContainerBackgroundProcessor具體是如何實(shí)現(xiàn)的。
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
// 請(qǐng)注意這里傳入的參數(shù)是 " 宿主類 " 的實(shí)例
processChildren(ContainerBase.this);
}
protected void processChildren(Container container) {
try {
//1. 調(diào)用當(dāng)前容器的 backgroundProcess 方法。
container.backgroundProcess();
//2. 遍歷所有的子容器,遞歸調(diào)用 processChildren,
// 這樣當(dāng)前容器的子孫都會(huì)被處理
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
// 這里請(qǐng)你注意,容器基類有個(gè)變量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后臺(tái)線程,無(wú)需父容器來(lái)調(diào)用它的 processChildren 方法。
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i]);
}
}
} catch (Throwable t) { ... }
Tomcat 的熱加載就是在 Context 容器實(shí)現(xiàn),主要是調(diào)用了 Context 容器的 reload 方法。拋開(kāi)細(xì)節(jié)從宏觀上看主要完成以下任務(wù):
- 停止和銷毀 Context 容器及其所有子容器,子容器其實(shí)就是 Wrapper,也就是說(shuō) Wrapper 里面 Servlet 實(shí)例也被銷毀了。
- 停止和銷毀 Context 容器關(guān)聯(lián)的 Listener 和 Filter。
- 停止和銷毀 Context 下的 Pipeline 和各種 Valve。
- 停止和銷毀 Context 的類加載器,以及類加載器加載的類文件資源。
- 啟動(dòng) Context 容器,在這個(gè)過(guò)程中會(huì)重新創(chuàng)建前面四步被銷毀的資源。
在這個(gè)過(guò)程中,類加載器發(fā)揮著關(guān)鍵作用。一個(gè) Context 容器對(duì)應(yīng)一個(gè)類加載器,類加載器在銷毀的過(guò)程中會(huì)把它加載的所有類也全部銷毀。Context 容器在啟動(dòng)過(guò)程中,會(huì)創(chuàng)建一個(gè)新的類加載器來(lái)加載新的類文件。
Tomcat 的類加載器
Tomcat 的自定義類加載器 WebAppClassLoader打破了雙親委托機(jī)制,它首先自己嘗試去加載某個(gè)類,如果找不到再代理給父類加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。具體實(shí)現(xiàn)就是重寫(xiě) ClassLoader的兩個(gè)方法:findClass和 loadClass。
findClass 方法
org.apache.catalina.loader.WebappClassLoaderBase#findClass;為了方便理解和閱讀,我去掉了一些細(xì)節(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. 如果在本地目錄沒(méi)有找到,交給父加載器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3. 如果父類也沒(méi)找到,拋出 ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
- 先在 Web 應(yīng)用本地目錄下查找要加載的類。
- 如果沒(méi)有找到,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類加載器 AppClassLoader。
- 如何父加載器也沒(méi)找到這個(gè)類,拋出 ClassNotFound異常。
loadClass 方法
再來(lái)看 Tomcat 類加載器的 loadClass方法的實(shí)現(xiàn),同樣我也去掉了一些細(xì)節(jié):
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地 cache 查找該類是否已經(jīng)加載過(guò)
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 從系統(tǒng)類加載器的 cache 中查找是否加載過(guò)
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) 來(lái)加載
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述過(guò)程都加載失敗,拋出異常
throw new ClassNotFoundException(name);
}
主要有六個(gè)步驟:
- 先在本地 Cache 查找該類是否已經(jīng)加載過(guò),也就是說(shuō) Tomcat 的類加載器是否已經(jīng)加載過(guò)這個(gè)類。
- 如果 Tomcat 類加載器沒(méi)有加載過(guò)這個(gè)類,再看看系統(tǒng)類加載器是否加載過(guò)。
- 如果都沒(méi)有,就讓ExtClassLoader去加載,這一步比較關(guān)鍵,目的 防止 Web 應(yīng)用自己的類覆蓋 JRE 的核心類。因?yàn)?Tomcat 需要打破雙親委托機(jī)制,假如 Web 應(yīng)用里自定義了一個(gè)叫 Object 的類,如果先加載這個(gè) Object 類,就會(huì)覆蓋 JRE 里面的那個(gè) Object 類,這就是為什么 Tomcat 的類加載器會(huì)優(yōu)先嘗試用 ExtClassLoader去加載,因?yàn)?nbsp;ExtClassLoader會(huì)委托給 BootstrapClassLoader去加載,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載了 Object 類,直接返回給 Tomcat 的類加載器,這樣 Tomcat 的類加載器就不會(huì)去加載 Web 應(yīng)用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問(wèn)題。
- 如果 ExtClassLoader加載器加載失敗,也就是說(shuō) JRE核心類中沒(méi)有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載。
- 如果本地目錄下沒(méi)有這個(gè)類,說(shuō)明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載。這里請(qǐng)你注意,Web 應(yīng)用是通過(guò)Class.forName調(diào)用交給系統(tǒng)類加載器的,因?yàn)镃lass.forName的默認(rèn)加載器就是系統(tǒng)類加載器。
- 如果上述加載過(guò)程全部失敗,拋出 ClassNotFound異常。
Tomcat 類加載器層次
Tomcat 作為 Servlet容器,它負(fù)責(zé)加載我們的 Servlet類,此外它還負(fù)責(zé)加載 Servlet所依賴的 JAR 包。并且 Tomcat本身也是也是一個(gè) Java 程序,因此它需要加載自己的類和依賴的 JAR 包。首先讓我們思考這一下這幾個(gè)問(wèn)題:
- 假如我們?cè)?Tomcat 中運(yùn)行了兩個(gè) Web 應(yīng)用程序,兩個(gè) Web 應(yīng)用中有同名的 Servlet,但是功能不同,Tomcat 需要同時(shí)加載和管理這兩個(gè)同名的 Servlet類,保證它們不會(huì)沖突,因此 Web 應(yīng)用之間的類需要隔離。
- 假如兩個(gè) Web 應(yīng)用都依賴同一個(gè)第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加載到內(nèi)存后,Tomcat要保證這兩個(gè) Web 應(yīng)用能夠共享,也就是說(shuō) Spring的 JAR 包只被加載一次,否則隨著依賴的第三方 JAR 包增多,JVM的內(nèi)存會(huì)膨脹。
- 跟 JVM 一樣,我們需要隔離 Tomcat 本身的類和 Web 應(yīng)用的類。
1、WebAppClassLoader
Tomcat 的解決方案是自定義一個(gè)類加載器 WebAppClassLoader, 并且給每個(gè) Web 應(yīng)用創(chuàng)建一個(gè)類加載器實(shí)例。我們知道,Context 容器組件對(duì)應(yīng)一個(gè) Web 應(yīng)用,因此,每個(gè) Context容器負(fù)責(zé)創(chuàng)建和維護(hù)一個(gè) WebAppClassLoader加載器實(shí)例。這背后的原理是,不同的加載器實(shí)例加載的類被認(rèn)為是不同的類,即使它們的類名相同。這就相當(dāng)于在 Java 虛擬機(jī)內(nèi)部創(chuàng)建了一個(gè)個(gè)相互隔離的 Java 類空間,每一個(gè) Web 應(yīng)用都有自己的類空間,Web 應(yīng)用之間通過(guò)各自的類加載器互相隔離。
2、SharedClassLoader
本質(zhì)需求是兩個(gè) Web 應(yīng)用之間怎么共享庫(kù)類,并且不能重復(fù)加載相同的類。在雙親委托機(jī)制里,各個(gè)子加載器都可以通過(guò)父加載器去加載類,那么把需要共享的類放到父加載器的加載路徑下不就行了嗎。
因此 Tomcat 的設(shè)計(jì)者又加了一個(gè)類加載器 SharedClassLoader,作為 WebAppClassLoader的父加載器,專門(mén)來(lái)加載 Web 應(yīng)用之間共享的類。如果 WebAppClassLoader自己沒(méi)有加載到某個(gè)類,就會(huì)委托父加載器 SharedClassLoader去加載這個(gè)類,SharedClassLoader會(huì)在指定目錄下加載共享類,之后返回給 WebAppClassLoader,這樣共享的問(wèn)題就解決了。
3、CatalinaClassloader
如何隔離 Tomcat 本身的類和 Web 應(yīng)用的類?
要共享可以通過(guò)父子關(guān)系,要隔離那就需要兄弟關(guān)系了。兄弟關(guān)系就是指兩個(gè)類加載器是平行的,它們可能擁有同一個(gè)父加載器,基于此 Tomcat 又設(shè)計(jì)一個(gè)類加載器 CatalinaClassloader,專門(mén)來(lái)加載 Tomcat 自身的類。
這樣設(shè)計(jì)有個(gè)問(wèn)題,那 Tomcat 和各 Web 應(yīng)用之間需要共享一些類時(shí)該怎么辦呢?
老辦法,還是再增加一個(gè) CommonClassLoader,作為 CatalinaClassloader和 SharedClassLoader的父加載器。CommonClassLoader能加載的類都可以被 CatalinaClassLoader和 SharedClassLoader使用。
整體架構(gòu)設(shè)計(jì)解析收獲總結(jié)
通過(guò)前面對(duì) Tomcat 整體架構(gòu)的學(xué)習(xí),知道了 Tomcat 有哪些核心組件,組件之間的關(guān)系。以及 Tomcat 是怎么處理一個(gè) HTTP 請(qǐng)求的。下面我們通過(guò)一張簡(jiǎn)化的類圖來(lái)回顧一下,從圖上你可以看到各種組件的層次關(guān)系,圖中的虛線表示一個(gè)請(qǐng)求在 Tomcat 中流轉(zhuǎn)的過(guò)程。
Tomcat 整體組件關(guān)系
連接器
Tomcat 的整體架構(gòu)包含了兩個(gè)核心組件連接器和容器。連接器負(fù)責(zé)對(duì)外交流,容器負(fù)責(zé)內(nèi)部處理。連接器用 ProtocolHandler接口來(lái)封裝通信協(xié)議和 I/O模型的差異,ProtocolHandler內(nèi)部又分為 EndPoint和 Processor模塊,EndPoint負(fù)責(zé)底層 Socket通信,Proccesor負(fù)責(zé)應(yīng)用層協(xié)議解析。連接器通過(guò)適配器 Adapter調(diào)用容器。
對(duì) Tomcat 整體架構(gòu)的學(xué)習(xí),我們可以得到一些設(shè)計(jì)復(fù)雜系統(tǒng)的基本思路。首先要分析需求,根據(jù)高內(nèi)聚低耦合的原則確定子模塊,然后找出子模塊中的變化點(diǎn)和不變點(diǎn),用接口和抽象基類去封裝不變點(diǎn),在抽象基類中定義模板方法,讓子類自行實(shí)現(xiàn)抽象方法,也就是具體子類去實(shí)現(xiàn)變化點(diǎn)。
容器
運(yùn)用了組合模式 管理容器、通過(guò) 觀察者模式 發(fā)布啟動(dòng)事件達(dá)到解耦、開(kāi)閉原則。骨架抽象類和模板方法抽象變與不變,變化的交給子類實(shí)現(xiàn),從而實(shí)現(xiàn)代碼復(fù)用,以及靈活的拓展。使用責(zé)任鏈的方式處理請(qǐng)求,比如記錄日志等。
類加載器
Tomcat 的自定義類加載器 WebAppClassLoader為了隔離 Web 應(yīng)用打破了雙親委托機(jī)制,它首先自己嘗試去加載某個(gè)類,如果找不到再代理給父類加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。防止 Web 應(yīng)用自己的類覆蓋 JRE 的核心類,使用 ExtClassLoader 去加載,這樣即打破了雙親委派,又能安全加載。
如何閱讀源碼持續(xù)學(xué)習(xí)
學(xué)習(xí)是一個(gè)反人類的過(guò)程,是比較痛苦的。尤其學(xué)習(xí)我們常用的優(yōu)秀技術(shù)框架本身比較龐大,設(shè)計(jì)比較復(fù)雜,在學(xué)習(xí)初期很容易遇到 “挫折感”,debug 跳來(lái)跳去陷入恐怖細(xì)節(jié)之中無(wú)法自拔,往往就會(huì)放棄。
找到適合自己的學(xué)習(xí)方法非常重要,同樣關(guān)鍵的是要保持學(xué)習(xí)的興趣和動(dòng)力,并且得到學(xué)習(xí)反饋效果。
學(xué)習(xí)優(yōu)秀源碼,我們收獲的就是架構(gòu)設(shè)計(jì)能力,遇到復(fù)雜需求我們學(xué)習(xí)到可以利用合理模式與組件抽象設(shè)計(jì)了可拓展性強(qiáng)的代碼能力。
如何閱讀
比如我最初在學(xué)習(xí) Spring 框架的時(shí)候,一開(kāi)始就鉆進(jìn)某個(gè)模塊啃起來(lái)。然而由于 Spring 太龐大,模塊之間也有聯(lián)系,根本不明白為啥要這么寫(xiě),只覺(jué)得為啥設(shè)計(jì)這么 “繞”。
錯(cuò)誤方式
- 陷入細(xì)節(jié),不看全局:我還沒(méi)弄清楚森林長(zhǎng)啥樣,就盯著葉子看 ,看不到全貌和整體設(shè)計(jì)思路。所以閱讀源碼學(xué)習(xí)的時(shí)候不要一開(kāi)始就進(jìn)入細(xì)節(jié),而是宏觀看待整體架構(gòu)設(shè)計(jì)思想,模塊之間的關(guān)系。
- 還沒(méi)學(xué)會(huì)用就研究如何設(shè)計(jì):首先基本上框架都運(yùn)用了設(shè)計(jì)模式,我們最起碼也要了解常用的設(shè)計(jì)模式,即使是“背”,也得了然于胸。在學(xué)習(xí)一門(mén)技術(shù),我推薦先看官方文檔,看看有哪些模塊、整體設(shè)計(jì)思想。然后下載示例跑一遍,最后才是看源碼。
- 看源碼深究細(xì)節(jié):到了看具體某個(gè)模塊源碼的時(shí)候也要下意識(shí)的不要去深入細(xì)節(jié),重要的是學(xué)習(xí)設(shè)計(jì)思路,而不是具體一個(gè)方法實(shí)現(xiàn)邏輯。除非自己要基于源碼做二次開(kāi)發(fā)。
正確方式
- 定焦原則:抓主線(抓住一個(gè)核心流程去分析,不要漫無(wú)目的的到處閱讀)。
- 宏觀思維:從全局的視角去看待,上帝視角理出主要核心架構(gòu)設(shè)計(jì),先森林后樹(shù)葉。切勿不要試圖去搞明白每一行代碼。
- 斷點(diǎn):合理運(yùn)用調(diào)用棧(觀察調(diào)用過(guò)程上下文)。
帶著目標(biāo)去學(xué)
比如某些知識(shí)點(diǎn)是面試的熱點(diǎn),那學(xué)習(xí)目標(biāo)就是徹底理解和掌握它,當(dāng)被問(wèn)到相關(guān)問(wèn)題時(shí),你的回答能夠使得面試官對(duì)你刮目相看,有時(shí)候往往憑著某一個(gè)亮點(diǎn)就能影響最后的錄用結(jié)果。
又或者接到一個(gè)稍微復(fù)雜的需求,學(xué)習(xí)從優(yōu)秀源碼中借鑒設(shè)計(jì)思路與優(yōu)化技巧。
最后就是動(dòng)手實(shí)踐,將所學(xué)運(yùn)用在工作項(xiàng)目中。只有動(dòng)手實(shí)踐才會(huì)讓我們對(duì)技術(shù)有最直觀的感受。有時(shí)候我們聽(tīng)別人講經(jīng)驗(yàn)和理論,感覺(jué)似乎懂了,但是過(guò)一段時(shí)間便又忘記了。
實(shí)際場(chǎng)景運(yùn)用
簡(jiǎn)單的分析了 Tomcat 整體架構(gòu)設(shè)計(jì),從 【連接器】 到 【容器】,并且分別細(xì)說(shuō)了一些組件的設(shè)計(jì)思想以及設(shè)計(jì)模式。接下來(lái)就是如何學(xué)以致用,借鑒優(yōu)雅的設(shè)計(jì)運(yùn)用到實(shí)際工作開(kāi)發(fā)中。學(xué)習(xí),從模仿開(kāi)始。
責(zé)任鏈模式
在工作中,有這么一個(gè)需求,用戶可以輸入一些信息并可以選擇查驗(yàn)該企業(yè)的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個(gè)或者多個(gè)模塊,而且模塊之間還有一些公共的東西是要各個(gè)模塊復(fù)用。
這里就像一個(gè)請(qǐng)求,會(huì)被多個(gè)模塊去處理。所以每個(gè)查詢模塊我們可以抽象為 處理閥門(mén),使用一個(gè) List 將這些 閥門(mén)保存起來(lái),這樣新增模塊我們只需要新增一個(gè)閥門(mén)即可,實(shí)現(xiàn)了開(kāi)閉原則,同時(shí)將一堆查驗(yàn)的代碼解耦到不同的具體閥門(mén)中,使用抽象類提取 “不變的”功能。
具體示例代碼如下所示:
首先抽象我們的處理閥門(mén), NetCheckDTO是請(qǐng)求信息。
/**
* 責(zé)任鏈模式:處理每個(gè)模塊閥門(mén)
*/
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){
// 獲取歷史記錄,省略代碼邏輯
}
// 獲取查驗(yàn)數(shù)據(jù)源配置
public final String getModuleSource(String querySource, ModuleEnum moduleEnum){
// 省略代碼邏輯
}
}
定義具體每個(gè)模塊處理的業(yè)務(wù)邏輯,比如 【百度負(fù)面新聞】對(duì)應(yīng)的處理。
@Slf4j
@Service
public class BaiduNegativeValve extends AbstractCheckValve {
@Override
public void invoke(NetCheckDTO netCheckDTO) {
}
}
最后就是管理用戶選擇要查驗(yàn)的模塊,我們通過(guò) List 保存。用于觸發(fā)所需要的查驗(yàn)?zāi)K。
@Slf4j
@Service
public class NetCheckService {
// 注入所有的閥門(mén)
@Autowired
private Map<String, Valve> valveMap;
/**
* 發(fā)送查驗(yàn)請(qǐng)求
*
* @param netCheckDTO
*/
@Async("asyncExecutor")
public void sendCheckRequest(NetCheckDTO netCheckDTO) {
// 用于保存客戶選擇處理的模塊閥門(mén)
List<Valve> valves = new ArrayList<>();
CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig();
// 將用戶選擇查驗(yàn)的模塊添加到 閥門(mén)鏈條中
if (checkModuleConfig.getBaiduNegative()) {
valves.add(valveMap.get("baiduNegativeValve"));
}
// 省略部分代碼.......
if (CollectionUtils.isEmpty(valves)) {
log.info("網(wǎng)查查驗(yàn)?zāi)K為空,沒(méi)有需要查驗(yàn)的任務(wù)");
return;
}
// 觸發(fā)處理
valves.forEach(valve -> valve.invoke(netCheckDTO));
}
}
模板方法模式
需求是這樣的,可根據(jù)客戶錄入的財(cái)報(bào) excel 數(shù)據(jù)或者企業(yè)名稱執(zhí)行財(cái)報(bào)分析。
對(duì)于非上市的則解析 excel -> 校驗(yàn)數(shù)據(jù)是否合法->執(zhí)行計(jì)算。
上市企業(yè):判斷名稱是否存在 ,不存在則發(fā)送郵件并中止計(jì)算-> 從數(shù)據(jù)庫(kù)拉取財(cái)報(bào)數(shù)據(jù),初始化查驗(yàn)日志、生成一條報(bào)告記錄,觸發(fā)計(jì)算-> 根據(jù)失敗與成功修改任務(wù)狀態(tài) 。
重要的 ”變“ 與 ”不變“,
- 不變的是整個(gè)流程是初始化查驗(yàn)日志、初始化一條報(bào)告、前期校驗(yàn)數(shù)據(jù)(若是上市公司校驗(yàn)不通過(guò)還需要構(gòu)建郵件數(shù)據(jù)并發(fā)送)、從不同來(lái)源拉取財(cái)報(bào)數(shù)據(jù)并且適配通用數(shù)據(jù)、然后觸發(fā)計(jì)算,任務(wù)異常與成功都需要修改狀態(tài)。
- 變化的是上市與非上市校驗(yàn)規(guī)則不一樣,獲取財(cái)報(bào)數(shù)據(jù)方式不一樣,兩種方式的財(cái)報(bào)數(shù)據(jù)需要適配
整個(gè)算法流程是固定的模板,但是需要將算法內(nèi)部變化的部分具體實(shí)現(xiàn)延遲到不同子類實(shí)現(xiàn),這正是模板方法模式的最佳場(chǎng)景。
public abstract class AbstractAnalysisTemplate {
/**
* 提交財(cái)報(bào)分析模板方法,定義骨架流程
* @param reportAnalysisRequest
* @return
*/
public final FinancialAnalysisResultDTO doProcess(FinancialReportAnalysisRequest reportAnalysisRequest) {
FinancialAnalysisResultDTO analysisDTO = new FinancialAnalysisResultDTO();
// 抽象方法:提交查驗(yàn)的合法校驗(yàn)
boolean prepareValidate = prepareValidate(reportAnalysisRequest, analysisDTO);
log.info("prepareValidate 校驗(yàn)結(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 {
// 抽象方法:拉取財(cái)報(bào)數(shù)據(jù),不同子類實(shí)現(xiàn)
FinancialDataDTO financialData = pullFinancialData(reportAnalysisRequest);
log.info("拉取財(cái)報(bào)數(shù)據(jù)完成, 準(zhǔn)備執(zhí)行計(jì)算");
// 測(cè)算指標(biāo)
financialCalcContext.calc(reportAnalysisRequest, financialData, reportNo);
// 設(shè)置分析日志為成功
successCalc(reportNo);
} catch (Exception e) {
log.error("財(cái)報(bào)計(jì)算子任務(wù)出現(xiàn)異常", e);
// 設(shè)置分析日志失敗
failCalc(reportNo);
throw e;
}
return analysisDTO;
}
}
最后新建兩個(gè)子類繼承該模板,并實(shí)現(xiàn)抽象方法。這樣就將上市與非上市兩種類型的處理邏輯解耦,同時(shí)又復(fù)用了代碼。
策略模式
需求是這樣,要做一個(gè)萬(wàn)能識(shí)別銀行流水的 excel 接口,假設(shè)標(biāo)準(zhǔn)流水包含【交易時(shí)間、收入、支出、交易余額、付款人賬號(hào)、付款人名字、收款人名稱、收款人賬號(hào)】等字段?,F(xiàn)在我們解析出來(lái)每個(gè)必要字段所在 excel 表頭的下標(biāo)。但是流水有多種情況:
- 一種就是包含所有標(biāo)準(zhǔn)字段。
- 收入、支出下標(biāo)是同一列,通過(guò)正負(fù)來(lái)區(qū)分收入與支出。
- 收入與支出是同一列,有一個(gè)交易類型的字段來(lái)區(qū)分。
- 特殊銀行的特殊處理。
也就是我們要根據(jù)解析對(duì)應(yīng)的下標(biāo)找到對(duì)應(yīng)的處理邏輯算法,我們可能在一個(gè)方法里面寫(xiě)超多 if else
的代碼,整個(gè)流水處理都偶合在一起,假如未來(lái)再來(lái)一種新的流水類型,還要繼續(xù)改老代碼。最后可能出現(xiàn) “又臭又長(zhǎng),難以維護(hù)” 的代碼復(fù)雜度。
這個(gè)時(shí)候我們可以用到策略模式,將不同模板的流水使用不同的處理器處理,根據(jù)模板找到對(duì)應(yīng)的策略算法去處理。即使未來(lái)再加一種類型,我們只要新加一種處理器即可,高內(nèi)聚低耦合,且可拓展。
定義處理器接口,不同處理器去實(shí)現(xiàn)處理邏輯。將所有的處理器注入到 BankFlowDataHandler 的data_processor_map中,根據(jù)不同的場(chǎng)景取出對(duì)已經(jīng)的處理器處理流水。
public interface DataProcessor {
/**
* 處理流水?dāng)?shù)據(jù)
* @param bankFlowTemplateDO 流水下標(biāo)數(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;
// 找對(duì)對(duì)應(yīng)的處理器處理流水
public void process() {
DataProcessor processor = getProcessor(bankFlowTemplateDO);
for(DataProcessor processor :processors) {
if (processor.isSupport(bankFlowTemplateDO)) {
// row 就是一行流水?dāng)?shù)據(jù)
processor.doProcess(bankFlowTemplateDO, row);
break;
}
}
}
}
定義默認(rèn)處理器,處理正常模板,新增模板只要新增處理器實(shí)現(xiàn) DataProcessor即可。
/**
* 默認(rèn)處理器:正對(duì)規(guī)范流水模板
*
*/
@Component("defaultDataProcessor")
@Slf4j
public class DefaultDataProcessor implements DataProcessor {
@Override
public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) {
// 省略處理邏輯細(xì)節(jié)
return bankTransactionFlowDO;
}
@Override
public String strategy(BankFlowTemplateDO bankFlowTemplateDO) {
// 省略判斷是否支持解析該流水
boolean isDefault = true;
return isDefault;
}
}
通過(guò)策略模式,我們將不同處理邏輯分配到不同的處理類中,這樣完全解耦,便于拓展。
使用內(nèi)嵌 Tomcat 方式調(diào)試源代碼:GitHub: https://github.com/UniqueDong/tomcat-embedded。
完美分割線,由于篇幅限制對(duì)于如何借鑒 Tomcat 的設(shè)計(jì)思想運(yùn)用到實(shí)際開(kāi)發(fā)中的綜合例子就放到下回講解了。本篇干貨滿滿,建議收藏以后多多回味。