Tomcat基本原理

1.Tomcat核心: Http服務(wù)器+Servlet容器

組件分工:

  • 連接器Connector:處理 Socket 連接,負(fù)責(zé)網(wǎng)絡(luò)字節(jié)流與 Request 和 Response 對(duì)象的轉(zhuǎn)化。
  • 容器Container:加載和管理 Servlet,以及具體處理 Request 請(qǐng)求。

Tomcat的架構(gòu)分為以下幾個(gè)部分:

  • Server:指的就是整個(gè) Tomcat 服務(wù)器,包含多組服務(wù)(Service),負(fù)責(zé)管理和啟動(dòng)各個(gè)Service,同時(shí)監(jiān)聽 8005 端口發(fā)過(guò)來(lái)的 shutdown 命令,用于關(guān)閉整個(gè)容器 。
  • Service:每個(gè) Service 組件都包含了若干用于接收客戶端消息的 Connector 組件和處理請(qǐng)求的 Engine 組件。 Service 組件還包含了若干 Executor 組件,每個(gè) Executor 都是一個(gè)線程池,它可以為 Service 內(nèi)所有組件提供線程池執(zhí)行任務(wù)。 Tomcat 內(nèi)可能有多個(gè) Service,這樣的設(shè)計(jì)也是出于靈活性的考慮。通過(guò)在 Tomcat 中配置多個(gè) Service,可以實(shí)現(xiàn)通過(guò)不同的端口號(hào)來(lái)訪問(wèn)同一臺(tái)機(jī)器上部署的不同應(yīng)用。
  • Connector:Tomcat 與外部世界的連接器,監(jiān)聽固定端口接收外部請(qǐng)求,傳遞給 Container,并將Container 處理的結(jié)果返回給外部。連接器對(duì) Servlet 容器屏蔽了不同的應(yīng)用層協(xié)議及 I/O 模型,無(wú)論是 HTTP 還是 AJP,在容器中獲取到的都是一個(gè)標(biāo)準(zhǔn)的 ServletRequest 對(duì)象。
  • Container:Tomcat的容器,負(fù)責(zé)管理Servlet、JSP和靜態(tài)資源的生命周期。Tomcat 通過(guò)一種分層的架構(gòu),使得 Servlet 容器具有很好的靈活性。Tomcat 設(shè)計(jì)了 4 種容器,分別是 Engine、Host、Context 和 Wrapper。這 4 種容器不是平行關(guān)系,而是父子關(guān)系。
  • Engine:Tomcat的引擎,管理容器的生命周期和分配請(qǐng)求。Servlet 的頂層容器,用來(lái)管理多個(gè)虛擬站點(diǎn),一個(gè) Service 最多只能有一個(gè) Engine;
  • Host:Tomcat的主機(jī),可以管理多個(gè)Web應(yīng)用程序。負(fù)責(zé) web 應(yīng)用的部署和 Context 的創(chuàng)建??梢越o Tomcat 配置多個(gè)虛擬主機(jī)地址,而一個(gè)虛擬主機(jī)下可以部署多個(gè) Web 應(yīng)用程序;
  • Context:Tomcat的上下文,用于管理單個(gè)Web應(yīng)用程序的配置信息。Web 應(yīng)用上下文,包含多個(gè) Wrapper,負(fù)責(zé) web 配置的解析、管理所有的 Web 資源。一個(gè)Context對(duì)應(yīng)一個(gè) Web 應(yīng)用程序。
  • Wrapper:表示一個(gè) Servlet,最底層的容器,是對(duì) Servlet 的封裝,負(fù)責(zé) Servlet 實(shí)例的創(chuàng)建、執(zhí)行和銷毀。
  • JSP:Tomcat的JSP,用于動(dòng)態(tài)生成Web內(nèi)容。

Server.xml:

<Server>    //頂層組件,可以包括多個(gè)Service
    <Service>  //頂層組件,可包含一個(gè)Engine,多個(gè)連接器
        <Connector/>//連接器組件,代表通信接口           
        <Engine>//容器組件,一個(gè)Engine組件處理Service中的所有請(qǐng)求,包含多個(gè)Host
            <Host>  //容器組件,處理特定的Host下客戶請(qǐng)求,可包含多個(gè)Context
                <Context/>  //容器組件,為特定的Web應(yīng)用處理所有的客戶請(qǐng)求
        </Host>
        </Engine>
    </Service>    
</Server>    

Tomcat啟動(dòng)期間會(huì)通過(guò)解析 server.xml,利用反射創(chuàng)建相應(yīng)的組件,所以xml中的標(biāo)簽和源碼一一對(duì)應(yīng)。

2.請(qǐng)求定位 Servlet 的過(guò)程

Tomcat 是用 Mapper 組件來(lái)完成這個(gè)任務(wù)的。Mapper 組件的功能就是將用戶請(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。一個(gè)請(qǐng)求 URL 最后只會(huì)定位到一個(gè) Wrapper 容器,也就是一個(gè) Servlet。

3.Connector

3.1 Connector高內(nèi)聚低耦合設(shè)計(jì)

優(yōu)秀的模塊化設(shè)計(jì)應(yīng)該考慮高內(nèi)聚、低耦合:

  • 高內(nèi)聚是指相關(guān)度比較高的功能要盡可能集中,不要分散。
  • 低耦合是指兩個(gè)相關(guān)的模塊要盡可能減少依賴的部分和降低依賴的程度,不要讓兩個(gè)模塊產(chǎn)生強(qiáng)依賴。

Tomcat連接器需要實(shí)現(xiàn)的功能:

  • 監(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é)流寫回給瀏覽器。

分析連接器詳細(xì)功能列表,我們會(huì)發(fā)現(xiàn)連接器需要完成 3 個(gè)高內(nèi)聚的功能:

  • 網(wǎng)絡(luò)通信。
  • 應(yīng)用層協(xié)議解析。
  • Tomcat Request/Response 與 ServletRequest/ServletResponse 的轉(zhuǎn)化。

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

  • EndPoint 負(fù)責(zé)提供字節(jié)流給 Processor;
  • Processor 負(fù)責(zé)提供 Tomcat Request 對(duì)象給 Adapter;
  • Adapter 負(fù)責(zé)提供 ServletRequest 對(duì)象給容器。

組件之間通過(guò)抽象接口交互。這樣做的好處是封裝變化。這是面向?qū)ο笤O(shè)計(jì)的精髓,將系統(tǒng)中經(jīng)常變化的部分和穩(wěn)定的部分隔離,有助于增加復(fù)用性,并降低系統(tǒng)耦合度。

由于 I/O 模型和應(yīng)用層協(xié)議可以自由組合,比如 NIO + HTTP 或者 NIO2 + AJP。Tomcat 的設(shè)計(jì)者將網(wǎng)絡(luò)通信和應(yīng)用層協(xié)議解析放在一起考慮,設(shè)計(jì)了一個(gè)叫 ProtocolHandler 的接口來(lái)封裝這兩種變化點(diǎn)。各種協(xié)議和通信模型的組合有相應(yīng)的具體實(shí)現(xiàn)類。比如:Http11NioProtocol 和 AjpNioProtocol。

除了這些變化點(diǎn),系統(tǒng)也存在一些相對(duì)穩(wěn)定的部分,因此 Tomcat 設(shè)計(jì)了一系列抽象基類來(lái)封裝這些穩(wěn)定的部分,抽象基類 AbstractProtocol 實(shí)現(xiàn)了 ProtocolHandler 接口。每一種應(yīng)用層協(xié)議有自己的抽象基類,比如 AbstractAjpProtocol 和 AbstractHttp11Protocol,具體協(xié)議的實(shí)現(xiàn)類擴(kuò)展了協(xié)議層抽象基類。

連接器用 ProtocolHandler 來(lái)處理網(wǎng)絡(luò)連接和應(yīng)用層協(xié)議,包含了 2 個(gè)重要部件:EndPoint 和 Processor。

組件:

  • Processor
    連接器用 ProtocolHandler 接口來(lái)封裝通信協(xié)議和 I/O 模型的差異,ProtocolHandler 內(nèi)部又分為 EndPoint 和 Processor 模塊,EndPoint 負(fù)責(zé)底層 Socket 通信,Proccesor 負(fù)責(zé)應(yīng)用層協(xié)議解析。連接器通過(guò)適配器 Adapter 調(diào)用容器。
  • EndPoint
    EndPoint 是通信端點(diǎn),即通信監(jiān)聽的接口,是具體的 Socket 接收和發(fā)送處理器,是對(duì)傳輸層的抽象,因此 EndPoint 是用來(lái)實(shí)現(xiàn) TCP/IP 協(xié)議的。
    EndPoint 是一個(gè)接口,對(duì)應(yīng)的抽象實(shí)現(xiàn)類是 AbstractEndpoint,而 AbstractEndpoint 的具體子類,比如在 NioEndpoint 和 Nio2Endpoint 中,有兩個(gè)重要的子組件:Acceptor 和 SocketProcessor。其中 Acceptor 用于監(jiān)聽 Socket 連接請(qǐng)求。SocketProcessor 用于處理接收到的 Socket 請(qǐng)求,它實(shí)現(xiàn) Runnable 接口,在 Run 方法里調(diào)用協(xié)議處理組件 Processor 進(jìn)行處理。為了提高處理能力,SocketProcessor 被提交到線程池來(lái)執(zhí)行,而這個(gè)線程池叫作執(zhí)行器(Executor)。
  • Processor
    Processor 用來(lái)實(shí)現(xiàn) HTTP/AJP 協(xié)議,Processor 接收來(lái)自 EndPoint 的 Socket,讀取字節(jié)流解析成 Tomcat Request 和 Response 對(duì)象,并通過(guò) Adapter 將其提交到容器處理,Processor 是對(duì)應(yīng)用層協(xié)議的抽象。
    Processor 是一個(gè)接口,定義了請(qǐng)求的處理等方法。它的抽象實(shí)現(xiàn)類 AbstractProcessor 對(duì)一些協(xié)議共有的屬性進(jìn)行封裝,沒有對(duì)方法進(jìn)行實(shí)現(xiàn)。具體的實(shí)現(xiàn)有 AJPProcessor、HTTP11Processor 等,這些具體實(shí)現(xiàn)類實(shí)現(xiàn)了特定協(xié)議的解析方法和請(qǐng)求處理方式。
    EndPoint 接收到 Socket 連接后,生成一個(gè) SocketProcessor 任務(wù)提交到線程池去處理,SocketProcessor 的 Run 方法會(huì)調(diào)用 Processor 組件去解析應(yīng)用層協(xié)議,Processor 通過(guò)解析生成 Request 對(duì)象后,會(huì)調(diào)用 Adapter 的 Service 方法。
  • Adapter
    由于協(xié)議不同,客戶端發(fā)過(guò)來(lái)的請(qǐng)求信息也不盡相同,Tomcat 定義了自己的 Request 類來(lái)“存放”這些請(qǐng)求信息。ProtocolHandler 接口負(fù)責(zé)解析請(qǐng)求并生成 Tomcat Request 類。但是這個(gè) Request 對(duì)象不是標(biāo)準(zhǔn)的 ServletRequest,也就意味著,不能用 Tomcat Request 作為參數(shù)來(lái)調(diào)用容器。Tomcat 設(shè)計(jì)者的解決方案是引入 CoyoteAdapter,這是適配器模式的經(jīng)典運(yùn)用,連接器調(diào)用 CoyoteAdapter 的 Sevice 方法,傳入的是 Tomcat Request 對(duì)象,CoyoteAdapter 負(fù)責(zé)將 Tomcat Request 轉(zhuǎn)成 ServletRequest,再調(diào)用容器的 Service 方法。

3.2 IO

一般情況下默認(rèn)都是 NIO,在絕大多數(shù)情況下都是夠用的,除非你的 Web 應(yīng)用用到了 TLS 加密傳輸,而且對(duì)性能要求極高,這個(gè)時(shí)候可以考慮 APR,因?yàn)?APR 通過(guò) OpenSSL 來(lái)處理 TLS 握手和加密 / 解密。OpenSSL 本身用 C 語(yǔ)言實(shí)現(xiàn),它還對(duì) TLS 通信做了優(yōu)化,所以性能比 Java 要高。如果你的 Tomcat 跑在 Windows 平臺(tái)上,并且 HTTP 請(qǐng)求的數(shù)據(jù)量比較大,可以考慮 NIO2,這是因?yàn)?Windows 從操作系統(tǒng)層面實(shí)現(xiàn)了真正意義上的異步 I/O,如果傳輸?shù)臄?shù)據(jù)量比較大,異步 I/O 的效果就能顯現(xiàn)出來(lái)。如果你的 Tomcat 跑在 Linux 平臺(tái)上,建議使用 NIO。因?yàn)樵?Linux 平臺(tái)上,Java NIO 和 Java NIO2 底層都是通過(guò) epoll 來(lái)實(shí)現(xiàn)的,但是 Java NIO 更加簡(jiǎn)單高效。

3.2.1 Tomcat NIO實(shí)現(xiàn)

在 Tomcat 中,EndPoint 組件的主要工作就是處理 I/O,而 NioEndpoint 利用 Java NIO API 實(shí)現(xiàn)了多路復(fù)用 I/O 模型。Tomcat的NioEndpoint 是基于主從Reactor多線程模型設(shè)計(jì)的。

各組件作用:

  • LimitLatch 是連接控制器,它負(fù)責(zé)控制最大連接數(shù),NIO 模式下默認(rèn)是 10000(tomcat9中8192),當(dāng)連接數(shù)到達(dá)最大時(shí)阻塞線程,直到后續(xù)組件處理完一個(gè)連接后將連接數(shù)減 1。注意到達(dá)最大連接數(shù)后操作系統(tǒng)底層還是會(huì)接收客戶端連接,但用戶層已經(jīng)不再接收。(實(shí)際8192 + 底層TCP還有一個(gè)隊(duì)列設(shè)置的128,默認(rèn)配置100,所以最大為8192 + 100)
  • Acceptor 跑在一個(gè)單獨(dú)的線程里,它在一個(gè)死循環(huán)里調(diào)用 accept 方法來(lái)接收新連接,一旦有新的連接請(qǐng)求到來(lái),accept 方法返回一個(gè) Channel 對(duì)象,接著把 Channel 對(duì)象交給 Poller 去處理。
    ServerSocketChannel 通過(guò) accept() 接受新的連接,accept() 方法返回獲得 SocketChannel 對(duì)象,然后將 SocketChannel 對(duì)象封裝在一個(gè) PollerEvent 對(duì)象中,并將 PollerEvent 對(duì)象壓入 Poller 的 SynchronizedQueue 里,這是個(gè)典型的生產(chǎn)者 - 消費(fèi)者模式,Acceptor 與 Poller 線程之間通過(guò) SynchronizedQueue 通信。
  • 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 去處理。
  • Executor 就是線程池,負(fù)責(zé)運(yùn)行 SocketProcessor 任務(wù)類,SocketProcessor 的 run 方法會(huì)調(diào)用 Http11Processor 來(lái)讀取和解析請(qǐng)求數(shù)據(jù)。Http11Processor 是應(yīng)用層協(xié)議的封裝,它會(huì)調(diào)用容器獲得響應(yīng),再把響應(yīng)通過(guò) Channel 寫出。

3.2.2 Tomcat 異步IO實(shí)現(xiàn)

NIO 和 NIO2 最大的區(qū)別是,一個(gè)是同步一個(gè)是異步。異步最大的特點(diǎn)是,應(yīng)用程序不需要自己去觸發(fā)數(shù)據(jù)從內(nèi)核空間到用戶空間的拷貝。

Nio2Endpoint 中沒有 Poller 組件,也就是沒有 Selector。在異步 I/O 模式下,Selector 的工作交給內(nèi)核來(lái)做了。

4.容器

4.1 父子容器組合模式設(shè)計(jì)

Tomcat 設(shè)計(jì)了 4 種容器,分別是 Engine、Host、Context 和 Wrapper,Tomcat 是怎么管理這些容器的?

Tomcat 采用組合模式來(lái)管理這些容器。具體實(shí)現(xiàn)方法是,所有容器組件都實(shí)現(xiàn)了 Container 接口,因此組合模式可以使得用戶對(duì)單容器對(duì)象和組合容器對(duì)象的使用具有一致性。

4.2 Pipeline-Valve 責(zé)任鏈模式設(shè)計(jì)

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

Pipeline-Valve 是責(zé)任鏈模式,責(zé)任鏈模式是指在一個(gè)請(qǐng)求處理的過(guò)程中有很多處理者依次對(duì)請(qǐng)求進(jìn)行處理,每個(gè)處理者負(fù)責(zé)做自己相應(yīng)的處理,處理完之后將再調(diào)用下一個(gè)處理者繼續(xù)處理。

為什么要使用管道機(jī)制?
在一個(gè)比較復(fù)雜的大型系統(tǒng)中,如果一個(gè)對(duì)象或數(shù)據(jù)流需要進(jìn)行繁雜的邏輯處理,我們可以選擇在一個(gè)大的組件中直接處理這些繁雜的業(yè)務(wù)邏輯, 這個(gè)方式雖然達(dá)到目的,但擴(kuò)展性和可重用性較差, 因?yàn)榭赡軤恳话l(fā)而動(dòng)全身。更好的解決方案是采用管道機(jī)制,用一條管道把多個(gè)對(duì)象(閥門部件)連接起來(lái),整體看起來(lái)就像若干個(gè)閥門嵌套在管道中一樣,而處理邏輯放在閥門上。

Pipeline 中維護(hù)了 Valve 鏈表,Valve 可以插入到 Pipeline 中,對(duì)請(qǐng)求做某些處理。整個(gè)調(diào)用鏈的觸發(fā)是 Valve 來(lái)完成的,Valve 完成自己的處理后,調(diào)用 getNext.invoke() 來(lái)觸發(fā)下一個(gè) Valve 調(diào)用。每一個(gè)容器都有一個(gè) Pipeline 對(duì)象,只要觸發(fā)這個(gè) Pipeline 的第一個(gè) Valve,這個(gè)容器里 Pipeline 中的 Valve 就都會(huì)被調(diào)用到。Basic Valve 處于 Valve 鏈表的末端,它是 Pipeline 中必不可少的一個(gè) Valve,負(fù)責(zé)調(diào)用下層容器的 Pipeline 里的第一個(gè) Valve。

調(diào)用:

  • 整個(gè)調(diào)用過(guò)程由連接器中的 Adapter 觸發(fā)的,它會(huì)調(diào)用 Engine 的第一個(gè) Valve。
  • Wrapper 容器的最后一個(gè) Valve 會(huì)創(chuàng)建一個(gè) Filter 鏈,并調(diào)用 doFilter() 方法,最終會(huì)調(diào)到 Servlet 的 service 方法。

Valve 和 Filter 的區(qū)別:

  • Valve 是 Tomcat 的私有機(jī)制,與 Tomcat 的基礎(chǔ)架構(gòu) /API 是緊耦合的。Servlet API 是公有的標(biāo)準(zhǔn),所有的 Web 容器包括 Jetty 都支持 Filter 機(jī)制。
  • Valve 工作在 Web 容器級(jí)別,攔截所有應(yīng)用的請(qǐng)求;而 Servlet Filter 工作在應(yīng)用級(jí)別,只能攔截某個(gè) Web 應(yīng)用的所有請(qǐng)求。

5.Tomcat生命周期設(shè)計(jì)

如果想讓Tomcat能夠?qū)ν馓峁┓?wù),我們需要?jiǎng)?chuàng)建、組裝并啟動(dòng)Tomcat組件;在服務(wù)停止的時(shí)候,我們還需要釋放資源,銷毀Tomcat組件,這是一個(gè)動(dòng)態(tài)的過(guò)程。Tomcat 需要?jiǎng)討B(tài)地管理這些組件的生命周期。

在我們實(shí)際的工作中,如果你需要設(shè)計(jì)一個(gè)比較大的系統(tǒng)或者框架時(shí),你同樣也需要考慮這幾個(gè)問(wèn)題:如何統(tǒng)一管理組件的創(chuàng)建、初始化、啟動(dòng)、停止和銷毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動(dòng)和停止不遺漏、不重復(fù)?

一鍵式啟停:LifeCycle 接口

  • 系統(tǒng)設(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)方法是不一樣的。因此,我們把不變點(diǎn)抽象出來(lái)成為一個(gè)接口,這個(gè)接口跟生命周期有關(guān),叫作 LifeCycle。LifeCycle 接口里應(yīng)該定義這么幾個(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)了。

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

  • 因?yàn)楦鱾€(gè)組件 init() 和 start() 方法的具體實(shí)現(xiàn)是復(fù)雜多變的,比如在 Host 容器的啟動(dòng)方法里需要掃描 webapps 目錄下的 Web 應(yīng)用,創(chuàng)建相應(yīng)的 Context 容器,如果將來(lái)需要增加新的邏輯,直接修改 start() 方法?這樣會(huì)違反開閉原則,那如何解決這個(gè)問(wèn)題呢?開閉原則說(shuō)的是為了擴(kuò)展系統(tǒng)的功能,你不能直接修改系統(tǒng)中已有的類,但是你可以定義新的類。
  • 組件的 init() 和 start() 調(diào)用是由它的父組件的狀態(tài)變化觸發(fā)的,上層組件的初始化會(huì)觸發(fā)子組件的初始化,上層組件的啟動(dòng)會(huì)觸發(fā)子組件的啟動(dòng),因此我們把組件的生命周期定義成一個(gè)個(gè)狀態(tài),把狀態(tài)的轉(zhuǎn)變看作是一個(gè)事件。而事件是有監(jiān)聽器的,在監(jiān)聽器里可以實(shí)現(xiàn)一些邏輯,并且監(jiān)聽器也可以方便的添加和刪除,這就是典型的觀察者模式。
  • 具體來(lái)說(shuō)就是在 LifeCycle 接口里加入兩個(gè)方法:添加監(jiān)聽器和刪除監(jiān)聽器。
  • Tomcat 自定義了一些監(jiān)聽器,這些監(jiān)聽器是父組件在創(chuàng)建子組件的過(guò)程中注冊(cè)到子組件的。比如 MemoryLeakTrackingListener監(jiān)聽器,用來(lái)檢測(cè) Context 容器中的內(nèi)存泄漏,這個(gè)監(jiān)聽器是 Host 容器在創(chuàng)建 Context 容器時(shí)注冊(cè)到 Context 中的。
  • 我們還可以在 server.xml 中定義自己的監(jiān)聽器,Tomcat 在啟動(dòng)時(shí)會(huì)解析 server.xml,創(chuàng)建監(jiān)聽器并注冊(cè)到容器組件。

重用性:LifeCycleBase 抽象基類

  • 有了接口,我們就要用類去實(shí)現(xiàn)接口。一般來(lái)說(shuō)實(shí)現(xiàn)類不止一個(gè),不同的類在實(shí)現(xiàn)接口時(shí)往往會(huì)有一些相同的邏輯,如果讓各個(gè)子類都去實(shí)現(xiàn)一遍,就會(huì)有重復(fù)代碼。那子類如何重用這部分邏輯呢?其實(shí)就是定義一個(gè)基類來(lái)實(shí)現(xiàn)共同的邏輯,然后讓各個(gè)子類去繼承它,就達(dá)到了重用的目的。而基類中往往會(huì)定義一些抽象方法,所謂的抽象方法就是說(shuō)基類不會(huì)去實(shí)現(xiàn)這些方法,而是調(diào)用這些方法來(lái)實(shí)現(xiàn)骨架邏輯。抽象方法是留給各個(gè)子類去實(shí)現(xiàn)的,并且子類必須實(shí)現(xiàn),否則無(wú)法實(shí)例化。
  • Tomcat 定義一個(gè)基類 LifeCycleBase 來(lái)實(shí)現(xiàn) LifeCycle 接口,把一些公共的邏輯放到基類中去,比如生命狀態(tài)的轉(zhuǎn)變與維護(hù)、生命周期事件的觸發(fā)以及監(jiān)聽器的添加和刪除等,而子類就負(fù)責(zé)實(shí)現(xiàn)自己的初始化、啟動(dòng)和停止等方法。為了避免跟基類中的方法同名,我們把具體子類的實(shí)現(xiàn)方法改個(gè)名字,在后面加上 Internal,叫 initInternal()、startInternal() 等。
  • LifeCycleBase 實(shí)現(xiàn)了 LifeCycle 接口中所有的方法,還定義了相應(yīng)的抽象方法交給具體子類去實(shí)現(xiàn),這是典型的模板設(shè)計(jì)模式(骨架抽象類和模板方法)。

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

Tomcat 為了實(shí)現(xiàn)一鍵式啟停以及優(yōu)雅的生命周期管理,并考慮到了可擴(kuò)展性和可重用性,將面向?qū)ο笏枷牒驮O(shè)計(jì)模式發(fā)揮到了極致,分別運(yùn)用了組合模式、觀察者模式、骨架抽象類和模板方法。如果你需要維護(hù)一堆具有父子關(guān)系的實(shí)體,可以考慮使用組合模式。觀察者模式聽起來(lái)“高大上”,其實(shí)就是當(dāng)一個(gè)事件發(fā)生后,需要執(zhí)行一連串更新操作。傳統(tǒng)的實(shí)現(xiàn)方式是在事件響應(yīng)代碼里直接加更新邏輯,當(dāng)更新邏輯加多了之后,代碼會(huì)變得臃腫,并且這種方式是緊耦合的、侵入式的。觀察者模式實(shí)現(xiàn)了低耦合、非侵入式的通知與更新機(jī)制。模板方法在抽象基類中經(jīng)常用到,用來(lái)實(shí)現(xiàn)通用邏輯。

6.Tomcat類加載機(jī)制及其熱加載和熱部署原理剖析

6.1 Tomcat類加載機(jī)制

Java中有 3 個(gè)類加載器,另外你也可以自定義類加載器

  • 引導(dǎo)(啟動(dòng))類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的核心類庫(kù),比如rt.jar、charsets.jar等
  • 擴(kuò)展類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的ext擴(kuò)展目錄中的JAR類包
  • 應(yīng)用程序(系統(tǒng))類加載器:負(fù)責(zé)加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類
  • 自定義加載器:負(fù)責(zé)加載用戶自定義路徑下的類包

為什么要設(shè)計(jì)雙親委派機(jī)制?

  • 沙箱安全機(jī)制:自己寫的java.lang.String.class類不會(huì)被加載,這樣便可以防止核心 API庫(kù)被隨意篡改
  • 避免類的重復(fù)加載:當(dāng)父親已經(jīng)加載了該類時(shí),就沒有必要子ClassLoader再加載一次,保證被加載類的唯一性

Tomcat自己的需求:

  • 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)用能夠共享,也就是說(shuō) Spring 的 JAR 包只被加載一次,否則隨著依賴的第三方 JAR 包增多,JVM 的內(nèi)存會(huì)膨脹。
  • 3)跟 JVM 一樣,我們需要隔離 Tomcat 本身的類和 Web 應(yīng)用的類。

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)為是不同的類,即使它們的類名相同。

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

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;
        //1. 先在本地 cache 查找該類是否已經(jīng)加載過(guò)
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        //2. 從系統(tǒng)類加載器的 cache 中查找是否加載過(guò)
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
 
        // 3. 嘗試用 ExtClassLoader 類加載器類加載,為什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

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

        // 5. 嘗試用系統(tǒng)類加載器 (也就是 AppClassLoader) 來(lái)加載
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }
    
    //6. 上述過(guò)程都加載失敗,拋出異常
    throw new ClassNotFoundException(name);
}

loadClass 方法稍微復(fù)雜一點(diǎn),主要有六個(gè)步驟:

  • 1)先在本地 Cache 查找該類是否已經(jīng)加載過(guò),也就是說(shuō) Tomcat 的類加載器是否已經(jīng)加載過(guò)這個(gè)類。
  • 2)如果 Tomcat 類加載器沒有加載過(guò)這個(gè)類,再看看系統(tǒng)類加載器是否加載過(guò)。
    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 核心類的問(wèn)題。
  • 4)如果 ExtClassLoader 加載器加載失敗,也就是說(shuō) JRE 核心類中沒有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載。
  • 5)如果本地目錄下沒有這個(gè)類,說(shuō)明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載。這里請(qǐng)你注意,Web 應(yīng)用是通過(guò)Class.forName調(diào)用交給系統(tǒng)類加載器的,因?yàn)镃lass.forName的默認(rèn)加載器就是系統(tǒng)類加載器。
  • 6)如果上述加載過(guò)程全部失敗,拋出 ClassNotFound 異常。
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;
}

在 findClass 方法里,主要有三個(gè)步驟:

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

6.2 Tomcat熱加載和熱部署

熱部署表示重新部署應(yīng)用,它的執(zhí)行主體是Host。 熱加載表示重新加載class,它的執(zhí)行主體是Context。

它們的區(qū)別是:

  • 熱加載的實(shí)現(xiàn)方式是 Web 容器啟動(dòng)一個(gè)后臺(tái)線程,定期檢測(cè)類文件的變化,如果有變化,就重新加載類,在這個(gè)過(guò)程中不會(huì)清空 Session ,一般用在開發(fā)環(huán)境。
  • 熱部署原理類似,也是由后臺(tái)線程定時(shí)檢測(cè) Web 應(yīng)用的變化,但它會(huì)重新加載整個(gè) Web 應(yīng)用。這種方式會(huì)清空 Session,比熱加載更加干凈、徹底,一般用在生產(chǎn)環(huán)境。

Tomcat開啟后臺(tái)線程執(zhí)行周期性任務(wù)

  • Tomcat 通過(guò)開啟后臺(tái)線程ContainerBase.ContainerBackgroundProcessor,使得各個(gè)層次的容器組件都有機(jī)會(huì)完成一些周期性任務(wù)。
  • 此后臺(tái)線程會(huì)調(diào)用當(dāng)前容器的 backgroundProcess 方法,以及遞歸調(diào)用子孫的 backgroundProcess 方法,backgroundProcess 方法會(huì)觸發(fā)容器的周期性任務(wù)。
  • ContainerBackgroundProcessMonitor 會(huì)監(jiān)控線程,如果出問(wèn)題,會(huì)重新起一個(gè)任務(wù)。

Tomcat熱加載實(shí)現(xiàn)原理

  • 有了 ContainerBase 的周期性任務(wù)處理“框架”,作為具體容器子類,只需要實(shí)現(xiàn)自己的周期性任務(wù)就行。而 Tomcat 的熱加載,就是在 Context 容器中實(shí)現(xiàn)的(StandardContext#backgroundProcess)。
  • WebappLoader 實(shí)現(xiàn)熱加載的邏輯:它主要是調(diào)用了 Context 容器的 reload 方法,先stop Context容器,再start Context容器。具體的實(shí)現(xiàn):
    1)停止和銷毀 Context 容器及其所有子容器,子容器其實(shí)就是 Wrapper,也就是說(shuō) Wrapper 里面 Servlet 實(shí)例也被銷毀了。
    2)停止和銷毀 Context 容器關(guān)聯(lián)的 Listener 和 Filter。
    3)停止和銷毀 Context 下的 Pipeline 和各種 Valve。
    4)停止和銷毀 Context 的類加載器,以及類加載器加載的類文件資源。
    5)啟動(dòng) Context 容器,在這個(gè)過(guò)程中會(huì)重新創(chuàng)建前面四步被銷毀的資源。
    在這個(gè)過(guò)程中,類加載器發(fā)揮著關(guān)鍵作用。一個(gè) Context 容器對(duì)應(yīng)一個(gè)類加載器,類加載器在銷毀的過(guò)程中會(huì)把它加載的所有類也全部銷毀。Context 容器在啟動(dòng)過(guò)程中,會(huì)創(chuàng)建一個(gè)新的類加載器來(lái)加載新的類文件。

Tomcat熱部署實(shí)現(xiàn)原理

  • 熱部署跟熱加載的本質(zhì)區(qū)別是,熱部署會(huì)重新部署 Web 應(yīng)用,原來(lái)的 Context 對(duì)象會(huì)整個(gè)被銷毀掉,因此這個(gè) Context 所關(guān)聯(lián)的一切資源都會(huì)被銷毀,包括 Session。
  • Host 容器并沒有在 backgroundProcess 方法中實(shí)現(xiàn)周期性檢測(cè)的任務(wù),而是通過(guò)監(jiān)聽器 HostConfig 來(lái)實(shí)現(xiàn)的(HostConfig#lifecycleEvent)
  • HostConfig 會(huì)檢查 webapps 目錄下的所有 Web 應(yīng)用:
    如果原來(lái) Web 應(yīng)用目錄被刪掉了,就把相應(yīng) Context 容器整個(gè)銷毀掉。
    是否有新的 Web 應(yīng)用目錄放進(jìn)來(lái)了,或者有新的 WAR 包放進(jìn)來(lái)了,就部署相應(yīng)的 Web 應(yīng)用。
  • 因此 HostConfig 做的事情都是比較“宏觀”的,它不會(huì)去檢查具體類文件或者資源文件是否有變化,而是檢查 Web 應(yīng)用目錄級(jí)別的變化。

7.Tomcat性能調(diào)優(yōu)

Tomcat 的關(guān)鍵指標(biāo)有吞吐量、響應(yīng)時(shí)間、錯(cuò)誤數(shù)、線程池、CPU 以及 JVM 內(nèi)存。

線程池調(diào)優(yōu)指的是給 Tomcat 的線程池設(shè)置合適的參數(shù),使得 Tomcat 能夠又快又好地處理請(qǐng)求。

  • namePrefix: 線程前綴
  • maxThreads: 最大線程數(shù),默認(rèn)設(shè)置 200,一般建議在 500 ~ 1000,根據(jù)硬件設(shè)施和業(yè)務(wù)來(lái)判斷
  • minSpareThreads: 核心線程數(shù),默認(rèn)設(shè)置 25
  • prestartminSpareThreads: 在 Tomcat 初始化的時(shí)候就初始化核心線程
  • maxQueueSize: 最大的等待隊(duì)列數(shù),超過(guò)則拒絕請(qǐng)求 ,默認(rèn) Integer.MAX_VALUE
  • maxIdleTime: 線程空閑時(shí)間,超過(guò)該時(shí)間,線程會(huì)被銷毀,單位毫秒

className: 線程實(shí)現(xiàn)類,默認(rèn)org.apache.catalina.core.StandardThreadExecutor


JDK線程池:


對(duì)于 io 密集型任務(wù),如數(shù)據(jù)庫(kù)查詢,rpc 請(qǐng)求調(diào)用等,JDK線程池就不是很友好了。

由于 Tomcat/Jetty 需要處理大量客戶端請(qǐng)求任務(wù),如果采用原生線程池,一旦接受請(qǐng)求數(shù)量大于線程池核心線程數(shù),這些請(qǐng)求就會(huì)被放入到隊(duì)列中,等待核心線程處理。這樣做顯然降低這些請(qǐng)求總體處理速度,所以兩者都沒采用 JDK 原生線程池。


參考:

  • 圖靈vip課程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容