Tomcat 架構(gòu)原理到架構(gòu)設(shè)計(jì),寫得非常好!
Tomcat 架構(gòu)原理解析到架構(gòu)設(shè)計(jì)借鑒
Tomcat 發(fā)展這么多年,已經(jīng)比較成熟穩(wěn)定。在如今『追新求快』的時(shí)代,Tomcat 作為 Java Web 開發(fā)必備的工具似乎變成了『熟悉的陌生人』,難道說(shuō)如今就沒有必要深入學(xué)習(xí)它了么?學(xué)習(xí)它我們又有什么收獲呢?
靜下心來(lái),細(xì)細(xì)品味經(jīng)典的開源作品 。提升我們的「內(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)秀開源軟件能獲得的「內(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)而已。腦子里似乎沒有一張 Java Web 開發(fā)全景圖,比如我并不知道瀏覽器的請(qǐng)求是怎么跟 Spring 中的代碼聯(lián)系起來(lái)的。
為了突破這個(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 高級(jí)技術(shù),比如 Java 多線程并發(fā)編程、Socket 網(wǎng)絡(luò)編程以及反射等。之前也只是了解這些技術(shù),為了面試也背過(guò)一些題。但是總感覺「知道」與會(huì)用之間存在一道溝壑,通過(guò)對(duì) Tomcat 源碼學(xué)習(xí),我學(xué)會(huì)了什么場(chǎng)景去使用這些技術(shù)。
還有就是系統(tǒng)設(shè)計(jì)能力,比如面向接口編程、組件化組合模式、骨架抽象類、一鍵式啟停、對(duì)象池技術(shù)以及各種設(shè)計(jì)模式,比如模板方法、觀察者模式、責(zé)任鏈模式等,之后我也開始模仿它們并把這些設(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è)門。
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ì)就像是「俄羅斯套娃」一樣。
連接器
在開始講連接器前,我先鋪墊一下 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++編寫的本地庫(kù)。
Tomcat 支持的應(yīng)用層協(xié)議有:
HTTP/1.1:這是大部分 Web 應(yīng)用采用的訪問協(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)聽網(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é)流寫回給瀏覽器。
需求列清楚后,我們要考慮的下一個(gè)問題是,連接器應(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)聽的接口,是具體的 Socket 接收和發(fā)送處理器,是對(duì)傳輸層的抽象,因此 EndPoint是用來(lái)實(shí)現(xiàn) TCP/IP 協(xié)議數(shù)據(jù)讀寫的,本質(zhì)調(diào)用操作系統(tǒng)的 socket 接口。
EndPoint是一個(gè)接口,對(duì)應(yīng)的抽象實(shí)現(xiàn)類是 AbstractEndpoint,而 AbstractEndpoint的具體子類,比如在 NioEndpoint和 Nio2Endpoint中,有兩個(gè)重要的子組件:Acceptor和 SocketProcessor。
其中 Acceptor 用于監(jiān)聽 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 的方法去讀寫數(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寫出。
工作流程如下所示:
NioEndPointProcessor
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ì)問,為啥要設(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"?/>
????
????<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è)樹形結(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í)就是容器組件與訪問路徑的映射關(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。

假如有用戶訪問一個(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)聽 8080 端口、默認(rèn)的 AJP 連接器監(jiān)聽 8009 端口。上面例子中的 URL 訪問的是 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 訪問的域名是
user.shopping.com,因此 Mapper 會(huì)找到 Host2 這個(gè)容器。 - 根據(jù) URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據(jù) URL 的路徑來(lái)匹配相應(yīng)的 Web 應(yīng)用的路徑,比如例子中訪問的是 /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è)處理閥門),因此 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 中沒有 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)?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è)問題,那就是系統(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ì)違反開閉原則,那如何解決這個(gè)問題呢?開閉原則說(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)聽器的,在監(jiān)聽器里可以實(shí)現(xiàn)一些邏輯,并且監(jiān)聽器也可以方便的添加和刪除,這就是典型的觀察者模式。
以下就是 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)聽器的添加和刪除等,而子類就負(fù)責(zé)實(shí)現(xiàn)自己的初始化、啟動(dòng)和停止等方法。
public?abstract?class?LifecycleBase?implements?Lifecycle{
????//?持有所有的觀察者
????private?final?List?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)聽器
????????????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í)體,可以考慮使用組合模式。
觀察者模式聽起來(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 類,它們的生命周期管理接口和功能接口是分開的,這也符合設(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) 用于子類重寫打破雙親委派: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);
????????//?如果沒有加載過(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 中是唯一的,假如你不小心寫了一個(gè)與 JRE 核心類同名的類,比如 Object類,雙親委托機(jī)制能保證加載的是 JRE里的那個(gè) Object類,而不是你寫的 Object類。這是因?yàn)?AppClassLoader在加載你的 Object 類時(shí),會(huì)委托給 ExtClassLoader去加載,而 ExtClassLoader又會(huì)委托給 BootstrapClassLoader,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載過(guò)了 Object類,會(huì)直接返回,不會(huì)去加載你寫的 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?????????????//?這里請(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 方法。拋開細(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)就是重寫 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.?如果父類也沒找到,拋出?ClassNotFoundException
????if?(clazz?==?null)?{
????????throw?new?ClassNotFoundException(name);
?????}
????return?clazz;
}
- 先在 Web 應(yīng)用本地目錄下查找要加載的類。
- 如果沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類加載器
AppClassLoader。 - 如何父加載器也沒找到這個(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 類加載器沒有加載過(guò)這個(gè)類,再看看系統(tǒng)類加載器是否加載過(guò)。
- 如果都沒有,就讓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)?ExtClassLoader會(huì)委托給BootstrapClassLoader去加載,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載了 Object 類,直接返回給 Tomcat 的類加載器,這樣 Tomcat 的類加載器就不會(huì)去加載 Web 應(yīng)用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題。 - 如果
ExtClassLoader加載器加載失敗,也就是說(shuō)JRE核心類中沒有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載。 - 如果本地目錄下沒有這個(gè)類,說(shuō)明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載。這里請(qǐng)你注意,Web 應(yīng)用是通過(guò)
Class.forName調(diào)用交給系統(tǒng)類加載器的,因?yàn)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">Class.forName的默認(rèn)加載器就是系統(tǒng)類加載器。 - 如果上述加載過(guò)程全部失敗,拋出
ClassNotFound異常。
Tomcat 類加載器層次
Tomcat 作為 Servlet容器,它負(fù)責(zé)加載我們的 Servlet類,此外它還負(fù)責(zé)加載 Servlet所依賴的 JAR 包。并且 Tomcat本身也是也是一個(gè) Java 程序,因此它需要加載自己的類和依賴的 JAR 包。首先讓我們思考這一下這幾個(gè)問題:
- 假如我們?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的父加載器,專門來(lái)加載 Web 應(yīng)用之間共享的類。如果 WebAppClassLoader自己沒有加載到某個(gè)類,就會(huì)委托父加載器 SharedClassLoader去加載這個(gè)類,SharedClassLoader會(huì)在指定目錄下加載共享類,之后返回給 WebAppClassLoader,這樣共享的問題就解決了。
3. CatalinaClassloader
如何隔離 Tomcat 本身的類和 Web 應(yīng)用的類?
要共享可以通過(guò)父子關(guān)系,要隔離那就需要兄弟關(guān)系了。兄弟關(guān)系就是指兩個(gè)類加載器是平行的,它們可能擁有同一個(gè)父加載器,基于此 Tomcat 又設(shè)計(jì)一個(gè)類加載器 CatalinaClassloader,專門來(lái)加載 Tomcat 自身的類。
這樣設(shè)計(jì)有個(gè)問題,那 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á)到解耦、開閉原則。骨架抽象類和模板方法抽象變與不變,變化的交給子類實(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í)候,一開始就鉆進(jìn)某個(gè)模塊啃起來(lái)。然而由于 Spring 太龐大,模塊之間也有聯(lián)系,根本不明白為啥要這么寫,只覺得為啥設(shè)計(jì)這么 “繞”。
錯(cuò)誤方式
- 陷入細(xì)節(jié),不看全局:我還沒弄清楚森林長(zhǎng)啥樣,就盯著葉子看 ,看不到全貌和整體設(shè)計(jì)思路。所以閱讀源碼學(xué)習(xí)的時(shí)候不要一開始就進(jìn)入細(xì)節(jié),而是宏觀看待整體架構(gòu)設(shè)計(jì)思想,模塊之間的關(guān)系。
- 還沒學(xué)會(huì)用就研究如何設(shè)計(jì):首先基本上框架都運(yùn)用了設(shè)計(jì)模式,我們最起碼也要了解常用的設(shè)計(jì)模式,即使是“背”,也得了然于胸。在學(xué)習(xí)一門技術(shù),我推薦先看官方文檔,看看有哪些模塊、整體設(shè)計(jì)思想。然后下載示例跑一遍,最后才是看源碼。
- 看源碼深究細(xì)節(jié):到了看具體某個(gè)模塊源碼的時(shí)候也要下意識(shí)的不要去深入細(xì)節(jié),重要的是學(xué)習(xí)設(shè)計(jì)思路,而不是具體一個(gè)方法實(shí)現(xiàn)邏輯。除非自己要基于源碼做二次開發(fā)。
正確方式
- 定焦原則:抓主線(抓住一個(gè)核心流程去分析,不要漫無(wú)目的的到處閱讀)。
- 宏觀思維:從全局的視角去看待,上帝視角理出主要核心架構(gòu)設(shè)計(jì),先森林后樹葉。切勿不要試圖去搞明白每一行代碼。
- 斷點(diǎn):合理運(yùn)用調(diào)用棧(觀察調(diào)用過(guò)程上下文)。
帶著目標(biāo)去學(xué)
比如某些知識(shí)點(diǎn)是面試的熱點(diǎn),那學(xué)習(xí)目標(biāo)就是徹底理解和掌握它,當(dāng)被問到相關(guā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í)候我們聽別人講經(jīng)驗(yàn)和理論,感覺似乎懂了,但是過(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í)際工作開發(fā)中。學(xué)習(xí),從模仿開始。
責(zé)任鏈模式
在工作中,有這么一個(gè)需求,用戶可以輸入一些信息并可以選擇查驗(yàn)該企業(yè)的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個(gè)或者多個(gè)模塊,而且模塊之間還有一些公共的東西是要各個(gè)模塊復(fù)用。
這里就像一個(gè)請(qǐng)求,會(huì)被多個(gè)模塊去處理。所以每個(gè)查詢模塊我們可以抽象為 處理閥門,使用一個(gè) List 將這些 閥門保存起來(lái),這樣新增模塊我們只需要新增一個(gè)閥門即可,實(shí)現(xiàn)了開閉原則,同時(shí)將一堆查驗(yàn)的代碼解耦到不同的具體閥門中,使用抽象類提取 “不變的”功能。

具體示例代碼如下所示:
首先抽象我們的處理閥門, NetCheckDTO是請(qǐng)求信息
/**
?*?責(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ù)面新聞】對(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?{
????//?注入所有的閥門
????@Autowired
????private?Map?valveMap;
????/**
?????*?發(fā)送查驗(yàn)請(qǐng)求
?????*
?????*?@param?netCheckDTO
?????*/
????@Async("asyncExecutor")
????public?void?sendCheckRequest(NetCheckDTO?netCheckDTO)?{
????????//?用于保存客戶選擇處理的模塊閥門
????????List?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)分析。
對(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)】等字段。現(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è)方法里面寫超多 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?row) ;
????/**
?????*?是否支持處理該模板,不同類型的流水策略根據(jù)模板數(shù)據(jù)判斷是否支持解析
?????*?@return
?????*/
????boolean?isSupport(BankFlowTemplateDO?bankFlowTemplateDO);
}
//?處理器的上下文
@Service
@Slf4j
public?class?BankFlowDataContext?{
????//?將所有處理器注入到?map?中
????@Autowired
????private?List?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ò)策略模式,我們將不同處理邏輯分配到不同的處理類中,這樣完全解耦,便于拓展。
最近熱文:1、盤點(diǎn) 6 個(gè)被淘汰的 Java 技術(shù),曾經(jīng)風(fēng)光過(guò)!2、Spring Boot 太狠了,一次發(fā)布 3 個(gè)版本!3、Spring Boot Redis 實(shí)現(xiàn)分布式鎖,真香!4、Spring Boot 如何快速集成 Redis?5、Java 14 祭出神器,Lombok 被干掉了?6、Java 14 祭出增強(qiáng)版 switch,真香!!7、Spring Boot 2.3 優(yōu)雅關(guān)閉新姿勢(shì),真香!8、Spring Boot 干掉了 Maven 擁抱 Gradle!
9、公司來(lái)了個(gè)新同事不會(huì)用 Lombok!
10、Spring Cloud 2020 版本重大變革!掃碼關(guān)注Java技術(shù)棧公眾號(hào)閱讀更多干貨。
點(diǎn)擊「閱讀原文」獲取面試題大全~
