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