宏觀(guān)上看
Tomcat 作為一個(gè) 「Http
服務(wù)器 + Servlet
容器」,對我們屏蔽了應用層協(xié)議和網(wǎng)絡(luò )通信細節,給我們的是標準的 Request
和 Response
對象;對于具體的業(yè)務(wù)邏輯則作為變化點(diǎn),交給我們來(lái)實(shí)現。我們使用了SpringMVC
之類(lèi)的框架,可是卻從來(lái)不需要考慮 TCP
連接、 Http
協(xié)議的數據處理與響應。就是因為 Tomcat 已經(jīng)為我們做好了這些,我們只需要關(guān)注每個(gè)請求的具體業(yè)務(wù)邏輯。
微觀(guān)上看
Tomcat
內部也隔離了變化點(diǎn)與不變點(diǎn),使用了組件化設計,目的就是為了實(shí)現「俄羅斯套娃式」的高度定制化(組合模式),而每個(gè)組件的生命周期管理又有一些共性的東西,則被提取出來(lái)成為接口和抽象類(lèi),讓具體子類(lèi)實(shí)現變化點(diǎn),也就是模板方法設計模式。
當今流行的微服務(wù)也是這個(gè)思路,按照功能將單體應用拆成「微服務(wù)」,拆分過(guò)程要將共性提取出來(lái),而這些共性就會(huì )成為核心的基礎服務(wù)或者通用庫?!钢信_」思想亦是如此。
設計模式往往就是封裝變化的一把利器,合理的運用設計模式能讓我們的代碼與系統設計變得優(yōu)雅且整潔。
這就是學(xué)習優(yōu)秀開(kāi)源軟件能獲得的「內功」,從不會(huì )過(guò)時(shí),其中的設計思想與哲學(xué)才是根本之道。從中借鑒設計經(jīng)驗,合理運用設計模式封裝變與不變,更能從它們的源碼中汲取經(jīng)驗,提升自己的系統設計能力。
在工作過(guò)程中,我們對 Java 語(yǔ)法已經(jīng)很熟悉了,甚至「背」過(guò)一些設計模式,用過(guò)很多 Web 框架,但是很少有機會(huì )將他們用到實(shí)際項目中,讓自己獨立設計一個(gè)系統似乎也是根據需求一個(gè)個(gè) Service 實(shí)現而已。腦子里似乎沒(méi)有一張 Java Web 開(kāi)發(fā)全景圖,比如我并不知道瀏覽器的請求是怎么跟 Spring 中的代碼聯(lián)系起來(lái)的。
為了突破這個(gè)瓶頸,為何不站在巨人的肩膀上學(xué)習優(yōu)秀的開(kāi)源系統,看大牛們是如何思考這些問(wèn)題。
學(xué)習 Tomcat 的原理,我發(fā)現 Servlet
技術(shù)是 Web 開(kāi)發(fā)的原點(diǎn),幾乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet
的封裝,Spring 應用本身就是一個(gè) Servlet
(DispatchSevlet
),而 Tomcat 和 Jetty 這樣的 Web 容器,負責加載和運行 Servlet
。如圖所示:
學(xué)習 Tomcat ,我還發(fā)現用到不少 Java 高級技術(shù),比如 Java 多線(xiàn)程并發(fā)編程、Socket 網(wǎng)絡(luò )編程以及反射等。之前也只是了解這些技術(shù),為了面試也背過(guò)一些題。但是總感覺(jué)「知道」與會(huì )用之間存在一道溝壑,通過(guò)對 Tomcat 源碼學(xué)習,我學(xué)會(huì )了什么場(chǎng)景去使用這些技術(shù)。
還有就是系統設計能力,比如面向接口編程、組件化組合模式、骨架抽象類(lèi)、一鍵式啟停、對象池技術(shù)以及各種設計模式,比如模板方法、觀(guān)察者模式、責任鏈模式等,之后我也開(kāi)始模仿它們并把這些設計思想運用到實(shí)際的工作中。
今天咱們就來(lái)一步一步分析 Tomcat 的設計思路,一方面我們可以學(xué)到 Tomcat 的總體架構,學(xué)會(huì )從宏觀(guān)上怎么去設計一個(gè)復雜系統,怎么設計頂層模塊,以及模塊之間的關(guān)系;另一方面也為我們深入學(xué)習 Tomcat 的工作原理打下基礎。
Tomcat 啟動(dòng)流程:
startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()
Tomcat 實(shí)現的 2 個(gè)核心功能:
Socket
連接,負責網(wǎng)絡(luò )字節流與 Request
和 Response
對象的轉化。Servlet
,以及處理具體的 Request
請求。所以 Tomcat 設計了兩個(gè)核心組件連接器(Connector)和容器(Container)。連接器負責對外交流,容器負責內部 處理
Tomcat
為了實(shí)現支持多種 I/O
模型和應用層協(xié)議,一個(gè)容器可能對接多個(gè)連接器,就好比一個(gè)房間有多個(gè)門(mén)。
每個(gè)組件都有對應的生命周期,需要啟動(dòng),同時(shí)還要啟動(dòng)自己內部的子組件,比如一個(gè) Tomcat 實(shí)例包含一個(gè) Service,一個(gè) Service 包含多個(gè)連接器和一個(gè)容器。而一個(gè)容器包含多個(gè) Host, Host 內部可能有多個(gè) Contex t 容器,而一個(gè) Context 也會(huì )包含多個(gè) Servlet,所以 Tomcat 利用組合模式管理組件每個(gè)組件,對待過(guò)個(gè)也想對待單個(gè)組一樣對待。整體上每個(gè)組件設計就像是「俄羅斯套娃」一樣。
在開(kāi)始講連接器前,我先鋪墊一下 Tomcat
支持的多種 I/O
模型和應用層協(xié)議。
Tomcat
支持的 I/O
模型有:
NIO
:非阻塞 I/O
,采用 Java NIO
類(lèi)庫實(shí)現。NIO2
:異步I/O
,采用 JDK 7
最新的 NIO2
類(lèi)庫實(shí)現。APR
:采用 Apache
可移植運行庫實(shí)現,是 C/C++
編寫(xiě)的本地庫。Tomcat 支持的應用層協(xié)議有:
HTTP/1.1
:這是大部分 Web 應用采用的訪(fǎng)問(wèn)協(xié)議。AJP
:用于和 Web 服務(wù)器集成(如 Apache)。HTTP/2
:HTTP 2.0 大幅度的提升了 Web 性能。所以一個(gè)容器可能對接多個(gè)連接器。連接器對 Servlet
容器屏蔽了網(wǎng)絡(luò )協(xié)議與 I/O
模型的區別,無(wú)論是 Http
還是 AJP
,在容器中獲取到的都是一個(gè)標準的 ServletRequest
對象。
細化連接器的功能需求就是:
HTTP/AJP
)解析字節流,生成統一的 Tomcat Request
對象。Tomcat Request
對象轉成標準的 ServletRequest
。Servlet
容器,得到 ServletResponse
。ServletResponse
轉成 Tomcat Response
對象。Tomcat Response
轉成網(wǎng)絡(luò )字節流。將響應字節流寫(xiě)回給瀏覽器。需求列清楚后,我們要考慮的下一個(gè)問(wèn)題是,連接器應該有哪些子模塊??jì)?yōu)秀的模塊化設計應該考慮高內聚、低耦合。
我們發(fā)現連接器需要完成 3 個(gè)高內聚的功能:
Tomcat Request/Response
與 ServletRequest/ServletResponse
的轉化。因此 Tomcat 的設計者設計了 3 個(gè)組件來(lái)實(shí)現這 3 個(gè)功能,分別是 EndPoint、Processor 和 Adapter
。
網(wǎng)絡(luò )通信的 I/O 模型是變化的, 應用層協(xié)議也是變化的,但是整體的處理邏輯是不變的,EndPoint
負責提供字節流給 Processor
,Processor
負責提供 Tomcat Request
對象給 Adapter
,Adapter
負責提供 ServletRequest
對象給容器。
因此 Tomcat 設計了一系列抽象基類(lèi)來(lái)封裝這些穩定的部分,抽象基類(lèi) AbstractProtocol
實(shí)現了 ProtocolHandler
接口。每一種應用層協(xié)議有自己的抽象基類(lèi),比如 AbstractAjpProtocol
和 AbstractHttp11Protocol
,具體協(xié)議的實(shí)現類(lèi)擴展了協(xié)議層抽象基類(lèi)。
這就是模板方法設計模式的運用。
總結下來(lái),連接器的三個(gè)核心組件 Endpoint
、Processor
和 Adapter
來(lái)分別做三件事情,其中 Endpoint
和 Processor
放在一起抽象成了 ProtocolHandler
組件,它們的關(guān)系如下圖所示。
ProtocolHandler 組件:
主要處理 網(wǎng)絡(luò )連接 和 應用層協(xié)議 ,包含了兩個(gè)重要部件 EndPoint 和 Processor,兩個(gè)組件組合形成 ProtocoHandler,下面我來(lái)詳細介紹它們的工作原理。
EndPoint:
EndPoint
是通信端點(diǎn),即通信監聽(tīng)的接口,是具體的 Socket 接收和發(fā)送處理器,是對傳輸層的抽象,因此 EndPoint
是用來(lái)實(shí)現 TCP/IP
協(xié)議數據讀寫(xiě)的,本質(zhì)調用操作系統的 socket 接口。
EndPoint
是一個(gè)接口,對應的抽象實(shí)現類(lèi)是 AbstractEndpoint
,而 AbstractEndpoint
的具體子類(lèi),比如在 NioEndpoint
和 Nio2Endpoint
中,有兩個(gè)重要的子組件:Acceptor
和 SocketProcessor
。
其中 Acceptor 用于監聽(tīng) Socket 連接請求。SocketProcessor
用于處理 Acceptor
接收到的 Socket
請求,它實(shí)現 Runnable
接口,在 Run
方法里調用應用層協(xié)議處理組件 Processor
進(jìn)行處理。為了提高處理能力,SocketProcessor
被提交到線(xiàn)程池來(lái)執行。
我們知道,對于 Java 的多路復用器的使用,無(wú)非是兩步:
在 Tomcat 中 NioEndpoint
則是 AbstractEndpoint
的具體實(shí)現,里面組件雖然很多,但是處理邏輯還是前面兩步。它一共包含 LimitLatch
、Acceptor
、Poller
、SocketProcessor
和 Executor
共 5 個(gè)組件,分別分工合作實(shí)現整個(gè) TCP/IP 協(xié)議的處理。
LimitLatch 是連接控制器,它負責控制最大連接數,NIO 模式下默認是 10000,達到這個(gè)閾值后,連接請求被拒絕。
Acceptor
跑在一個(gè)單獨的線(xiàn)程里,它在一個(gè)死循環(huán)里調用 accept
方法來(lái)接收新連接,一旦有新的連接請求到來(lái),accept
方法返回一個(gè) Channel
對象,接著(zhù)把 Channel
對象交給 Poller 去處理。
Poller
的本質(zhì)是一個(gè) Selector
,也跑在單獨線(xiàn)程里。Poller
在內部維護一個(gè) Channel
數組,它在一個(gè)死循環(huán)里不斷檢測 Channel
的數據就緒狀態(tài),一旦有 Channel
可讀,就生成一個(gè) SocketProcessor
任務(wù)對象扔給 Executor
去處理。
SocketProcessor 實(shí)現了 Runnable 接口,其中 run 方法中的 getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
代碼則是獲取 handler 并執行處理 socketWrapper,最后通過(guò) socket 獲取合適應用層協(xié)議處理器,也就是調用 Http11Processor 組件來(lái)處理請求。Http11Processor 讀取 Channel 的數據來(lái)生成 ServletRequest 對象,Http11Processor 并不是直接讀取 Channel 的。這是因為 Tomcat 支持同步非阻塞 I/O 模型和異步 I/O 模型,在 Java API 中,相應的 Channel 類(lèi)也是不一樣的,比如有 AsynchronousSocketChannel 和 SocketChannel,為了對 Http11Processor 屏蔽這些差異,Tomcat 設計了一個(gè)包裝類(lèi)叫作 SocketWrapper,Http11Processor 只調用 SocketWrapper 的方法去讀寫(xiě)數據。
Executor
就是線(xiàn)程池,負責運行 SocketProcessor
任務(wù)類(lèi),SocketProcessor
的 run
方法會(huì )調用 Http11Processor
來(lái)讀取和解析請求數據。我們知道,Http11Processor
是應用層協(xié)議的封裝,它會(huì )調用容器獲得響應,再把響應通過(guò) Channel
寫(xiě)出。
工作流程如下所示:
Processor:
Processor 用來(lái)實(shí)現 HTTP 協(xié)議,Processor 接收來(lái)自 EndPoint 的 Socket,讀取字節流解析成 Tomcat Request 和 Response 對象,并通過(guò) Adapter 將其提交到容器處理,Processor 是對應用層協(xié)議的抽象。
從圖中我們看到,EndPoint 接收到 Socket 連接后,生成一個(gè) SocketProcessor 任務(wù)提交到線(xiàn)程池去處理,SocketProcessor 的 Run 方法會(huì )調用 HttpProcessor 組件去解析應用層協(xié)議,Processor 通過(guò)解析生成 Request 對象后,會(huì )調用 Adapter 的 Service 方法,方法內部通過(guò) 以下代碼將請求傳遞到容器中。
// Calling the container connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Adapter 組件:
由于協(xié)議的不同,Tomcat 定義了自己的 Request
類(lèi)來(lái)存放請求信息,這里其實(shí)體現了面向對象的思維。但是這個(gè) Request 不是標準的 ServletRequest
,所以不能直接使用 Tomcat 定義 Request 作為參數直接容器。
Tomcat 設計者的解決方案是引入 CoyoteAdapter
,這是適配器模式的經(jīng)典運用,連接器調用 CoyoteAdapter
的 Sevice
方法,傳入的是 Tomcat Request
對象,CoyoteAdapter
負責將 Tomcat Request
轉成 ServletRequest
,再調用容器的 Service
方法。
連接器負責外部交流,容器負責內部處理。具體來(lái)說(shuō)就是,連接器處理 Socket 通信和應用層協(xié)議的解析,得到 Servlet
請求;而容器則負責處理 Servlet
請求。
容器:顧名思義就是拿來(lái)裝東西的, 所以 Tomcat 容器就是拿來(lái)裝載 Servlet
。
Tomcat 設計了 4 種容器,分別是 Engine
、Host
、Context
和 Wrapper
。Server
代表 Tomcat 實(shí)例。
要注意的是這 4 種容器不是平行關(guān)系,屬于父子關(guān)系,如下圖所示:
你可能會(huì )問(wèn),為啥要設計這么多層次的容器,這不是增加復雜度么?其實(shí)這背后的考慮是,Tomcat 通過(guò)一種分層的架構,使得 Servlet 容器具有很好的靈活性。因為這里正好符合一個(gè) Host 多個(gè) Context, 一個(gè) Context 也包含多個(gè) Servlet,而每個(gè)組件都需要統一生命周期管理,所以組合模式設計這些容器
Wrapper
表示一個(gè) Servlet
,Context
表示一個(gè) Web 應用程序,而一個(gè) Web 程序可能有多個(gè) Servlet
;Host
表示一個(gè)虛擬主機,或者說(shuō)一個(gè)站點(diǎn),一個(gè) Tomcat 可以配置多個(gè)站點(diǎn)(Host);一個(gè)站點(diǎn)( Host) 可以部署多個(gè) Web 應用;Engine
代表 引擎,用于管理多個(gè)站點(diǎn)(Host),一個(gè) Service 只能有 一個(gè) Engine
。
可通過(guò) 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下的客戶(hù)端請求, 可包含多個(gè) Context <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> // 容器組件:處理特定 Context Web應用的所有客戶(hù)端請求 <Context></Context> </Host> </Engine> </Service> </Server>
如何管理這些容器?我們發(fā)現容器之間具有父子關(guān)系,形成一個(gè)樹(shù)形結構,是不是想到了設計模式中的 組合模式 。
Tomcat 就是用組合模式來(lái)管理這些容器的。具體實(shí)現方法是,所有容器組件都實(shí)現了 Container
接口,因此組合模式可以使得用戶(hù)對單容器對象和組合容器對象的使用具有一致性。這里單容器對象指的是最底層的 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
等方法,這里正好驗證了我們說(shuō)的組合模式。我們還看到 Container
接口拓展了 Lifecycle
,Tomcat 就是通過(guò) Lifecycle
統一管理所有容器的組件的生命周期。通過(guò)組合模式管理所有容器,拓展 Lifecycle
實(shí)現對每個(gè)組件的生命周期管理 ,Lifecycle
主要包含的方法init()、start()、stop() 和 destroy()
。
一個(gè)請求是如何定位到讓哪個(gè) Wrapper
的 Servlet
處理的?答案是,Tomcat 是用 Mapper 組件來(lái)完成這個(gè)任務(wù)的。
Mapper
組件的功能就是將用戶(hù)請求的 URL
定位到一個(gè) Servlet
,它的工作原理是:Mapper
組件里保存了 Web 應用的配置信息,其實(shí)就是容器組件與訪(fǎng)問(wèn)路徑的映射關(guān)系,比如 Host
容器里配置的域名、Context
容器里的 Web
應用路徑,以及 Wrapper
容器里 Servlet
映射的路徑,你可以想象這些配置信息就是一個(gè)多層次的 Map
。
當一個(gè)請求到來(lái)時(shí),Mapper
組件通過(guò)解析請求 URL 里的域名和路徑,再到自己保存的 Map 里去查找,就能定位到一個(gè) Servlet
。請你注意,一個(gè)請求 URL 最后只會(huì )定位到一個(gè) Wrapper
容器,也就是一個(gè) Servlet
。
假如有用戶(hù)訪(fǎng)問(wèn)一個(gè) URL,比如圖中的http://user.shopping.com:8080/order/buy
,Tomcat 如何將這個(gè) URL 定位到一個(gè) Servlet 呢?
1.首先根據協(xié)議和端口號確定 Service 和 Engine。Tomcat 默認的 HTTP 連接器監聽(tīng) 8080 端口、默認的 AJP 連接器監聽(tīng) 8009 端口。上面例子中的 URL 訪(fǎng)問(wèn)的是 8080 端口,因此這個(gè)請求會(huì )被 HTTP 連接器接收,而一個(gè)連接器是屬于一個(gè) Service 組件的,這樣 Service 組件就確定了。我們還知道一個(gè) Service 組件里除了有多個(gè)連接器,還有一個(gè)容器組件,具體來(lái)說(shuō)就是一個(gè) Engine 容器,因此 Service 確定了也就意味著(zhù) Engine 也確定了。
2.根據域名選定 Host。 Service 和 Engine 確定后,Mapper 組件通過(guò) URL 中的域名去查找相應的 Host 容器,比如例子中的 URL 訪(fǎng)問(wèn)的域名是user.shopping.com
,因此 Mapper 會(huì )找到 Host2 這個(gè)容器。
3.根據 URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據 URL 的路徑來(lái)匹配相應的 Web 應用的路徑,比如例子中訪(fǎng)問(wèn)的是 /order,因此找到了 Context4 這個(gè) Context 容器。
4.根據 URL 路徑找到 Wrapper(Servlet)。 Context 確定后,Mapper 再根據 web.xml 中配置的 Servlet 映射路徑來(lái)找到具體的 Wrapper 和 Servlet。
連接器中的 Adapter 會(huì )調用容器的 Service 方法來(lái)執行 Servlet,最先拿到請求的是 Engine 容器,Engine 容器對請求做一些處理后,會(huì )把請求傳給自己子容器 Host 繼續處理,依次類(lèi)推,最后這個(gè)請求會(huì )傳給 Wrapper 容器,Wrapper 會(huì )調用最終的 Servlet 來(lái)處理。那么這個(gè)調用過(guò)程具體是怎么實(shí)現的呢?答案是使用 Pipeline-Valve 管道。
Pipeline-Valve
是責任鏈模式,責任鏈模式是指在一個(gè)請求處理的過(guò)程中有很多處理者依次對請求進(jìn)行處理,每個(gè)處理者負責做自己相應的處理,處理完之后將再調用下一個(gè)處理者繼續處理,Valve 表示一個(gè)處理點(diǎn)(也就是一個(gè)處理閥門(mén)),因此 invoke
方法就是來(lái)處理請求的。
public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void invoke(Request request, Response response) }
繼續看 Pipeline 接口
public interface Pipeline { public void addValve(Valve valve); public Valve getBasic(); public void setBasic(Valve valve); public Valve getFirst(); }
Pipeline
中有 addValve
方法。Pipeline 中維護了 Valve
鏈表,Valve
可以插入到 Pipeline
中,對請求做某些處理。我們還發(fā)現 Pipeline 中沒(méi)有 invoke 方法,因為整個(gè)調用鏈的觸發(fā)是 Valve 來(lái)完成的,Valve
完成自己的處理后,調用 getNext.invoke()
來(lái)觸發(fā)下一個(gè) Valve 調用。
其實(shí)每個(gè)容器都有一個(gè) Pipeline 對象,只要觸發(fā)了這個(gè) Pipeline 的第一個(gè) Valve,這個(gè)容器里 Pipeline
中的 Valve 就都會(huì )被調用到。但是,不同容器的 Pipeline 是怎么鏈式觸發(fā)的呢,比如 Engine 中 Pipeline 需要調用下層容器 Host 中的 Pipeline。
這是因為 Pipeline
中還有個(gè) getBasic
方法。這個(gè) BasicValve
處于 Valve
鏈表的末端,它是 Pipeline
中必不可少的一個(gè) Valve
,負責調用下層容器的 Pipeline 里的第一個(gè) Valve。
整個(gè)過(guò)程分是通過(guò)連接器中的 CoyoteAdapter
觸發(fā),它會(huì )調用 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 鏈,并調用 doFilter()
方法,最終會(huì )調到 Servlet
的 service
方法。
前面我們不是講到了 Filter
,似乎也有相似的功能,那 Valve
和 Filter
有什么區別嗎?它們的區別是:
Valve
是 Tomcat
的私有機制,與 Tomcat 的基礎架構 API
是緊耦合的。Servlet API
是公有的標準,所有的 Web 容器包括 Jetty 都支持 Filter 機制。Valve
工作在 Web 容器級別,攔截所有應用的請求;而 Servlet Filter
工作在應用級別,只能攔截某個(gè) Web
應用的所有請求。如果想做整個(gè) Web
容器的攔截器,必須通過(guò) Valve
來(lái)實(shí)現。Lifecycle 生命周期
前面我們看到 Container
容器 繼承了 Lifecycle
生命周期。如果想讓一個(gè)系統能夠對外提供服務(wù),我們需要創(chuàng )建、組裝并啟動(dòng)這些組件;在服務(wù)停止的時(shí)候,我們還需要釋放資源,銷(xiāo)毀這些組件,因此這是一個(gè)動(dòng)態(tài)的過(guò)程。也就是說(shuō),Tomcat 需要動(dòng)態(tài)地管理這些組件的生命周期。
如何統一管理組件的創(chuàng )建、初始化、啟動(dòng)、停止和銷(xiāo)毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動(dòng)和停止不遺漏、不重復?
一鍵式啟停:LifeCycle 接口
設計就是要找到系統的變化點(diǎn)和不變點(diǎn)。這里的不變點(diǎn)就是每個(gè)組件都要經(jīng)歷創(chuàng )建、初始化、啟動(dòng)這幾個(gè)過(guò)程,這些狀態(tài)以及狀態(tài)的轉化是不變的。而變化點(diǎn)是每個(gè)具體組件的初始化方法,也就是啟動(dòng)方法是不一樣的。
因此,Tomcat 把不變點(diǎn)抽象出來(lái)成為一個(gè)接口,這個(gè)接口跟生命周期有關(guān),叫作 LifeCycle。LifeCycle 接口里定義這么幾個(gè)方法:init()、start()、stop() 和 destroy()
,每個(gè)具體的組件(也就是容器)去實(shí)現這些方法。
在父組件的 init()
方法里需要創(chuàng )建子組件并調用子組件的 init()
方法。同樣,在父組件的 start()
方法里也需要調用子組件的 start()
方法,因此調用者可以無(wú)差別的調用各組件的 init()
方法和 start()
方法,這就是組合模式的使用,并且只要調用最頂層組件,也就是 Server 組件的 init()
和start()
方法,整個(gè) Tomcat 就被啟動(dòng)起來(lái)了。所以 Tomcat 采取組合模式管理容器,容器繼承 LifeCycle 接口,這樣就可以向針對單個(gè)對象一樣一鍵管理各個(gè)容器的生命周期,整個(gè) Tomcat 就啟動(dòng)起來(lái)。
可擴展性:LifeCycle 事件
我們再來(lái)考慮另一個(gè)問(wèn)題,那就是系統的可擴展性。因為各個(gè)組件init()
和 start()
方法的具體實(shí)現是復雜多變的,比如在 Host 容器的啟動(dòng)方法里需要掃描 webapps 目錄下的 Web 應用,創(chuàng )建相應的 Context 容器,如果將來(lái)需要增加新的邏輯,直接修改start()
方法?這樣會(huì )違反開(kāi)閉原則,那如何解決這個(gè)問(wèn)題呢?開(kāi)閉原則說(shuō)的是為了擴展系統的功能,你不能直接修改系統中已有的類(lèi),但是你可以定義新的類(lèi)。
組件的 init()
和 start()
調用是由它的父組件的狀態(tài)變化觸發(fā)的,上層組件的初始化會(huì )觸發(fā)子組件的初始化,上層組件的啟動(dòng)會(huì )觸發(fā)子組件的啟動(dòng),因此我們把組件的生命周期定義成一個(gè)個(gè)狀態(tài),把狀態(tài)的轉變看作是一個(gè)事件。而事件是有監聽(tīng)器的,在監聽(tīng)器里可以實(shí)現一些邏輯,并且監聽(tīng)器也可以方便的添加和刪除,這就是典型的觀(guān)察者模式。
以下就是 Lyfecycle
接口的定義:
重用性:LifeCycleBase 抽象基類(lèi)
再次看到抽象模板設計模式。
有了接口,我們就要用類(lèi)去實(shí)現接口。一般來(lái)說(shuō)實(shí)現類(lèi)不止一個(gè),不同的類(lèi)在實(shí)現接口時(shí)往往會(huì )有一些相同的邏輯,如果讓各個(gè)子類(lèi)都去實(shí)現一遍,就會(huì )有重復代碼。那子類(lèi)如何重用這部分邏輯呢?其實(shí)就是定義一個(gè)基類(lèi)來(lái)實(shí)現共同的邏輯,然后讓各個(gè)子類(lèi)去繼承它,就達到了重用的目的。
Tomcat 定義一個(gè)基類(lèi) LifeCycleBase 來(lái)實(shí)現 LifeCycle 接口,把一些公共的邏輯放到基類(lèi)中去,比如生命狀態(tài)的轉變與維護、生命事件的觸發(fā)以及監聽(tīng)器的添加和刪除等,而子類(lèi)就負責實(shí)現自己的初始化、啟動(dòng)和停止等方法。
public abstract class LifecycleBase implements Lifecycle{ // 持有所有的觀(guān)察者 private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>(); /** * 發(fā)布事件 * * @param type Event type * @param data Data associated with event. */ protected void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(this, type, data); for (LifecycleListener listener : lifecycleListeners) { listener.lifecycleEvent(event); } } // 模板方法定義整個(gè)啟動(dòng)流程,啟動(dòng)所有容器 @Override public final synchronized void init() throws LifecycleException { //1. 狀態(tài)檢查 if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { //2. 觸發(fā) INITIALIZING 事件的監聽(tīng)器 setStateInternal(LifecycleState.INITIALIZING, null, false); // 3. 調用具體子類(lèi)的初始化方法 initInternal(); // 4. 觸發(fā) INITIALIZED 事件的監聽(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í)現一鍵式啟停以及優(yōu)雅的生命周期管理,并考慮到了可擴展性和可重用性,將面向對象思想和設計模式發(fā)揮到了極致,Containaer
接口維護了容器的父子關(guān)系,Lifecycle
組合模式實(shí)現組件的生命周期維護,生命周期每個(gè)組件有變與不變的點(diǎn),運用模板方法模式。 分別運用了組合模式、觀(guān)察者模式、骨架抽象類(lèi)和模板方法。
如果你需要維護一堆具有父子關(guān)系的實(shí)體,可以考慮使用組合模式。
觀(guān)察者模式聽(tīng)起來(lái) “高大上”,其實(shí)就是當一個(gè)事件發(fā)生后,需要執行一連串更新操作。實(shí)現了低耦合、非侵入式的通知與更新機制。
Container
繼承了 LifeCycle,StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應容器組件的具體實(shí)現類(lèi),因為它們都是容器,所以繼承了 ContainerBase 抽象基類(lèi),而 ContainerBase 實(shí)現了 Container 接口,也繼承了 LifeCycleBase 類(lèi),它們的生命周期管理接口和功能接口是分開(kāi)的,這也符合設計中接口分離的原則。
我們知道 JVM
的類(lèi)加載器加載 Class 的時(shí)候基于雙親委派機制,也就是會(huì )將加載交給自己的父加載器加載,如果 父加載器為空則查找Bootstrap
是否加載過(guò),當無(wú)法加載的時(shí)候才讓自己加載。JDK 提供一個(gè)抽象類(lèi) ClassLoader
,這個(gè)抽象類(lèi)中定義了三個(gè)關(guān)鍵方法。對外使用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) { // 委托給父加載器去加載,遞歸調用 if (parent != null) { c = parent.loadClass(name, false); } else { // 如果父加載器為空,查找 Bootstrap 是否加載過(guò) c = findBootstrapClassOrNull(name); } // 若果依然加載不到,則調用自己的 findClass 去加載 if (c == null) { c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } protected Class<?> findClass(String name){ //1. 根據傳入的類(lèi)名 name,到在特定目錄下去尋找類(lèi)文件,把.class 文件讀入內存 ... //2. 調用 defineClass 將字節數組轉成 Class 對象 return defineClass(buf, off, len); } // 將字節碼數組解析成一個(gè) Class 對象,用 native 方法實(shí)現 protected final Class<?> defineClass(byte[] b, int off, int len){ ... }
JDK 中有 3 個(gè)類(lèi)加載器,另外你也可以自定義類(lèi)加載器,它們的關(guān)系如下圖所示。
BootstrapClassLoader
是啟動(dòng)類(lèi)加載器,由 C 語(yǔ)言實(shí)現,用來(lái)加載 JVM
啟動(dòng)時(shí)所需要的核心類(lèi),比如rt.jar
、resources.jar
等。ExtClassLoader
是擴展類(lèi)加載器,用來(lái)加載\jre\lib\ext
目錄下 JAR 包。AppClassLoader
是系統類(lèi)加載器,用來(lái)加載 classpath
下的類(lèi),應用程序默認用它來(lái)加載類(lèi)。這些類(lèi)加載器的工作原理是一樣的,區別是它們的加載路徑不同,也就是說(shuō) findClass
這個(gè)方法查找的路徑不同。雙親委托機制是為了保證一個(gè) Java 類(lèi)在 JVM 中是唯一的,假如你不小心寫(xiě)了一個(gè)與 JRE 核心類(lèi)同名的類(lèi),比如 Object
類(lèi),雙親委托機制能保證加載的是 JRE
里的那個(gè) Object
類(lèi),而不是你寫(xiě)的 Object
類(lèi)。這是因為 AppClassLoader
在加載你的 Object 類(lèi)時(shí),會(huì )委托給 ExtClassLoader
去加載,而 ExtClassLoader
又會(huì )委托給 BootstrapClassLoader
,BootstrapClassLoader
發(fā)現自己已經(jīng)加載過(guò)了 Object
類(lèi),會(huì )直接返回,不會(huì )去加載你寫(xiě)的 Object
類(lèi)。我們最多只能 獲取到 ExtClassLoader
這里注意下。
Tomcat 本質(zhì)是通過(guò)一個(gè)后臺線(xiàn)程做周期性的任務(wù),定期檢測類(lèi)文件的變化,如果有變化就重新加載類(lèi)。我們來(lái)看 ContainerBackgroundProcessor
具體是如何實(shí)現的。
protected class ContainerBackgroundProcessor implements Runnable { @Override public void run() { // 請注意這里傳入的參數是 " 宿主類(lèi) " 的實(shí)例 processChildren(ContainerBase.this); } protected void processChildren(Container container) { try { //1. 調用當前容器的 backgroundProcess 方法。 container.backgroundProcess(); //2. 遍歷所有的子容器,遞歸調用 processChildren, // 這樣當前容器的子孫都會(huì )被處理 Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { // 這里請你注意,容器基類(lèi)有個(gè)變量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后臺線(xiàn)程,無(wú)需父容器來(lái)調用它的 processChildren 方法。 if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i]); } } } catch (Throwable t) { ... }
Tomcat 的熱加載就是在 Context 容器實(shí)現,主要是調用了 Context 容器的 reload 方法。拋開(kāi)細節從宏觀(guān)上看主要完成以下任務(wù):
在這個(gè)過(guò)程中,類(lèi)加載器發(fā)揮著(zhù)關(guān)鍵作用。一個(gè) Context 容器對應一個(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)加載器 WebAppClassLoader
打破了雙親委托機制,它首先自己嘗試去加載某個(gè)類(lèi),如果找不到再代理給父類(lèi)加載器,其目的是優(yōu)先加載 Web 應用自己定義的類(lèi)。具體實(shí)現就是重寫(xiě) ClassLoader
的兩個(gè)方法:findClass
和 loadClass
。
findClass 方法
org.apache.catalina.loader.WebappClassLoaderBase#findClass;
為了方便理解和閱讀,我去掉了一些細節:
public Class<?> findClass(String name) throws ClassNotFoundException { ... Class<?> clazz = null; try { //1. 先在 Web 應用目錄下查找類(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 應用本地目錄下查找要加載的類(lèi)。
2.如果沒(méi)有找到,交給父加載器去查找,它的父加載器就是上面提到的系統類(lèi)加載器 AppClassLoader
。
3.如何父加載器也沒(méi)找到這個(gè)類(lèi),拋出 ClassNotFound
異常。
loadClass 方法
再來(lái)看 Tomcat 類(lèi)加載器的 loadClass
方法的實(shí)現,同樣我也去掉了一些細節:
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. 從系統類(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. 嘗試用系統類(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),再看看系統類(lèi)加載器是否加載過(guò)。
3.如果都沒(méi)有,就讓ExtClassLoader去加載,這一步比較關(guān)鍵,目的 防止 Web 應用自己的類(lèi)覆蓋 JRE 的核心類(lèi)。因為 Tomcat 需要打破雙親委托機制,假如 Web 應用里自定義了一個(gè)叫 Object 的類(lèi),如果先加載這個(gè) Object 類(lèi),就會(huì )覆蓋 JRE 里面的那個(gè) Object 類(lèi),這就是為什么 Tomcat 的類(lèi)加載器會(huì )優(yōu)先嘗試用 ExtClassLoader
去加載,因為 ExtClassLoader
會(huì )委托給 BootstrapClassLoader
去加載,BootstrapClassLoader
發(fā)現自己已經(jīng)加載了 Object 類(lèi),直接返回給 Tomcat 的類(lèi)加載器,這樣 Tomcat 的類(lèi)加載器就不會(huì )去加載 Web 應用下的 Object 類(lèi)了,也就避免了覆蓋 JRE 核心類(lèi)的問(wèn)題。
4.如果 ExtClassLoader
加載器加載失敗,也就是說(shuō) JRE
核心類(lèi)中沒(méi)有這類(lèi),那么就在本地 Web 應用目錄下查找并加載。
5.如果本地目錄下沒(méi)有這個(gè)類(lèi),說(shuō)明不是 Web 應用自己定義的類(lèi),那么由系統類(lèi)加載器去加載。這里請你注意,Web 應用是通過(guò)Class.forName
調用交給系統類(lèi)加載器的,因為Class.forName
的默認加載器就是系統類(lèi)加載器。
6.如果上述加載過(guò)程全部失敗,拋出 ClassNotFound
異常。
Tomcat 作為 Servlet
容器,它負責加載我們的 Servlet
類(lèi),此外它還負責加載 Servlet
所依賴(lài)的 JAR 包。并且 Tomcat
本身也是也是一個(gè) Java 程序,因此它需要加載自己的類(lèi)和依賴(lài)的 JAR 包。首先讓我們思考這一下這幾個(gè)問(wèn)題:
1.假如我們在 Tomcat 中運行了兩個(gè) Web 應用程序,兩個(gè) Web 應用中有同名的 Servlet
,但是功能不同,Tomcat 需要同時(shí)加載和管理這兩個(gè)同名的 Servlet
類(lèi),保證它們不會(huì )沖突,因此 Web 應用之間的類(lèi)需要隔離。
2.假如兩個(gè) Web 應用都依賴(lài)同一個(gè)第三方的 JAR 包,比如 Spring
,那 Spring
的 JAR 包被加載到內存后,Tomcat
要保證這兩個(gè) Web 應用能夠共享,也就是說(shuō) Spring
的 JAR 包只被加載一次,否則隨著(zhù)依賴(lài)的第三方 JAR 包增多,JVM
的內存會(huì )膨脹。
3.跟 JVM 一樣,我們需要隔離 Tomcat 本身的類(lèi)和 Web 應用的類(lèi)。
1. WebAppClassLoader
Tomcat 的解決方案是自定義一個(gè)類(lèi)加載器 WebAppClassLoader
, 并且給每個(gè) Web 應用創(chuàng )建一個(gè)類(lèi)加載器實(shí)例。我們知道,Context 容器組件對應一個(gè) Web 應用,因此,每個(gè) Context
容器負責創(chuàng )建和維護一個(gè) WebAppClassLoader
加載器實(shí)例。這背后的原理是,不同的加載器實(shí)例加載的類(lèi)被認為是不同的類(lèi),即使它們的類(lèi)名相同。這就相當于在 Java 虛擬機內部創(chuàng )建了一個(gè)個(gè)相互隔離的 Java 類(lèi)空間,每一個(gè) Web 應用都有自己的類(lèi)空間,Web 應用之間通過(guò)各自的類(lèi)加載器互相隔離。
2.SharedClassLoader
本質(zhì)需求是兩個(gè) Web 應用之間怎么共享庫類(lèi),并且不能重復加載相同的類(lèi)。在雙親委托機制里,各個(gè)子加載器都可以通過(guò)父加載器去加載類(lèi),那么把需要共享的類(lèi)放到父加載器的加載路徑下不就行了嗎。
因此 Tomcat 的設計者又加了一個(gè)類(lèi)加載器 SharedClassLoader
,作為 WebAppClassLoader
的父加載器,專(zhuān)門(mén)來(lái)加載 Web 應用之間共享的類(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 應用的類(lèi)?
要共享可以通過(guò)父子關(guān)系,要隔離那就需要兄弟關(guān)系了。兄弟關(guān)系就是指兩個(gè)類(lèi)加載器是平行的,它們可能擁有同一個(gè)父加載器,基于此 Tomcat 又設計一個(gè)類(lèi)加載器 CatalinaClassloader
,專(zhuān)門(mén)來(lái)加載 Tomcat 自身的類(lèi)。
這樣設計有個(gè)問(wèn)題,那 Tomcat 和各 Web 應用之間需要共享一些類(lèi)時(shí)該怎么辦呢?
老辦法,還是再增加一個(gè) CommonClassLoader
,作為 CatalinaClassloader
和 SharedClassLoader
的父加載器。CommonClassLoader
能加載的類(lèi)都可以被 CatalinaClassLoader
和 SharedClassLoader
使用
通過(guò)前面對 Tomcat 整體架構的學(xué)習,知道了 Tomcat 有哪些核心組件,組件之間的關(guān)系。以及 Tomcat 是怎么處理一個(gè) HTTP 請求的。下面我們通過(guò)一張簡(jiǎn)化的類(lèi)圖來(lái)回顧一下,從圖上你可以看到各種組件的層次關(guān)系,圖中的虛線(xiàn)表示一個(gè)請求在 Tomcat 中流轉的過(guò)程。
Tomcat 的整體架構包含了兩個(gè)核心組件連接器和容器。連接器負責對外交流,容器負責內部處理。連接器用 ProtocolHandler
接口來(lái)封裝通信協(xié)議和 I/O
模型的差異,ProtocolHandler
內部又分為 EndPoint
和 Processor
模塊,EndPoint
負責底層 Socket
通信,Proccesor
負責應用層協(xié)議解析。連接器通過(guò)適配器 Adapter
調用容器。
對 Tomcat 整體架構的學(xué)習,我們可以得到一些設計復雜系統的基本思路。首先要分析需求,根據高內聚低耦合的原則確定子模塊,然后找出子模塊中的變化點(diǎn)和不變點(diǎn),用接口和抽象基類(lèi)去封裝不變點(diǎn),在抽象基類(lèi)中定義模板方法,讓子類(lèi)自行實(shí)現抽象方法,也就是具體子類(lèi)去實(shí)現變化點(diǎn)。
運用了組合模式 管理容器、通過(guò) 觀(guān)察者模式 發(fā)布啟動(dòng)事件達到解耦、開(kāi)閉原則。骨架抽象類(lèi)和模板方法抽象變與不變,變化的交給子類(lèi)實(shí)現,從而實(shí)現代碼復用,以及靈活的拓展。使用責任鏈的方式處理請求,比如記錄日志等。
Tomcat 的自定義類(lèi)加載器 WebAppClassLoader
為了隔離 Web 應用打破了雙親委托機制,它首先自己嘗試去加載某個(gè)類(lèi),如果找不到再代理給父類(lèi)加載器,其目的是優(yōu)先加載 Web 應用自己定義的類(lèi)。防止 Web 應用自己的類(lèi)覆蓋 JRE 的核心類(lèi),使用 ExtClassLoader 去加載,這樣即打破了雙親委派,又能安全加載。
簡(jiǎn)單的分析了 Tomcat 整體架構設計,從 【連接器】 到 【容器】,并且分別細說(shuō)了一些組件的設計思想以及設計模式。接下來(lái)就是如何學(xué)以致用,借鑒優(yōu)雅的設計運用到實(shí)際工作開(kāi)發(fā)中。學(xué)習,從模仿開(kāi)始。
在工作中,有這么一個(gè)需求,用戶(hù)可以輸入一些信息并可以選擇查驗該企業(yè)的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個(gè)或者多個(gè)模塊,而且模塊之間還有一些公共的東西是要各個(gè)模塊復用。
這里就像一個(gè)請求,會(huì )被多個(gè)模塊去處理。所以每個(gè)查詢(xún)模塊我們可以抽象為 處理閥門(mén),使用一個(gè) List 將這些 閥門(mén)保存起來(lái),這樣新增模塊我們只需要新增一個(gè)閥門(mén)即可,實(shí)現了開(kāi)閉原則,同時(shí)將一堆查驗的代碼解耦到不同的具體閥門(mén)中,使用抽象類(lèi)提取 “不變的”功能。
具體示例代碼如下所示:
首先抽象我們的處理閥門(mén), NetCheckDTO
是請求信息
/** * 責任鏈模式:處理每個(gè)模塊閥門(mén) */ public interface Valve { /** * 調用 * @param netCheckDTO */ void invoke(NetCheckDTO netCheckDTO); }
定義抽象基類(lèi),復用代碼。
public abstract class AbstractCheckValve implements Valve { public final AnalysisReportLogDO getLatestHistoryData(NetCheckDTO netCheckDTO, NetCheckDataTypeEnum checkDataTypeEnum){ // 獲取歷史記錄,省略代碼邏輯 } // 獲取查驗數據源配置 public final String getModuleSource(String querySource, ModuleEnum moduleEnum){ // 省略代碼邏輯 } }
定義具體每個(gè)模塊處理的業(yè)務(wù)邏輯,比如 【百度負面新聞】對應的處理
@Slf4j @Service public class BaiduNegativeValve extends AbstractCheckValve { @Override public void invoke(NetCheckDTO netCheckDTO) { } }
最后就是管理用戶(hù)選擇要查驗的模塊,我們通過(guò) List 保存。用于觸發(fā)所需要的查驗模塊
@Slf4j @Service public class NetCheckService { // 注入所有的閥門(mén) @Autowired private Map<String, Valve> valveMap; /** * 發(fā)送查驗請求 * * @param netCheckDTO */ @Async("asyncExecutor") public void sendCheckRequest(NetCheckDTO netCheckDTO) { // 用于保存客戶(hù)選擇處理的模塊閥門(mén) List<Valve> valves = new ArrayList<>(); CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig(); // 將用戶(hù)選擇查驗的模塊添加到 閥門(mén)鏈條中 if (checkModuleConfig.getBaiduNegative()) { valves.add(valveMap.get("baiduNegativeValve")); } // 省略部分代碼....... if (CollectionUtils.isEmpty(valves)) { log.info("網(wǎng)查查驗模塊為空,沒(méi)有需要查驗的任務(wù)"); return; } // 觸發(fā)處理 valves.forEach(valve -> valve.invoke(netCheckDTO)); } }
需求是這樣的,可根據客戶(hù)錄入的財報 excel 數據或者企業(yè)名稱(chēng)執行財報分析。
對于非上市的則解析 excel -> 校驗數據是否合法->執行計算。
上市企業(yè):判斷名稱(chēng)是否存在 ,不存在則發(fā)送郵件并中止計算-> 從數據庫拉取財報數據,初始化查驗日志、生成一條報告記錄,觸發(fā)計算-> 根據失敗與成功修改任務(wù)狀態(tài) 。
重要的 ”變“ 與 ”不變“,
整個(gè)算法流程是固定的模板,但是需要將算法內部變化的部分具體實(shí)現延遲到不同子類(lèi)實(shí)現,這正是模板方法模式的最佳場(chǎng)景。
public abstract class AbstractAnalysisTemplate { /** * 提交財報分析模板方法,定義骨架流程 * @param reportAnalysisRequest * @return */ public final FinancialAnalysisResultDTO doProcess(FinancialReportAnalysisRequest reportAnalysisRequest) { FinancialAnalysisResultDTO analysisDTO = new FinancialAnalysisResultDTO(); // 抽象方法:提交查驗的合法校驗 boolean prepareValidate = prepareValidate(reportAnalysisRequest, analysisDTO); log.info("prepareValidate 校驗結果 = {} ", prepareValidate); if (!prepareValidate) { // 抽象方法:構建通知郵件所需要的數據 buildEmailData(analysisDTO); log.info("構建郵件信息,data = {}", JSON.toJSONString(analysisDTO)); return analysisDTO; } String reportNo = FINANCIAL_REPORT_NO_PREFIX + reportAnalysisRequest.getUserId() + SerialNumGenerator.getFixLenthSerialNumber(); // 生成分析日志 initFinancialAnalysisLog(reportAnalysisRequest, reportNo); // 生成分析記錄 initAnalysisReport(reportAnalysisRequest, reportNo); try { // 抽象方法:拉取財報數據,不同子類(lèi)實(shí)現 FinancialDataDTO financialData = pullFinancialData(reportAnalysisRequest); log.info("拉取財報數據完成, 準備執行計算"); // 測算指標 financialCalcContext.calc(reportAnalysisRequest, financialData, reportNo); // 設置分析日志為成功 successCalc(reportNo); } catch (Exception e) { log.error("財報計算子任務(wù)出現異常", e); // 設置分析日志失敗 failCalc(reportNo); throw e; } return analysisDTO; } }
最后新建兩個(gè)子類(lèi)繼承該模板,并實(shí)現抽象方法。這樣就將上市與非上市兩種類(lèi)型的處理邏輯解耦,同時(shí)又復用了代碼。
需求是這樣,要做一個(gè)萬(wàn)能識別銀行流水的 excel 接口,假設標準流水包含【交易時(shí)間、收入、支出、交易余額、付款人賬號、付款人名字、收款人名稱(chēng)、收款人賬號】等字段?,F在我們解析出來(lái)每個(gè)必要字段所在 excel 表頭的下標。但是流水有多種情況:
1.一種就是包含所有標準字段。
2.收入、支出下標是同一列,通過(guò)正負來(lái)區分收入與支出。
3.收入與支出是同一列,有一個(gè)交易類(lèi)型的字段來(lái)區分。
4.特殊銀行的特殊處理。
也就是我們要根據解析對應的下標找到對應的處理邏輯算法,我們可能在一個(gè)方法里面寫(xiě)超多 if else
的代碼,整個(gè)流水處理都偶合在一起,假如未來(lái)再來(lái)一種新的流水類(lèi)型,還要繼續改老代碼。最后可能出現 “又臭又長(cháng),難以維護” 的代碼復雜度。
這個(gè)時(shí)候我們可以用到策略模式,將不同模板的流水使用不同的處理器處理,根據模板找到對應的策略算法去處理。即使未來(lái)再加一種類(lèi)型,我們只要新加一種處理器即可,高內聚低耦合,且可拓展。
定義處理器接口,不同處理器去實(shí)現處理邏輯。將所有的處理器注入到 BankFlowDataHandler
的data_processor_map
中,根據不同的場(chǎng)景取出對已經(jīng)的處理器處理流水。
public interface DataProcessor { /** * 處理流水數據 * @param bankFlowTemplateDO 流水下標數據 * @param row * @return */ BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO, List<String> row); /** * 是否支持處理該模板,不同類(lèi)型的流水策略根據模板數據判斷是否支持解析 * @return */ boolean isSupport(BankFlowTemplateDO bankFlowTemplateDO); } // 處理器的上下文 @Service @Slf4j public class BankFlowDataContext { // 將所有處理器注入到 map 中 @Autowired private List<DataProcessor> processors; // 找對對應的處理器處理流水 public void process() { DataProcessor processor = getProcessor(bankFlowTemplateDO); for(DataProcessor processor : processors) { if (processor.isSupport(bankFlowTemplateDO)) { // row 就是一行流水數據 processor.doProcess(bankFlowTemplateDO, row); break; } } } }
定義默認處理器,處理正常模板,新增模板只要新增處理器實(shí)現 DataProcessor
即可。
/** * 默認處理器:正對規范流水模板 * */ @Component("defaultDataProcessor") @Slf4j public class DefaultDataProcessor implements DataProcessor { @Override public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) { // 省略處理邏輯細節 return bankTransactionFlowDO; } @Override public String strategy(BankFlowTemplateDO bankFlowTemplateDO) { // 省略判斷是否支持解析該流水 boolean isDefault = true; return isDefault; } }
通過(guò)策略模式,我們將不同處理邏輯分配到不同的處理類(lèi)中,這樣完全解耦,便于拓展。
使用內嵌 Tomcat 方式調試源代碼:GitHub:
以上就是解析Tomcat架構原理到架構設計的詳細內容,更多關(guān)于Tomcat 架構原理 架構設計的資料請關(guān)注腳本之家其它相關(guān)文章!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站