1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Tomcat 架構(gòu)原理解析到架構(gòu)設(shè)計(jì)借鑒

        共 28493字,需瀏覽 57分鐘

         ·

        2020-09-24 10:56

        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è)類(lèi)幾千行、一個(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)的 RequestResponse 對(duì)象;對(duì)于具體的業(yè)務(wù)邏輯則作為變化點(diǎn),交給我們來(lái)實(shí)現(xiàn)。我們使用了SpringMVC 之類(lèi)的框架,可是卻從來(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)成為接口和抽象類(lèi),讓具體子類(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è) ServletDispatchSevlet),而 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ì)能力,比如面向接口編程、組件化組合模式、骨架抽象類(lèi)、一鍵式啟停、對(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é)流與 RequestResponse 對(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 類(lèi)庫(kù)實(shí)現(xiàn)。
        • NIO2:異步I/O,采用 JDK 7 最新的 NIO2 類(lèi)庫(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)的模塊要盡可能減少依賴(lài)的部分和降低依賴(lài)的程度,不要讓兩個(gè)模塊產(chǎn)生強(qiáng)依賴(lài)。

        我們發(fā)現(xiàn)連接器需要完成 3 個(gè)高內(nèi)聚的功能:

        • 網(wǎng)絡(luò)通信。
        • 應(yīng)用層協(xié)議解析。
        • Tomcat Request/ResponseServletRequest/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é)流給 ProcessorProcessor負(fù)責(zé)提供 Tomcat Request 對(duì)象給 Adapter,Adapter負(fù)責(zé)提供 ServletRequest對(duì)象給容器。

        封裝變與不變

        因此 Tomcat 設(shè)計(jì)了一系列抽象基類(lèi)來(lái)封裝這些穩(wěn)定的部分,抽象基類(lèi) AbstractProtocol實(shí)現(xiàn)了 ProtocolHandler接口。每一種應(yīng)用層協(xié)議有自己的抽象基類(lèi),比如 AbstractAjpProtocolAbstractHttp11Protocol,具體協(xié)議的實(shí)現(xiàn)類(lèi)擴(kuò)展了協(xié)議層抽象基類(lèi)。

        這就是模板方法設(shè)計(jì)模式的運(yùn)用。

        應(yīng)用層協(xié)議抽象

        總結(jié)下來(lái),連接器的三個(gè)核心組件 Endpoint、ProcessorAdapter來(lái)分別做三件事情,其中 EndpointProcessor放在一起抽象成了 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)類(lèi)是 AbstractEndpoint,而 AbstractEndpoint的具體子類(lèi),比如在 NioEndpointNio2Endpoint中,有兩個(gè)重要的子組件:AcceptorSocketProcessor

        其中 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ú)非是兩步:

        1. 創(chuàng)建一個(gè) Seletor,在它身上注冊(cè)各種感興趣的事件,然后調(diào)用 select 方法,等待感興趣的事情發(fā)生。

        2. 感興趣的事情發(fā)生了,比如可以讀了,這時(shí)便創(chuàng)建一個(gè)新的線程從 Channel 中讀數(shù)據(jù)。

        在 Tomcat 中 NioEndpoint 則是 AbstractEndpoint 的具體實(shí)現(xiàn),里面組件雖然很多,但是處理邏輯還是前面兩步。它一共包含 LimitLatch、Acceptor、Poller、SocketProcessorExecutor 共 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 類(lèi)也是不一樣的,比如有 AsynchronousSocketChannel 和 SocketChannel,為了對(duì) Http11Processor 屏蔽這些差異,Tomcat 設(shè)計(jì)了一個(gè)包裝類(lèi)叫作 SocketWrapper,Http11Processor 只調(diào)用 SocketWrapper 的方法去讀寫(xiě)數(shù)據(jù)。

        • Executor就是線程池,負(fù)責(zé)運(yùn)行 SocketProcessor任務(wù)類(lèi),SocketProcessorrun方法會(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)來(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)用 CoyoteAdapterSevice 方法,傳入的是 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、ContextWrapper。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"?/>


        ????
        ????<Connector?port="8009"?protocol="AJP/1.3"?redirectPort="8443"?/>??//?連接器

        ?//?容器組件:一個(gè) Engine 處理 Service 所有請(qǐng)求,包含多個(gè) Host
        ????<Engine?name="Catalina"?defaultHost="localhost">
        ???//?容器組件:處理指定Host下的客戶(hù)端請(qǐng)求,?可包含多個(gè) Context
        ??????<Host?name="localhost"??appBase="webapps"
        ????????????unpackWARs="true"?autoDeploy="true">

        ???//?容器組件:處理特定 Context Web應(yīng)用的所有客戶(hù)端請(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接口,因此組合模式可以使得用戶(hù)對(duì)單容器對(duì)象和組合容器對(duì)象的使用具有一致性。這里單容器對(duì)象指的是最底層的 Wrapper,組合容器對(duì)象指的是上面的 ContextHost或者 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、SetParentaddChildremoveChild等方法,這里正好驗(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è) WrapperServlet 處理的?答案是,Tomcat 是用 Mapper 組件來(lái)完成這個(gè)任務(wù)的。

        Mapper 組件的功能就是將用戶(hù)請(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。

        假如有用戶(hù)訪問(wèn)一個(gè) URL,比如圖中的http://user.shopping.com:8080/order/buy,Tomcat 如何將這個(gè) URL 定位到一個(gè) Servlet 呢?

        1. 首先根據(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 也確定了。
        2. 根據(jù)域名選定 Host。 Service 和 Engine 確定后,Mapper 組件通過(guò) URL 中的域名去查找相應(yīng)的 Host 容器,比如例子中的 URL 訪問(wèn)的域名是user.shopping.com,因此 Mapper 會(huì)找到 Host2 這個(gè)容器。
        3. 根據(jù) URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據(jù) URL 的路徑來(lái)匹配相應(yīng)的 Web 應(yīng)用的路徑,比如例子中訪問(wèn)的是 /order,因此找到了 Context4 這個(gè) Context 容器。
        4. 根據(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ù)處理,依次類(lèi)推,最后這個(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)?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)到 Servletservice方法。

        前面我們不是講到了 Filter,似乎也有相似的功能,那 ValveFilter有什么區(qū)別嗎?它們的區(qū)別是:

        • ValveTomcat的私有機(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í)候,我們還需要釋放資源,銷(xiāo)毀這些組件,因此這是一個(gè)動(dòng)態(tài)的過(guò)程。也就是說(shuō),Tomcat 需要?jiǎng)討B(tài)地管理這些組件的生命周期。

        如何統(tǒng)一管理組件的創(chuàng)建、初始化、啟動(dòng)、停止和銷(xiāo)毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動(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)中已有的類(lèi),但是你可以定義新的類(lèi)。

        組件的 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 抽象基類(lèi)

        再次看到抽象模板設(shè)計(jì)模式。

        有了接口,我們就要用類(lèi)去實(shí)現(xiàn)接口。一般來(lái)說(shuō)實(shí)現(xiàn)類(lèi)不止一個(gè),不同的類(lèi)在實(shí)現(xiàn)接口時(shí)往往會(huì)有一些相同的邏輯,如果讓各個(gè)子類(lèi)都去實(shí)現(xiàn)一遍,就會(huì)有重復(fù)代碼。那子類(lèi)如何重用這部分邏輯呢?其實(shí)就是定義一個(gè)基類(lèi)來(lái)實(shí)現(xiàn)共同的邏輯,然后讓各個(gè)子類(lèi)去繼承它,就達(dá)到了重用的目的。

        Tomcat 定義一個(gè)基類(lèi) LifeCycleBase 來(lái)實(shí)現(xiàn) LifeCycle 接口,把一些公共的邏輯放到基類(lèi)中去,比如生命狀態(tài)的轉(zhuǎn)變與維護(hù)、生命事件的觸發(fā)以及監(jiān)聽(tīng)器的添加和刪除等,而子類(lèi)就負(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)聽(tīng)器
        ????????????setStateInternal(LifecycleState.INITIALIZING,?null,?false);
        ????????????//?3.?調(diào)用具體子類(lèi)的初始化方法
        ????????????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)用了組合模式、觀察者模式、骨架抽象類(lèi)和模板方法

        如果你需要維護(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)類(lèi),因?yàn)樗鼈兌际侨萜鳎岳^承了 ContainerBase 抽象基類(lèi),而 ContainerBase 實(shí)現(xiàn)了 Container 接口,也繼承了 LifeCycleBase 類(lèi),它們的生命周期管理接口和功能接口是分開(kāi)的,這也符合設(shè)計(jì)中接口分離的原則。

        Tomcat 為何打破雙親委派機(jī)制

        雙親委派

        我們知道 JVM的類(lèi)加載器加載 Class 的時(shí)候基于雙親委派機(jī)制,也就是會(huì)將加載交給自己的父加載器加載,如果 父加載器為空則查找Bootstrap 是否加載過(guò),當(dāng)無(wú)法加載的時(shí)候才讓自己加載。JDK 提供一個(gè)抽象類(lèi) ClassLoader,這個(gè)抽象類(lèi)中定義了三個(gè)關(guān)鍵方法。對(duì)外使用loadClass(String name) 用于子類(lèi)重寫(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ù)傳入的類(lèi)名?name,到在特定目錄下去尋找類(lèi)文件,把.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è)類(lèi)加載器,另外你也可以自定義類(lèi)加載器,它們的關(guān)系如下圖所示。

        類(lèi)加載器
        • BootstrapClassLoader是啟動(dòng)類(lèi)加載器,由 C 語(yǔ)言實(shí)現(xiàn),用來(lái)加載 JVM啟動(dòng)時(shí)所需要的核心類(lèi),比如rt.jar、resources.jar等。
        • ExtClassLoader是擴(kuò)展類(lèi)加載器,用來(lái)加載\jre\lib\ext目錄下 JAR 包。
        • AppClassLoader是系統(tǒng)類(lèi)加載器,用來(lái)加載 classpath下的類(lèi),應(yīng)用程序默認(rèn)用它來(lái)加載類(lèi)。
        • 自定義類(lèi)加載器,用來(lái)加載自定義路徑下的類(lèi)。

        這些類(lèi)加載器的工作原理是一樣的,區(qū)別是它們的加載路徑不同,也就是說(shuō) findClass這個(gè)方法查找的路徑不同。雙親委托機(jī)制是為了保證一個(gè) Java 類(lèi)在 JVM 中是唯一的,假如你不小心寫(xiě)了一個(gè)與 JRE 核心類(lèi)同名的類(lèi),比如 Object類(lèi),雙親委托機(jī)制能保證加載的是 JRE里的那個(gè) Object類(lèi),而不是你寫(xiě)的 Object類(lèi)。這是因?yàn)?AppClassLoader在加載你的 Object 類(lèi)時(shí),會(huì)委托給 ExtClassLoader去加載,而 ExtClassLoader又會(huì)委托給 BootstrapClassLoader,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載過(guò)了 Object類(lèi),會(huì)直接返回,不會(huì)去加載你寫(xiě)的 Object類(lèi)。我們最多只能 獲取到 ExtClassLoader這里注意下。

        Tomcat 熱加載

        Tomcat 本質(zhì)是通過(guò)一個(gè)后臺(tái)線程做周期性的任務(wù),定期檢測(cè)類(lèi)文件的變化,如果有變化就重新加載類(lèi)。我們來(lái)看 ContainerBackgroundProcessor具體是如何實(shí)現(xiàn)的。

        protected?class?ContainerBackgroundProcessor?implements?Runnable?{

        ????@Override
        ????public?void?run()?{
        ????????//?請(qǐng)注意這里傳入的參數(shù)是?"?宿主類(lèi)?"?的實(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)你注意,容器基類(lèi)有個(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ù):

        1. 停止和銷(xiāo)毀 Context 容器及其所有子容器,子容器其實(shí)就是 Wrapper,也就是說(shuō) Wrapper 里面 Servlet 實(shí)例也被銷(xiāo)毀了。
        2. 停止和銷(xiāo)毀 Context 容器關(guān)聯(lián)的 Listener 和 Filter。
        3. 停止和銷(xiāo)毀 Context 下的 Pipeline 和各種 Valve。
        4. 停止和銷(xiāo)毀 Context 的類(lèi)加載器,以及類(lèi)加載器加載的類(lèi)文件資源。
        5. 啟動(dòng) Context 容器,在這個(gè)過(guò)程中會(huì)重新創(chuàng)建前面四步被銷(xiāo)毀的資源。

        在這個(gè)過(guò)程中,類(lèi)加載器發(fā)揮著關(guān)鍵作用。一個(gè) Context 容器對(duì)應(yīng)一個(gè)類(lèi)加載器,類(lèi)加載器在銷(xiāo)毀的過(guò)程中會(huì)把它加載的所有類(lèi)也全部銷(xiāo)毀。Context 容器在啟動(dòng)過(guò)程中,會(huì)創(chuàng)建一個(gè)新的類(lèi)加載器來(lái)加載新的類(lèi)文件。

        Tomcat 的類(lèi)加載器

        Tomcat 的自定義類(lèi)加載器 WebAppClassLoader打破了雙親委托機(jī)制,它首先自己嘗試去加載某個(gè)類(lèi),如果找不到再代理給父類(lèi)加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類(lèi)。具體實(shí)現(xiàn)就是重寫(xiě) ClassLoader的兩個(gè)方法:findClassloadClass

        findClass 方法

        org.apache.catalina.loader.WebappClassLoaderBase#findClass;為了方便理解和閱讀,我去掉了一些細(xì)節(jié):

        public?Class?findClass(String?name)?throws?ClassNotFoundException?{
        ????...

        ????Class?clazz?=?null;
        ????try?{
        ????????????//1.?先在?Web?應(yīng)用目錄下查找類(lèi)
        ????????????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.?如果父類(lèi)也沒(méi)找到,拋出?ClassNotFoundException
        ????if?(clazz?==?null)?{
        ????????throw?new?ClassNotFoundException(name);
        ?????}

        ????return?clazz;
        }
        1. 先在 Web 應(yīng)用本地目錄下查找要加載的類(lèi)。
        2. 如果沒(méi)有找到,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類(lèi)加載器 AppClassLoader。
        3. 如何父加載器也沒(méi)找到這個(gè)類(lèi),拋出 ClassNotFound異常。
        loadClass 方法

        再來(lái)看 Tomcat 類(lèi)加載器的 loadClass方法的實(shí)現(xiàn),同樣我也去掉了一些細(xì)節(jié):

        public?Class?loadClass(String?name,?boolean?resolve)?throws?ClassNotFoundException?{

        ????synchronized?(getClassLoadingLock(name))?{

        ????????Class?clazz?=?null;

        ????????//1.?先在本地?cache?查找該類(lèi)是否已經(jīng)加載過(guò)
        ????????clazz?=?findLoadedClass0(name);
        ????????if?(clazz?!=?null)?{
        ????????????if?(resolve)
        ????????????????resolveClass(clazz);
        ????????????return?clazz;
        ????????}

        ????????//2.?從系統(tǒng)類(lèi)加載器的?cache?中查找是否加載過(guò)
        ????????clazz?=?findLoadedClass(name);
        ????????if?(clazz?!=?null)?{
        ????????????if?(resolve)
        ????????????????resolveClass(clazz);
        ????????????return?clazz;
        ????????}

        ????????// 3. 嘗試用 ExtClassLoader 類(lèi)加載器類(lèi)加載,為什么?
        ????????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)類(lèi)加載器?(也就是?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è)步驟:

        1. 先在本地 Cache 查找該類(lèi)是否已經(jīng)加載過(guò),也就是說(shuō) Tomcat 的類(lèi)加載器是否已經(jīng)加載過(guò)這個(gè)類(lèi)。
        2. 如果 Tomcat 類(lèi)加載器沒(méi)有加載過(guò)這個(gè)類(lèi),再看看系統(tǒng)類(lèi)加載器是否加載過(guò)。
        3. 如果都沒(méi)有,就讓ExtClassLoader去加載,這一步比較關(guān)鍵,目的 防止 Web 應(yīng)用自己的類(lèi)覆蓋 JRE 的核心類(lèi)。因?yàn)?Tomcat 需要打破雙親委托機(jī)制,假如 Web 應(yīng)用里自定義了一個(gè)叫 Object 的類(lèi),如果先加載這個(gè) Object 類(lèi),就會(huì)覆蓋 JRE 里面的那個(gè) Object 類(lèi),這就是為什么 Tomcat 的類(lèi)加載器會(huì)優(yōu)先嘗試用 ExtClassLoader去加載,因?yàn)?ExtClassLoader會(huì)委托給 BootstrapClassLoader去加載,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載了 Object 類(lèi),直接返回給 Tomcat 的類(lèi)加載器,這樣 Tomcat 的類(lèi)加載器就不會(huì)去加載 Web 應(yīng)用下的 Object 類(lèi)了,也就避免了覆蓋 JRE 核心類(lèi)的問(wèn)題。
        4. 如果 ExtClassLoader加載器加載失敗,也就是說(shuō) JRE核心類(lèi)中沒(méi)有這類(lèi),那么就在本地 Web 應(yīng)用目錄下查找并加載。
        5. 如果本地目錄下沒(méi)有這個(gè)類(lèi),說(shuō)明不是 Web 應(yīng)用自己定義的類(lèi),那么由系統(tǒng)類(lèi)加載器去加載。這里請(qǐng)你注意,Web 應(yīng)用是通過(guò)Class.forName調(diào)用交給系統(tǒng)類(lèi)加載器的,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">Class.forName的默認(rèn)加載器就是系統(tǒng)類(lèi)加載器。
        6. 如果上述加載過(guò)程全部失敗,拋出 ClassNotFound異常。

        Tomcat 類(lèi)加載器層次

        Tomcat 作為 Servlet容器,它負(fù)責(zé)加載我們的 Servlet類(lèi),此外它還負(fù)責(zé)加載 Servlet所依賴(lài)的 JAR 包。并且 Tomcat本身也是也是一個(gè) Java 程序,因此它需要加載自己的類(lèi)和依賴(lài)的 JAR 包。首先讓我們思考這一下這幾個(gè)問(wèn)題:

        1. 假如我們?cè)?Tomcat 中運(yùn)行了兩個(gè) Web 應(yīng)用程序,兩個(gè) Web 應(yīng)用中有同名的 Servlet,但是功能不同,Tomcat 需要同時(shí)加載和管理這兩個(gè)同名的 Servlet類(lèi),保證它們不會(huì)沖突,因此 Web 應(yīng)用之間的類(lèi)需要隔離。
        2. 假如兩個(gè) Web 應(yīng)用都依賴(lài)同一個(gè)第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加載到內(nèi)存后,Tomcat要保證這兩個(gè) Web 應(yīng)用能夠共享,也就是說(shuō) Spring的 JAR 包只被加載一次,否則隨著依賴(lài)的第三方 JAR 包增多,JVM的內(nèi)存會(huì)膨脹。
        3. 跟 JVM 一樣,我們需要隔離 Tomcat 本身的類(lèi)和 Web 應(yīng)用的類(lèi)。
        1. WebAppClassLoader

        Tomcat 的解決方案是自定義一個(gè)類(lèi)加載器 WebAppClassLoader, 并且給每個(gè) Web 應(yīng)用創(chuàng)建一個(gè)類(lèi)加載器實(shí)例。我們知道,Context 容器組件對(duì)應(yīng)一個(gè) Web 應(yīng)用,因此,每個(gè) Context容器負(fù)責(zé)創(chuàng)建和維護(hù)一個(gè) WebAppClassLoader加載器實(shí)例。這背后的原理是,不同的加載器實(shí)例加載的類(lèi)被認(rèn)為是不同的類(lèi),即使它們的類(lèi)名相同。這就相當(dāng)于在 Java 虛擬機(jī)內(nèi)部創(chuàng)建了一個(gè)個(gè)相互隔離的 Java 類(lèi)空間,每一個(gè) Web 應(yīng)用都有自己的類(lèi)空間,Web 應(yīng)用之間通過(guò)各自的類(lèi)加載器互相隔離。

        2.SharedClassLoader

        本質(zhì)需求是兩個(gè) Web 應(yīng)用之間怎么共享庫(kù)類(lèi),并且不能重復(fù)加載相同的類(lèi)。在雙親委托機(jī)制里,各個(gè)子加載器都可以通過(guò)父加載器去加載類(lèi),那么把需要共享的類(lèi)放到父加載器的加載路徑下不就行了嗎。

        因此 Tomcat 的設(shè)計(jì)者又加了一個(gè)類(lèi)加載器 SharedClassLoader,作為 WebAppClassLoader的父加載器,專(zhuān)門(mén)來(lái)加載 Web 應(yīng)用之間共享的類(lèi)。如果 WebAppClassLoader自己沒(méi)有加載到某個(gè)類(lèi),就會(huì)委托父加載器 SharedClassLoader去加載這個(gè)類(lèi),SharedClassLoader會(huì)在指定目錄下加載共享類(lèi),之后返回給 WebAppClassLoader,這樣共享的問(wèn)題就解決了。

        3. CatalinaClassloader

        如何隔離 Tomcat 本身的類(lèi)和 Web 應(yīng)用的類(lèi)?

        要共享可以通過(guò)父子關(guān)系,要隔離那就需要兄弟關(guān)系了。兄弟關(guān)系就是指兩個(gè)類(lèi)加載器是平行的,它們可能擁有同一個(gè)父加載器,基于此 Tomcat 又設(shè)計(jì)一個(gè)類(lèi)加載器 CatalinaClassloader,專(zhuān)門(mén)來(lái)加載 Tomcat 自身的類(lèi)。

        這樣設(shè)計(jì)有個(gè)問(wèn)題,那 Tomcat 和各 Web 應(yīng)用之間需要共享一些類(lèi)時(shí)該怎么辦呢?

        老辦法,還是再增加一個(gè) CommonClassLoader,作為 CatalinaClassloaderSharedClassLoader的父加載器。CommonClassLoader能加載的類(lèi)都可以被 CatalinaClassLoaderSharedClassLoader使用

        整體架構(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)圖來(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)部又分為 EndPointProcessor模塊,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),用接口和抽象基類(lèi)去封裝不變點(diǎn),在抽象基類(lèi)中定義模板方法,讓子類(lèi)自行實(shí)現(xiàn)抽象方法,也就是具體子類(lèi)去實(shí)現(xiàn)變化點(diǎn)。

        容器

        運(yùn)用了組合模式 管理容器、通過(guò) 觀察者模式 發(fā)布啟動(dòng)事件達(dá)到解耦、開(kāi)閉原則。骨架抽象類(lèi)和模板方法抽象變與不變,變化的交給子類(lèi)實(shí)現(xiàn),從而實(shí)現(xiàn)代碼復(fù)用,以及靈活的拓展。使用責(zé)任鏈的方式處理請(qǐng)求,比如記錄日志等。

        類(lèi)加載器

        Tomcat 的自定義類(lèi)加載器 WebAppClassLoader為了隔離 Web 應(yīng)用打破了雙親委托機(jī)制,它首先自己嘗試去加載某個(gè)類(lèi),如果找不到再代理給父類(lèi)加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類(lèi)。防止 Web 應(yīng)用自己的類(lèi)覆蓋 JRE 的核心類(lèi),使用 ExtClassLoader 去加載,這樣即打破了雙親委派,又能安全加載。

        如何閱讀源碼持續(xù)學(xué)習(xí)

        學(xué)習(xí)是一個(gè)反人類(lèi)的過(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è)需求,用戶(hù)可以輸入一些信息并可以選擇查驗(yàn)該企業(yè)的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個(gè)或者多個(gè)模塊,而且模塊之間還有一些公共的東西是要各個(gè)模塊復(fù)用。

        這里就像一個(gè)請(qǐng)求,會(huì)被多個(gè)模塊去處理。所以每個(gè)查詢(xún)模塊我們可以抽象為 處理閥門(mén),使用一個(gè) List 將這些 閥門(mén)保存起來(lái),這樣新增模塊我們只需要新增一個(gè)閥門(mén)即可,實(shí)現(xiàn)了開(kāi)閉原則同時(shí)將一堆查驗(yàn)的代碼解耦到不同的具體閥門(mén)中,使用抽象類(lèi)提取 “不變的”功能。

        具體示例代碼如下所示:

        首先抽象我們的處理閥門(mén), NetCheckDTO是請(qǐng)求信息

        /**
        ?*?責(zé)任鏈模式:處理每個(gè)模塊閥門(mén)
        ?*/

        public?interface?Valve?{
        ????/**
        ?????*?調(diào)用
        ?????*?@param?netCheckDTO
        ?????*/

        ????void?invoke(NetCheckDTO?netCheckDTO);
        }

        定義抽象基類(lèi),復(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)?{

        ????}
        }

        最后就是管理用戶(hù)選擇要查驗(yàn)的模塊,我們通過(guò) List 保存。用于觸發(fā)所需要的查驗(yàn)?zāi)K

        @Slf4j
        @Service
        public?class?NetCheckService?{
        ????//?注入所有的閥門(mén)
        ????@Autowired
        ????private?Map?valveMap;

        ????/**
        ?????*?發(fā)送查驗(yàn)請(qǐng)求
        ?????*
        ?????*?@param?netCheckDTO
        ?????*/

        ????@Async("asyncExecutor")
        ????public?void?sendCheckRequest(NetCheckDTO?netCheckDTO)?{
        ????????//?用于保存客戶(hù)選擇處理的模塊閥門(mén)
        ????????List?valves?=?new?ArrayList<>();

        ????????CheckModuleConfigDTO?checkModuleConfig?=?netCheckDTO.getCheckModuleConfig();
        ????????//?將用戶(hù)選擇查驗(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ù)客戶(hù)錄入的財(cái)報(bào) excel 數(shù)據(jù)或者企業(yè)名稱(chēng)執(zhí)行財(cái)報(bào)分析。

        對(duì)于非上市的則解析 excel -> 校驗(yàn)數(shù)據(jù)是否合法->執(zhí)行計(jì)算。

        上市企業(yè):判斷名稱(chēng)是否存在 ,不存在則發(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)延遲到不同子類(lèi)實(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ù),不同子類(lèi)實(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è)子類(lèi)繼承該模板,并實(shí)現(xiàn)抽象方法。這樣就將上市與非上市兩種類(lèi)型的處理邏輯解耦,同時(shí)又復(fù)用了代碼。

        策略模式

        需求是這樣,要做一個(gè)萬(wàn)能識(shí)別銀行流水的 excel 接口,假設(shè)標(biāo)準(zhǔn)流水包含【交易時(shí)間、收入、支出、交易余額、付款人賬號(hào)、付款人名字、收款人名稱(chēng)、收款人賬號(hào)】等字段。現(xiàn)在我們解析出來(lái)每個(gè)必要字段所在 excel 表頭的下標(biāo)。但是流水有多種情況:

        1. 一種就是包含所有標(biāo)準(zhǔn)字段。
        2. 收入、支出下標(biāo)是同一列,通過(guò)正負(fù)來(lái)區(qū)分收入與支出。
        3. 收入與支出是同一列,有一個(gè)交易類(lèi)型的字段來(lái)區(qū)分。
        4. 特殊銀行的特殊處理。

        也就是我們要根據(jù)解析對(duì)應(yīng)的下標(biāo)找到對(duì)應(yīng)的處理邏輯算法,我們可能在一個(gè)方法里面寫(xiě)超多 if else 的代碼,整個(gè)流水處理都偶合在一起,假如未來(lái)再來(lái)一種新的流水類(lèi)型,還要繼續(xù)改老代碼。最后可能出現(xiàn) “又臭又長(zhǎng),難以維護(hù)” 的代碼復(fù)雜度。

        這個(gè)時(shí)候我們可以用到策略模式,將不同模板的流水使用不同的處理器處理,根據(jù)模板找到對(duì)應(yīng)的策略算法去處理。即使未來(lái)再加一種類(lèi)型,我們只要新加一種處理器即可,高內(nèi)聚低耦合,且可拓展。

        定義處理器接口,不同處理器去實(shí)現(xiàn)處理邏輯。將所有的處理器注入到 BankFlowDataHandlerdata_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);

        ????/**
        ?????*?是否支持處理該模板,不同類(lèi)型的流水策略根據(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ò)策略模式,我們將不同處理邏輯分配到不同的處理類(lèi)中,這樣完全解耦,便于拓展。

        使用內(nèi)嵌 Tomcat 方式調(diào)試源代碼:GitHub: https://github.com/UniqueDong/tomcat-embedded


        完美分割線,由于篇幅限制對(duì)于如何借鑒 Tomcat 的設(shè)計(jì)思想運(yùn)用到實(shí)際開(kāi)發(fā)中的綜合例子就放到下回講解了。本篇干貨滿(mǎn)滿(mǎn),建議收藏以后多多回味,也希望讀者 「點(diǎn)贊」「分享」「在看」三連就是最大的鼓勵(lì)。

        我整理的4本PDF文檔,公眾號(hào)“后端技術(shù)進(jìn)階”后臺(tái)回復(fù)“面試突擊”即可免費(fèi)獲取。

        文章有幫助可以點(diǎn)個(gè)「在看」或「分享」,都是支持,我都喜歡!

        我是Guide哥,Java后端開(kāi)發(fā),會(huì)一點(diǎn)前端知識(shí),喜歡烹飪,自由的少年。一個(gè)三觀比主角還正的技術(shù)人。我們下期再見(jiàn)!

        瀏覽 33
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            亚洲中文字幕在线播放视频 | 亚洲秘 无码一区二区三区电影 | 四虎影院色| 二级片免费看 | 亚洲传媒视频 | 日日碰夜夜操 | 色色色综合色 | 国产 激情 在线 | 欧美日韩第一区 | 女13昨晚被弄了好爽 |