架構(gòu)篇:Tomcat?高層組件構(gòu)建一個商業(yè)帝國
Tomcat 實現(xiàn)的 2 個核心功能:
- 處理
Socket
連接,負(fù)責(zé)網(wǎng)絡(luò)字節(jié)流與Request
和Response
對象的轉(zhuǎn)化。 - 加載并管理
Servlet
,以及處理具體的Request
請求。

startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()


Bootstrap
當(dāng)執(zhí)行startup.sh
腳本的時候,就會啟動一個 JVM 運行 Tomcat 的啟動類 Bootstrap
的 main
方法。先看下他的成員變量窺探核心功能:public?final?class?Bootstrap?{
????ClassLoader?commonLoader?=?null;
????ClassLoader?catalinaLoader?=?null;
????ClassLoader?sharedLoader?=?null;
}
它的主要任務(wù)就是初始化 Tomcat 定義的類加載器,同時創(chuàng)建 Catalina 對象。Bootstrap 就像一個大神,初始化了類加載器,加載萬物。關(guān)于為何自定義各種類加載器詳情請查看碼哥的 Tomcat 架構(gòu)設(shè)計解析 類加載器部分。初始化類加載器

WebAppClassLoader
假如我們在 Tomcat 中運行了兩個 Web 應(yīng)用程序,兩個 Web 應(yīng)用中有同名的Servlet
,但是功能不同,Tomcat 需要同時加載和管理這兩個同名的 Servlet
類,保證它們不會沖突,因此 Web 應(yīng)用之間的類需要隔離。Tomcat 的解決方案是自定義一個類加載器 WebAppClassLoader
, 并且給每個 Web 應(yīng)用創(chuàng)建一個類加載器實例。我們知道,Context 容器組件對應(yīng)一個 Web 應(yīng)用,因此,每個 Context
容器負(fù)責(zé)創(chuàng)建和維護(hù)一個 WebAppClassLoader
加載器實例。這背后的原理是,不同的加載器實例加載的類被認(rèn)為是不同的類,即使它們的類名相同。Tomcat 的自定義類加載器 WebAppClassLoader
打破了雙親委托機制,它首先自己嘗試去加載某個類,如果找不到則通過 ExtClassLoader 加載 JRE 核心類防止黑客攻擊,無法加載再代理給 AppClassLoader 加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。具體實現(xiàn)就是重寫 ClassLoader
的兩個方法:findClass
和 loadClass
。SharedClassLoader
假如兩個 Web 應(yīng)用都依賴同一個第三方的 JAR 包,比如Spring
,那 Spring
的 JAR 包被加載到內(nèi)存后,Tomcat
要保證這兩個 Web 應(yīng)用能夠共享,也就是說 Spring
的 JAR 包只被加載一次。SharedClassLoader 就是 Web 應(yīng)用共享的類庫的加載器,專門加載 Web 應(yīng)用共享的類。如果 ?WebAppClassLoader
自己沒有加載到某個類,就會委托父加載器 SharedClassLoader
去加載這個類,SharedClassLoader
會在指定目錄下加載共享類,之后返回給 WebAppClassLoader
,這樣共享的問題就解決了。CatalinaClassloader
如何隔離 Tomcat 本身的類和 Web 應(yīng)用的類?要共享可以通過父子關(guān)系,要隔離那就需要兄弟關(guān)系了。兄弟關(guān)系就是指兩個類加載器是平行的,它們可能擁有同一個父加載器,基于此 Tomcat 又設(shè)計一個類加載器CatalinaClassloader
,專門來加載 Tomcat 自身的類。這樣設(shè)計有個問題,那 Tomcat 和各 Web 應(yīng)用之間需要共享一些類時該怎么辦呢?老辦法,還是再增加一個 CommonClassLoader
,作為 CatalinaClassloader
和 ?SharedClassLoader
的父加載器。CommonClassLoader
能加載的類都可以被 ?CatalinaClassLoader
和 SharedClassLoader
使用。Catalina
Tomcat 是一個公司,Catalina 就好像是一個創(chuàng)始人。因為它負(fù)責(zé)組建團(tuán)隊,創(chuàng)建 Server 以及所有子組件。Catalina 的主要任務(wù)就是創(chuàng)建 Server,解析 server.xml 把里面配置的各個組件創(chuàng)建出來,并調(diào)用每個組件的init
和 start
方法,將整個 Tomcat 啟動,這樣整個公司就在正常運作了。我們可以根據(jù) Tomcat 配置文件來直觀感受下:<Server?port="8005"?shutdown="SHUTDOWN">?//?頂層組件,可包含多個?Service,代表一個?Tomcat?實例
??<Service?name="Catalina">??//?頂層組件,包含一個?Engine?,多個連接器
????<Connector?port="8080"?protocol="HTTP/1.1"
???????????????connectionTimeout="20000"
???????????????redirectPort="8443"?/>
????
????<Connector?port="8009"?protocol="AJP/1.3"?redirectPort="8443"?/>??//?連接器
?//?容器組件:一個 Engine 處理 Service 所有請求,包含多個 Host
????<Engine?name="Catalina"?defaultHost="localhost">
???//?容器組件:處理指定Host下的客戶端請求,?可包含多個 Context
??????<Host?name="localhost"??appBase="webapps"
????????????unpackWARs="true"?autoDeploy="true">
???//?容器組件:處理特定 Context Web應(yīng)用的所有客戶端請求
???<Context>Context>
??????Host>
????Engine>
??Service>
Server>
作為創(chuàng)始人,Catalina 還需要處理公司的各種異常情況,比如有人搶公章(執(zhí)行了 Ctrl C 關(guān)閉 Tomcat)。Tomcat 要如何清理資源呢?通過向 JVM 注冊一個「關(guān)閉鉤子」,具體關(guān)鍵邏輯詳見org.apache.catalina.startup.Catalina#start
源碼:- Server 不存在則解析
server.xml
創(chuàng)建; - 創(chuàng)建失敗則報錯;
- 啟動 Server;
- 創(chuàng)建并注冊「關(guān)閉鉤子」;
- await 方法監(jiān)聽停止請求。
???/**
?????*?Start?a?new?server?instance.
?????*/
????public?void?start()?{
????????//?如果?Catalina?持有的?Server?為空則解析?server.xml?創(chuàng)建
????????if?(getServer()?==?null)?{
????????????load();
????????}
????????if?(getServer()?==?null)?{
????????????log.fatal("Cannot?start?server.?Server?instance?is?not?configured.");
????????????return;
????????}
????????//?Start?the?new?server
????????try?{
????????????getServer().start();
????????}?catch?(LifecycleException?e)?{
????????????//?省略部分代碼
????????}
????????//?創(chuàng)建鉤子并注冊
????????if?(useShutdownHook)?{
????????????if?(shutdownHook?==?null)?{
????????????????shutdownHook?=?new?CatalinaShutdownHook();
????????????}
????????????Runtime.getRuntime().addShutdownHook(shutdownHook);
????????}
????????//?監(jiān)聽停止請求,內(nèi)部調(diào)用?Server?的?stop
????????if?(await)?{
????????????await();
????????????stop();
????????}
????}
當(dāng)我們需要在 JVM 關(guān)閉做一些清理工作,比如將緩存數(shù)據(jù)刷到磁盤或者清理一些文件,就可以向 JVM 注冊一個「關(guān)閉鉤子」。它其實就是一個線程,當(dāng) JVM 停止前嘗試執(zhí)行這個線程的 run 方法。org.apache.catalina.startup.Catalina.CatalinaShutdownHook????protected?class?CatalinaShutdownHook?extends?Thread?{
????????@Override
????????public?void?run()?{
????????????try?{
????????????????if?(getServer()?!=?null)?{
????????????????????Catalina.this.stop();
????????????????}
????????????}?catch?(Throwable?ex)?{
??????????????//?省略部分代碼....
????????????}
????????}
????}
其實就是執(zhí)行了 Catalina 的 stop 方法,通過它將整個 Tomcat 停止。Server
Server 組件的職責(zé)就是管理 Service 組件,負(fù)責(zé)調(diào)用持有的Service
的 start
方法。他就像是公司的 CEO,負(fù)責(zé)管理多個事業(yè)部,每個事業(yè)部就是一個 Service
。它管理兩個部門:- Connector 連接器:對外市場營銷部,推廣吹牛寫 PPT 的。
- Container 容器:研發(fā)部門,沒有性生活的 996 。
org.apache.catalina.core.StandardServer
,Server 繼承 org.apache.catalina.util.LifecycleMBeanBase,所以他的生命周期也被統(tǒng)一管理,Server 的子組件是 Service,所以還需要管理 Service 的生命周期。也就是說在啟動和關(guān)閉 Server 的時候會分別先調(diào)用 Service
的 啟動和停止方法。這就是設(shè)計思想呀,抽象出生命周期 Lifecycle
接口,體現(xiàn)出接口隔離原則,將生命周期的相關(guān)功能內(nèi)聚。
public?void?addService(Service?service)?{
????????service.setServer(this);
????????synchronized?(servicesLock)?{
????????????//?創(chuàng)建?長度? 1?的數(shù)組
????????????Service?results[]?=?new?Service[services.length? ?1];
????????????//?將舊的數(shù)據(jù)復(fù)制到新數(shù)組
????????????System.arraycopy(services,?0,?results,?0,?services.length);
????????????results[services.length]?=?service;
????????????services?=?results;
????????????//?啟動?Service?組件
????????????if?(getState().isAvailable())?{
????????????????try?{
????????????????????service.start();
????????????????}?catch?(LifecycleException?e)?{
????????????????????//?Ignore
????????????????}
????????????}
????????????//?發(fā)送事件
????????????support.firePropertyChange("service",?null,?service);
????????}
????}
在添加 Service 過程中動態(tài)拓展數(shù)組長度,為了節(jié)省內(nèi)存。除此之外,Server 組件還有一個重要的任務(wù)是啟動一個 Socket 來監(jiān)聽停止端口,這就是為什么你能通過 shutdown 命令來關(guān)閉 Tomcat。不知道你留意到?jīng)]有,上面 Caralina 的啟動方法的最后一行代碼就是調(diào)用了 Server 的 await 方法。在 await 方法里會創(chuàng)建一個 Socket 監(jiān)聽 8005 端口,并在一個死循環(huán)里接收 Socket 上的連接請求,如果有新的連接到來就建立連接,然后從 Socket 中讀取數(shù)據(jù);如果讀到的數(shù)據(jù)是停止命令“SHUTDOWN”,就退出循環(huán),進(jìn)入 stop 流程。Service
他的職責(zé)就是管理Connector 連接器
和 頂層容器 Engine
,會分別調(diào)用他們的 start 方法。至此,整個 Tomcat 就算啟動完成了。Service 就是事業(yè)部的話事人,管理兩個職能部門對外推廣部(連接器),對內(nèi)研發(fā)部(容器)。Service 組件的實現(xiàn)類是org.apache.catalina.core.StandardService
,直接看關(guān)鍵的成員變量。public?class?StandardService?extends?LifecycleMBeanBase?implements?Service?{
????//?名字
????private?String?name?=?null;
????
????//?所屬的?Server?實例
????private?Server?server?=?null;
?
????//?連接器數(shù)組
????protected?Connector?connectors[]?=?new?Connector[0];
????private?final?Object?connectorsLock?=?new?Object();
?
????//?對應(yīng)的?Engine?容器
????private?Engine?engine?=?null;
????
????//?映射器及其監(jiān)聽器
????protected?final?Mapper?mapper?=?new?Mapper();
????protected?final?MapperListener?mapperListener?=?new?MapperListener(this);
}
繼承 LifecycleMBeanBase
而 ?LifecycleMBeanBase 又繼承 LifecycleBase
,這里實際上是模板方法模式的運用,org.apache.catalina.util.LifecycleBase#init
,org.apache.catalina.util.LifecycleBase#start
,org.apache.catalina.util.LifecycleBase#stop
分別是對應(yīng)的模板方法,內(nèi)部定義了整個算法流程,子類去實現(xiàn)自己內(nèi)部具體變化部分,將變與不變抽象出來實現(xiàn)開閉原則設(shè)計思路。那為什么還有一個 MapperListener?這是因為 Tomcat 支持熱部署,當(dāng) Web 應(yīng)用的部署發(fā)生變化時,Mapper 中的映射信息也要跟著變化,MapperListener 就是一個監(jiān)聽器,它監(jiān)聽容器的變化,并把信息更新到 Mapper 中,這是典型的觀察者模式。作為“管理”角色的組件,最重要的是維護(hù)其他組件的生命周期。此外在啟動各種組件時,要注意它們的依賴關(guān)系,也就是說,要注意啟動的順序。我們來看看 Service 啟動方法:protected?void?startInternal()?throws?LifecycleException?{
?
????//1.?觸發(fā)啟動監(jiān)聽器
????setState(LifecycleState.STARTING);
?
????//2.?先啟動?Engine,Engine?會啟動它子容器
????if?(engine?!=?null)?{
????????synchronized?(engine)?{
????????????engine.start();
????????}
????}
????
????//3.?再啟動?Mapper?監(jiān)聽器
????mapperListener.start();
?
????//4.?最后啟動連接器,連接器會啟動它子組件,比如?Endpoint
????synchronized?(connectorsLock)?{
????????for?(Connector?connector:?connectors)?{
????????????if?(connector.getState()?!=?LifecycleState.FAILED)?{
????????????????connector.start();
????????????}
????????}
????}
}
這里啟動順序也很講究,Service 先啟動了 Engine 組件,再啟動 Mapper 監(jiān)聽器,最后才是啟動連接器。這很好理解,因為內(nèi)層組件啟動好了才能對外提供服務(wù),產(chǎn)品沒做出來,市場部也不能瞎忽悠,研發(fā)好了才能啟動外層的連接器組件。而 Mapper 也依賴容器組件,容器組件啟動好了才能監(jiān)聽它們的變化,因此 Mapper 和 MapperListener 在容器組件之后啟動。組件停止的順序跟啟動順序正好相反的,也是基于它們的依賴關(guān)系。Engine
他就是一個研發(fā)部的頭頭,是最頂層的容器組件。繼承Container
,所有的容器組件都繼承 Container,這里實際上運用了組合模式統(tǒng)一管理。他的實現(xiàn)類是 org.apache.catalina.core.StandardEngine
,繼承 ContainerBase
。public?class?StandardEngine?extends?ContainerBase?implements?Engine?{
}
他的子容器是 Host
,所以持有 Host 容器數(shù)組,這個屬性每個容器都會存在,所以放在抽象類中protected?final?HashMap?children?=?new?HashMap<>();
ContainerBase 用 HashMap 保存了它的子容器,并且 ContainerBase 還實現(xiàn)了子容器的“增刪改查”,甚至連子組件的啟動和停止都提供了默認(rèn)實現(xiàn),比如 ContainerBase 會用專門的線程池來啟動子容器。org.apache.catalina.core.ContainerBase#startInternal//?Start?our?child?containers,?if?any
Container?children[]?=?findChildren();
List>?results?=?new?ArrayList<>();
for?(Container?child?:?children)?{
??results.add(startStopExecutor.submit(new?StartChild(child)));
}
Engine 在啟動 Host 子容器時就直接重用了這個方法。容器組件最重要的功能是處理請求,而 Engine 容器對請求的“處理”,其實就是把請求轉(zhuǎn)發(fā)給某一個 Host 子容器來處理,具體是通過 Valve 來實現(xiàn)的。每一個容器組件都有一個 Pipeline,而 Pipeline 中有一個基礎(chǔ)閥(Basic Valve),透過構(gòu)造方法創(chuàng)建 Pipeline。public?StandardEngine()?{
????super();
????pipeline.setBasic(new?StandardEngineValve());
????//?省略部分代碼
}
Engine 容器的基礎(chǔ)閥定義如下:final?class?StandardEngineValve?extends?ValveBase?{
?
????public?final?void?invoke(Request?request,?Response?response)
??????throws?IOException,?ServletException?{
??
??????//?拿到請求中的?Host?容器
??????Host?host?=?request.getHost();
??????if?(host?==?null)?{
??????????return;
??????}
??
??????//?調(diào)用?Host?容器中的?Pipeline?中的第一個?Valve
??????host.getPipeline().getFirst().invoke(request,?response);
??}
??
}
這個基礎(chǔ)閥實現(xiàn)非常簡單,就是把請求轉(zhuǎn)發(fā)到 Host 容器。從代碼中可以看到,處理請求的 Host 容器對象是從請求中拿到的,請求對象中怎么會有 Host 容器呢?這是因為請求到達(dá) Engine 容器中之前,Mapper 組件已經(jīng)對請求進(jìn)行了路由處理,Mapper 組件通過請求的 URL 定位了相應(yīng)的容器,并且把容器對象保存到了請求對象中。