主頁(yè) > 知識(shí)庫(kù) > 解析Tomcat架構(gòu)原理到架構(gòu)設(shè)計(jì)

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

熱門標(biāo)簽:10086外包用的什么外呼系統(tǒng) 宿城區(qū)電話機(jī)器人找哪家 怎么找到?jīng)]有地圖標(biāo)注的店 福州企業(yè)電銷機(jī)器人排名 上海申請(qǐng)高400開頭的電話 麗江真人語(yǔ)音電話外呼系統(tǒng) 打400電話怎么辦理收費(fèi) 河南防封號(hào)電銷機(jī)器人是什么 400電話辦理介紹信

一、學(xué)習(xí)目的

1.1、掌握 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),交給我們來實(shí)現(xiàn)。我們使用了SpringMVC 之類的框架,可是卻從來不需要考慮 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è)組件的生命周期管理又有一些共性的東西,則被提取出來成為接口和抽象類,讓具體子類實(shí)現(xiàn)變化點(diǎn),也就是模板方法設(shè)計(jì)模式。

當(dāng)今流行的微服務(wù)也是這個(gè)思路,按照功能將單體應(yīng)用拆成「微服務(wù)」,拆分過程要將共性提取出來,而這些共性就會(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ì)過時(shí),其中的設(shè)計(jì)思想與哲學(xué)才是根本之道。從中借鑒設(shè)計(jì)經(jīng)驗(yàn),合理運(yùn)用設(shè)計(jì)模式封裝變與不變,更能從它們的源碼中汲取經(jīng)驗(yàn),提升自己的系統(tǒng)設(shè)計(jì)能力。

1.2、宏觀理解一個(gè)請(qǐng)求如何與 Spring 聯(lián)系起來

在工作過程中,我們對(duì) Java 語(yǔ)法已經(jīng)很熟悉了,甚至「背」過一些設(shè)計(jì)模式,用過很多 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)系起來的。

為了突破這個(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è) ServletDispatchSevlet),而 Tomcat 和 Jetty 這樣的 Web 容器,負(fù)責(zé)加載和運(yùn)行 Servlet。如圖所示:

1.3、提升自己的系統(tǒng)設(shè)計(jì)能力

學(xué)習(xí) Tomcat ,我還發(fā)現(xiàn)用到不少 Java 高級(jí)技術(shù),比如 Java 多線程并發(fā)編程、Socket 網(wǎng)絡(luò)編程以及反射等。之前也只是了解這些技術(shù),為了面試也背過一些題。但是總感覺「知道」與會(huì)用之間存在一道溝壑,通過對(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ì)

今天咱們就來一步一步分析 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è)門。

  • 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ì)待過個(gè)也想對(duì)待單個(gè)組一樣對(duì)待。整體上每個(gè)組件設(shè)計(jì)就像是「俄羅斯套娃」一樣。

2.1、連接器

在開始講連接器前,我先鋪墊一下 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ū)別,無論是 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/ResponseServletRequest/ServletResponse 的轉(zhuǎn)化。

因此 Tomcat 的設(shè)計(jì)者設(shè)計(jì)了 3 個(gè)組件來實(shí)現(xiàn)這 3 個(gè)功能,分別是 EndPoint、Processor 和 Adapter。

網(wǎng)絡(luò)通信的 I/O 模型是變化的, 應(yīng)用層協(xié)議也是變化的,但是整體的處理邏輯是不變的,EndPoint 負(fù)責(zé)提供字節(jié)流給 Processor,Processor負(fù)責(zé)提供 Tomcat Request 對(duì)象給 AdapterAdapter負(fù)責(zé)提供 ServletRequest對(duì)象給容器。

2.2、封裝變與不變

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

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

總結(jié)下來,連接器的三個(gè)核心組件 Endpoint、ProcessorAdapter來分別做三件事情,其中 EndpointProcessor放在一起抽象成了 ProtocolHandler組件,它們的關(guān)系如下圖所示。

ProtocolHandler 組件:

主要處理 網(wǎng)絡(luò)連接 和 應(yīng)用層協(xié)議 ,包含了兩個(gè)重要部件 EndPoint 和 Processor,兩個(gè)組件組合形成 ProtocoHandler,下面我來詳細(xì)介紹它們的工作原理。

EndPoint:

EndPoint是通信端點(diǎn),即通信監(jiān)聽的接口,是具體的 Socket 接收和發(fā)送處理器,是對(duì)傳輸層的抽象,因此 EndPoint是用來實(shí)現(xiàn) TCP/IP 協(xié)議數(shù)據(jù)讀寫的,本質(zhì)調(diào)用操作系統(tǒng)的 socket 接口。

EndPoint是一個(gè)接口,對(duì)應(yīng)的抽象實(shí)現(xiàn)類是 AbstractEndpoint,而 AbstractEndpoint的具體子類,比如在 NioEndpointNio2Endpoint中,有兩個(gè)重要的子組件:AcceptorSocketProcessor。

其中 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被提交到線程池來執(zhí)行。

我們知道,對(duì)于 Java 的多路復(fù)用器的使用,無非是兩步:

  • 創(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、PollerSocketProcessorExecutor 共 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方法來接收新連接,一旦有新的連接請(qǐng)求到來,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,最后通過 socket 獲取合適應(yīng)用層協(xié)議處理器,也就是調(diào)用 Http11Processor 組件來處理請(qǐng)求。Http11Processor 讀取 Channel 的數(shù)據(jù)來生成 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ù)類,SocketProcessorrun方法會(huì)調(diào)用 Http11Processor 來讀取和解析請(qǐng)求數(shù)據(jù)。我們知道,Http11Processor是應(yīng)用層協(xié)議的封裝,它會(huì)調(diào)用容器獲得響應(yīng),再把響應(yīng)通過 Channel寫出。

工作流程如下所示:

Processor:

Processor 用來實(shí)現(xiàn) HTTP 協(xié)議,Processor 接收來自 EndPoint 的 Socket,讀取字節(jié)流解析成 Tomcat Request 和 Response 對(duì)象,并通過 Adapter 將其提交到容器處理,Processor 是對(duì)應(yīng)用層協(xié)議的抽象。

從圖中我們看到,EndPoint 接收到 Socket 連接后,生成一個(gè) SocketProcessor 任務(wù)提交到線程池去處理,SocketProcessor 的 Run 方法會(huì)調(diào)用 HttpProcessor 組件去解析應(yīng)用層協(xié)議,Processor 通過解析生成 Request 對(duì)象后,會(huì)調(diào)用 Adapter 的 Service 方法,方法內(nèi)部通過 以下代碼將請(qǐng)求傳遞到容器中。

// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Adapter 組件:

由于協(xié)議的不同,Tomcat 定義了自己的 Request 類來存放請(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方法。

2.3、容器

連接器負(fù)責(zé)外部交流,容器負(fù)責(zé)內(nèi)部處理。具體來說就是,連接器處理 Socket 通信和應(yīng)用層協(xié)議的解析,得到 Servlet請(qǐng)求;而容器則負(fù)責(zé)處理 Servlet請(qǐng)求。

容器:顧名思義就是拿來裝東西的, 所以 Tomcat 容器就是拿來裝載 Servlet。

Tomcat 設(shè)計(jì)了 4 種容器,分別是 Engine、Host、ContextWrapper。Server 代表 Tomcat 實(shí)例。

要注意的是這 4 種容器不是平行關(guān)系,屬于父子關(guān)系,如下圖所示:

你可能會(huì)問,為啥要設(shè)計(jì)這么多層次的容器,這不是增加復(fù)雜度么?其實(shí)這背后的考慮是,Tomcat 通過一種分層的架構(gòu),使得 Servlet 容器具有很好的靈活性。因?yàn)檫@里正好符合一個(gè) Host 多個(gè) Context, 一個(gè) Context 也包含多個(gè) Servlet,而每個(gè)組件都需要統(tǒng)一生命周期管理,所以組合模式設(shè)計(jì)這些容器

Wrapper 表示一個(gè) Servlet ,Context 表示一個(gè) Web 應(yīng)用程序,而一個(gè) Web 程序可能有多個(gè) Servlet ;Host 表示一個(gè)虛擬主機(jī),或者說一個(gè)站點(diǎn),一個(gè) Tomcat 可以配置多個(gè)站點(diǎn)(Host);一個(gè)站點(diǎn)( Host) 可以部署多個(gè) Web 應(yīng)用;Engine 代表 引擎,用于管理多個(gè)站點(diǎn)(Host),一個(gè) Service 只能有 一個(gè) Engine

可通過 Tomcat 配置文件加深對(duì)其層次關(guān)系理解。

<Server port="8005" shutdown="SHUTDOWN"> // 頂層組件,可包含多個(gè) Service,代表一個(gè) Tomcat 實(shí)例

  <Service name="Catalina">  // 頂層組件,包含一個(gè) Engine ,多個(gè)連接器
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />  // 連接器

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

如何管理這些容器?我們發(fā)現(xiàn)容器之間具有父子關(guān)系,形成一個(gè)樹形結(jié)構(gòu),是不是想到了設(shè)計(jì)模式中的 組合模式 。

Tomcat 就是用組合模式來管理這些容器的。具體實(shí)現(xiàn)方法是,所有容器組件都實(shí)現(xiàn)了 Container接口,因此組合模式可以使得用戶對(duì)單容器對(duì)象和組合容器對(duì)象的使用具有一致性。這里單容器對(duì)象指的是最底層的 Wrapper,組合容器對(duì)象指的是上面的 Context、Host或者 EngineContainer 接口定義如下:

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、addChildremoveChild等方法,這里正好驗(yàn)證了我們說的組合模式。我們還看到 Container接口拓展了 Lifecycle ,Tomcat 就是通過 Lifecycle 統(tǒng)一管理所有容器的組件的生命周期。通過組合模式管理所有容器,拓展 Lifecycle 實(shí)現(xiàn)對(duì)每個(gè)組件的生命周期管理 ,Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()

2.4、請(qǐng)求定位 Servlet 的過程

一個(gè)請(qǐng)求是如何定位到讓哪個(gè) WrapperServlet 處理的?答案是,Tomcat 是用 Mapper 組件來完成這個(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)求到來時(shí),Mapper 組件通過解析請(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 呢?

1.首先根據(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è)容器組件,具體來說就是一個(gè) Engine 容器,因此 Service 確定了也就意味著 Engine 也確定了。

2.根據(jù)域名選定 Host。 Service 和 Engine 確定后,Mapper 組件通過 URL 中的域名去查找相應(yīng)的 Host 容器,比如例子中的 URL 訪問的域名是user.shopping.com,因此 Mapper 會(huì)找到 Host2 這個(gè)容器。

3.根據(jù) URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據(jù) URL 的路徑來匹配相應(yīng)的 Web 應(yīng)用的路徑,比如例子中訪問的是 /order,因此找到了 Context4 這個(gè) Context 容器。

4.根據(jù) URL 路徑找到 Wrapper(Servlet)。 Context 確定后,Mapper 再根據(jù) web.xml 中配置的 Servlet 映射路徑來找到具體的 Wrapper 和 Servlet。

連接器中的 Adapter 會(huì)調(diào)用容器的 Service 方法來執(zhí)行 Servlet,最先拿到請(qǐng)求的是 Engine 容器,Engine 容器對(duì)請(qǐng)求做一些處理后,會(huì)把請(qǐng)求傳給自己子容器 Host 繼續(xù)處理,依次類推,最后這個(gè)請(qǐng)求會(huì)傳給 Wrapper 容器,Wrapper 會(huì)調(diào)用最終的 Servlet 來處理。那么這個(gè)調(diào)用過程具體是怎么實(shí)現(xiàn)的呢?答案是使用 Pipeline-Valve 管道。

Pipeline-Valve 是責(zé)任鏈模式,責(zé)任鏈模式是指在一個(gè)請(qǐng)求處理的過程中有很多處理者依次對(duì)請(qǐng)求進(jìn)行處理,每個(gè)處理者負(fù)責(zé)做自己相應(yīng)的處理,處理完之后將再調(diào)用下一個(gè)處理者繼續(xù)處理,Valve 表示一個(gè)處理點(diǎn)(也就是一個(gè)處理閥門),因此 invoke方法就是來處理請(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 來完成的,Valve完成自己的處理后,調(diào)用 getNext.invoke() 來觸發(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è)過程分是通過連接器中的 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容器的攔截器,必須通過 Valve來實(shí)現(xiàn)。

Lifecycle 生命周期

前面我們看到 Container容器 繼承了 Lifecycle 生命周期。如果想讓一個(gè)系統(tǒng)能夠?qū)ν馓峁┓?wù),我們需要?jiǎng)?chuàng)建、組裝并啟動(dòng)這些組件;在服務(wù)停止的時(shí)候,我們還需要釋放資源,銷毀這些組件,因此這是一個(gè)動(dòng)態(tài)的過程。也就是說,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è)過程,這些狀態(tài)以及狀態(tài)的轉(zhuǎn)化是不變的。而變化點(diǎn)是每個(gè)具體組件的初始化方法,也就是啟動(dòng)方法是不一樣的。

因此,Tomcat 把不變點(diǎn)抽象出來成為一個(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)用者可以無差別的調(diào)用各組件的 init() 方法和 start() 方法,這就是組合模式的使用,并且只要調(diào)用最頂層組件,也就是 Server 組件的 init()start() 方法,整個(gè) Tomcat 就被啟動(dòng)起來了。所以 Tomcat 采取組合模式管理容器,容器繼承 LifeCycle 接口,這樣就可以向針對(duì)單個(gè)對(duì)象一樣一鍵管理各個(gè)容器的生命周期,整個(gè) Tomcat 就啟動(dòng)起來。

可擴(kuò)展性:LifeCycle 事件

我們?cè)賮砜紤]另一個(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 容器,如果將來需要增加新的邏輯,直接修改start() 方法?這樣會(huì)違反開閉原則,那如何解決這個(gè)問題呢?開閉原則說的是為了擴(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 接口的定義:

重用性:LifeCycleBase 抽象基類

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

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

Tomcat 定義一個(gè)基類 LifeCycleBase 來實(shí)現(xiàn) LifeCycle 接口,把一些公共的邏輯放到基類中去,比如生命狀態(tài)的轉(zhuǎn)變與維護(hù)、生命事件的觸發(fā)以及監(jiān)聽器的添加和刪除等,而子類就負(fù)責(zé)實(shí)現(xiàn)自己的初始化、啟動(dòng)和停止等方法。

public abstract class LifecycleBase implements Lifecycle{
    // 持有所有的觀察者
    private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
    /**
     * 發(fā)布事件
     *
     * @param type  Event type
     * @param data  Data associated with event.
     */
    protected void fireLifecycleEvent(String type, Object data) {
        LifecycleEvent event = new LifecycleEvent(this, type, data);
        for (LifecycleListener listener : lifecycleListeners) {
            listener.lifecycleEvent(event);
        }
    }
    // 模板方法定義整個(gè)啟動(dòng)流程,啟動(dòng)所有容器
    @Override
    public final synchronized void init() throws LifecycleException {
        //1. 狀態(tài)檢查
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }

        try {
            //2. 觸發(fā) INITIALIZING 事件的監(jiān)聽器
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            // 3. 調(diào)用具體子類的初始化方法
            initInternal();
            // 4. 觸發(fā) INITIALIZED 事件的監(jiān)聽器
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            throw new LifecycleException(
                    sm.getString("lifecycleBase.initFail",toString()), t);
        }
    }
}

Tomcat 為了實(shí)現(xiàn)一鍵式啟停以及優(yōu)雅的生命周期管理,并考慮到了可擴(kuò)展性和可重用性,將面向?qū)ο笏枷牒驮O(shè)計(jì)模式發(fā)揮到了極致,Containaer接口維護(hù)了容器的父子關(guān)系,Lifecycle 組合模式實(shí)現(xiàn)組件的生命周期維護(hù),生命周期每個(gè)組件有變與不變的點(diǎn),運(yùn)用模板方法模式。 分別運(yùn)用了組合模式、觀察者模式、骨架抽象類和模板方法。

如果你需要維護(hù)一堆具有父子關(guān)系的實(shí)體,可以考慮使用組合模式。

觀察者模式聽起來 “高大上”,其實(shí)就是當(dāng)一個(gè)事件發(fā)生后,需要執(zhí)行一連串更新操作。實(shí)現(xiàn)了低耦合、非侵入式的通知與更新機(jī)制。

Container 繼承了 LifeCycle,StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應(yīng)容器組件的具體實(shí)現(xiàn)類,因?yàn)樗鼈兌际侨萜?,所以繼承了 ContainerBase 抽象基類,而 ContainerBase 實(shí)現(xiàn)了 Container 接口,也繼承了 LifeCycleBase 類,它們的生命周期管理接口和功能接口是分開的,這也符合設(shè)計(jì)中接口分離的原則。

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

3.1、雙親委派

我們知道 JVM的類加載器加載 Class 的時(shí)候基于雙親委派機(jī)制,也就是會(huì)將加載交給自己的父加載器加載,如果 父加載器為空則查找Bootstrap 是否加載過,當(dāng)無法加載的時(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)被加載過
        Class<?> c = findLoadedClass(name);
        // 如果沒有加載過
        if (c == null) {
            // 委托給父加載器去加載,遞歸調(diào)用
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                // 如果父加載器為空,查找 Bootstrap 是否加載過
                c = findBootstrapClassOrNull(name);
            }
            // 若果依然加載不到,則調(diào)用自己的 findClass 去加載
            if (c == null) {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
protected Class<?> findClass(String name){
    //1. 根據(jù)傳入的類名 name,到在特定目錄下去尋找類文件,把.class 文件讀入內(nèi)存
    ...

        //2. 調(diào)用 defineClass 將字節(jié)數(shù)組轉(zhuǎn)成 Class 對(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),用來加載 JVM啟動(dòng)時(shí)所需要的核心類,比如rt.jar、resources.jar等。
  • ExtClassLoader是擴(kuò)展類加載器,用來加載\jre\lib\ext目錄下 JAR 包。
  • AppClassLoader是系統(tǒng)類加載器,用來加載 classpath下的類,應(yīng)用程序默認(rèn)用它來加載類。
  • 自定義類加載器,用來加載自定義路徑下的類。

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

3.2、Tomcat 熱加載

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

protected class ContainerBackgroundProcessor implements Runnable {

    @Override
    public void run() {
        // 請(qǐng)注意這里傳入的參數(shù)是 " 宿主類 " 的實(shí)例
        processChildren(ContainerBase.this);
    }

    protected void processChildren(Container container) {
        try {
            //1. 調(diào)用當(dāng)前容器的 backgroundProcess 方法。
            container.backgroundProcess();

            //2. 遍歷所有的子容器,遞歸調(diào)用 processChildren,
            // 這樣當(dāng)前容器的子孫都會(huì)被處理
            Container[] children = container.findChildren();
            for (int i = 0; i < children.length; i++) {
            // 這里請(qǐng)你注意,容器基類有個(gè)變量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后臺(tái)線程,無需父容器來調(diào)用它的 processChildren 方法。
                if (children[i].getBackgroundProcessorDelay() <= 0) {
                    processChildren(children[i]);
                }
            }
        } catch (Throwable t) { ... }

Tomcat 的熱加載就是在 Context 容器實(shí)現(xiàn),主要是調(diào)用了 Context 容器的 reload 方法。拋開細(xì)節(jié)從宏觀上看主要完成以下任務(wù):

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

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

3.3、Tomcat 的類加載器

Tomcat 的自定義類加載器 WebAppClassLoader打破了雙親委托機(jī)制,它首先自己嘗試去加載某個(gè)類,如果找不到再代理給父類加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。具體實(shí)現(xiàn)就是重寫 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)用目錄下查找類
            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;
}

1.先在 Web 應(yīng)用本地目錄下查找要加載的類。

2.如果沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類加載器 AppClassLoader。

3.如何父加載器也沒找到這個(gè)類,拋出 ClassNotFound異常。

loadClass 方法

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

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

    synchronized (getClassLoadingLock(name)) {

        Class<?> clazz = null;

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

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

        // 3. 嘗試用 ExtClassLoader 類加載器類加載,為什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 4. 嘗試在本地目錄搜索 class 并加載
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 5. 嘗試用系統(tǒng)類加載器 (也就是 AppClassLoader) 來加載
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }

    //6. 上述過程都加載失敗,拋出異常
    throw new ClassNotFoundException(name);
}

主要有六個(gè)步驟:

1.先在本地 Cache 查找該類是否已經(jīng)加載過,也就是說 Tomcat 的類加載器是否已經(jīng)加載過這個(gè)類。

2.如果 Tomcat 類加載器沒有加載過這個(gè)類,再看看系統(tǒng)類加載器是否加載過。

3.如果都沒有,就讓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 核心類的問題。

4.如果 ExtClassLoader加載器加載失敗,也就是說 JRE核心類中沒有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載。

5.如果本地目錄下沒有這個(gè)類,說明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載。這里請(qǐng)你注意,Web 應(yīng)用是通過Class.forName調(diào)用交給系統(tǒng)類加載器的,因?yàn)?code>Class.forName的默認(rèn)加載器就是系統(tǒng)類加載器。

6.如果上述加載過程全部失敗,拋出 ClassNotFound異常。

3.4、Tomcat 類加載器層次

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

1.假如我們?cè)?Tomcat 中運(yùn)行了兩個(gè) Web 應(yīng)用程序,兩個(gè) Web 應(yīng)用中有同名的 Servlet,但是功能不同,Tomcat 需要同時(shí)加載和管理這兩個(gè)同名的 Servlet類,保證它們不會(huì)沖突,因此 Web 應(yīng)用之間的類需要隔離。

2.假如兩個(gè) Web 應(yīng)用都依賴同一個(gè)第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加載到內(nèi)存后,Tomcat要保證這兩個(gè) Web 應(yīng)用能夠共享,也就是說 Spring的 JAR 包只被加載一次,否則隨著依賴的第三方 JAR 包增多,JVM的內(nèi)存會(huì)膨脹。

3.跟 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)用之間通過各自的類加載器互相隔離。

2.SharedClassLoader

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

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

3. CatalinaClassloader

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

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

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

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

四、整體架構(gòu)設(shè)計(jì)解析收獲總結(jié)

通過前面對(duì) Tomcat 整體架構(gòu)的學(xué)習(xí),知道了 Tomcat 有哪些核心組件,組件之間的關(guān)系。以及 Tomcat 是怎么處理一個(gè) HTTP 請(qǐng)求的。下面我們通過一張簡(jiǎn)化的類圖來回顧一下,從圖上你可以看到各種組件的層次關(guān)系,圖中的虛線表示一個(gè)請(qǐng)求在 Tomcat 中流轉(zhuǎn)的過程。

4.1、連接器

Tomcat 的整體架構(gòu)包含了兩個(gè)核心組件連接器和容器。連接器負(fù)責(zé)對(duì)外交流,容器負(fù)責(zé)內(nèi)部處理。連接器用 ProtocolHandler接口來封裝通信協(xié)議和 I/O模型的差異,ProtocolHandler內(nèi)部又分為 EndPointProcessor模塊,EndPoint負(fù)責(zé)底層 Socket通信,Proccesor負(fù)責(zé)應(yīng)用層協(xié)議解析。連接器通過適配器 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)。

4.2、容器

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

4.3、類加載器

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

五、實(shí)際場(chǎng)景運(yùn)用

簡(jiǎn)單的分析了 Tomcat 整體架構(gòu)設(shè)計(jì),從 【連接器】 到 【容器】,并且分別細(xì)說了一些組件的設(shè)計(jì)思想以及設(shè)計(jì)模式。接下來就是如何學(xué)以致用,借鑒優(yōu)雅的設(shè)計(jì)運(yùn)用到實(shí)際工作開發(fā)中。學(xué)習(xí),從模仿開始。

5.1、責(zé)任鏈模式

在工作中,有這么一個(gè)需求,用戶可以輸入一些信息并可以選擇查驗(yàn)該企業(yè)的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個(gè)或者多個(gè)模塊,而且模塊之間還有一些公共的東西是要各個(gè)模塊復(fù)用。

這里就像一個(gè)請(qǐng)求,會(huì)被多個(gè)模塊去處理。所以每個(gè)查詢模塊我們可以抽象為 處理閥門,使用一個(gè) List 將這些 閥門保存起來,這樣新增模塊我們只需要新增一個(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)的模塊,我們通過 List 保存。用于觸發(fā)所需要的查驗(yàn)?zāi)K

@Slf4j
@Service
public class NetCheckService {
    // 注入所有的閥門
    @Autowired
    private Map<String, Valve> valveMap;

    /**
     * 發(fā)送查驗(yàn)請(qǐng)求
     *
     * @param netCheckDTO
     */
    @Async("asyncExecutor")
    public void sendCheckRequest(NetCheckDTO netCheckDTO) {
        // 用于保存客戶選擇處理的模塊閥門
        List<Valve> valves = new ArrayList<>();

        CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig();
        // 將用戶選擇查驗(yàn)的模塊添加到 閥門鏈條中
        if (checkModuleConfig.getBaiduNegative()) {
            valves.add(valveMap.get("baiduNegativeValve"));
        }
        // 省略部分代碼.......
        if (CollectionUtils.isEmpty(valves)) {
            log.info("網(wǎng)查查驗(yàn)?zāi)K為空,沒有需要查驗(yàn)的任務(wù)");
            return;
        }
        // 觸發(fā)處理
        valves.forEach(valve -> valve.invoke(netCheckDTO));
    }
}

5.2、模板方法模式

需求是這樣的,可根據(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)不通過還需要構(gòu)建郵件數(shù)據(jù)并發(fā)送)、從不同來源拉取財(cái)報(bào)數(shù)據(jù)并且適配通用數(shù)據(jù)、然后觸發(fā)計(jì)算,任務(wù)異常與成功都需要修改狀態(tài)。
  • 變化的是上市與非上市校驗(yàn)規(guī)則不一樣,獲取財(cái)報(bào)數(shù)據(jù)方式不一樣,兩種方式的財(cái)報(bào)數(shù)據(jù)需要適配

整個(gè)算法流程是固定的模板,但是需要將算法內(nèi)部變化的部分具體實(shí)現(xiàn)延遲到不同子類實(shí)現(xiàn),這正是模板方法模式的最佳場(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ù)用了代碼。

5.3、策略模式

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

1.一種就是包含所有標(biāo)準(zhǔn)字段。

2.收入、支出下標(biāo)是同一列,通過正負(fù)來區(qū)分收入與支出。

3.收入與支出是同一列,有一個(gè)交易類型的字段來區(qū)分。

4.特殊銀行的特殊處理。

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

這個(gè)時(shí)候我們可以用到策略模式,將不同模板的流水使用不同的處理器處理,根據(jù)模板找到對(duì)應(yīng)的策略算法去處理。即使未來再加一種類型,我們只要新加一種處理器即可,高內(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<String> row);

    /**
     * 是否支持處理該模板,不同類型的流水策略根據(jù)模板數(shù)據(jù)判斷是否支持解析
     * @return
     */
    boolean isSupport(BankFlowTemplateDO bankFlowTemplateDO);
}

// 處理器的上下文
@Service
@Slf4j
public class BankFlowDataContext {
    // 將所有處理器注入到 map 中
    @Autowired
    private List<DataProcessor> processors;

    // 找對(duì)對(duì)應(yīng)的處理器處理流水
    public void process() {
         DataProcessor processor = getProcessor(bankFlowTemplateDO);
      	 for(DataProcessor processor : processors) {
           if (processor.isSupport(bankFlowTemplateDO)) {
             // row 就是一行流水?dāng)?shù)據(jù)
        		 processor.doProcess(bankFlowTemplateDO, row);
             break;
           }
         }

    }


}

定義默認(rèn)處理器,處理正常模板,新增模板只要新增處理器實(shí)現(xiàn) DataProcessor即可。

/**
 * 默認(rèn)處理器:正對(duì)規(guī)范流水模板
 *
 */
@Component("defaultDataProcessor")
@Slf4j
public class DefaultDataProcessor implements DataProcessor {

    @Override
    public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) {
        // 省略處理邏輯細(xì)節(jié)
        return bankTransactionFlowDO;
    }

    @Override
    public String strategy(BankFlowTemplateDO bankFlowTemplateDO) {
      // 省略判斷是否支持解析該流水
      boolean isDefault = true;

      return isDefault;
    }
}

通過策略模式,我們將不同處理邏輯分配到不同的處理類中,這樣完全解耦,便于拓展。

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

以上就是解析Tomcat架構(gòu)原理到架構(gòu)設(shè)計(jì)的詳細(xì)內(nèi)容,更多關(guān)于Tomcat 架構(gòu)原理 架構(gòu)設(shè)計(jì)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

標(biāo)簽:隴南 朝陽(yáng) 運(yùn)城 雞西 連云港 遵義 荊門 面試通知

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《解析Tomcat架構(gòu)原理到架構(gòu)設(shè)計(jì)》,本文關(guān)鍵詞  解析,Tomcat,架構(gòu),原理,到,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《解析Tomcat架構(gòu)原理到架構(gòu)設(shè)計(jì)》相關(guān)的同類信息!
  • 本頁(yè)收集關(guān)于解析Tomcat架構(gòu)原理到架構(gòu)設(shè)計(jì)的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章