前情回顧
上一篇文章主要了解了一下Tomcat啟動(dòng)入口,以及初步的分析了Tomcat的啟動(dòng)流程,下面我們將會(huì)解密Tomcat應(yīng)用部署的實(shí)際流程。
一、直觀對(duì)比
雖然前面已經(jīng)說(shuō)了那么多關(guān)于Tomcat的東西,但是我相信絕大部分同學(xué)應(yīng)該都沒(méi)有專(zhuān)門(mén)的去研究過(guò)Tomcat的內(nèi)部實(shí)現(xiàn)。我們接觸最多的應(yīng)該還是上傳一個(gè)war包丟在webapps目錄下,然后重啟一下Tomcat服務(wù)器(甚至不重啟)。下面我們以圖形的形式,直觀的對(duì)比Tomcat各組件的關(guān)系。

二、應(yīng)用部署與加載流程分析
下面就針對(duì)應(yīng)用部署與加載流程展開(kāi)分析
2.1 部署方式
- 隱式部署
直接丟文件夾、war、jar 到 webapps 目錄,tomcat 會(huì)根據(jù)文件夾名稱(chēng)自動(dòng)生成虛擬路徑,簡(jiǎn)單,但是需要重啟 Tomcat 服務(wù)器,包括要修改端口和訪(fǎng)問(wèn)路徑的也需要重啟。
- 顯示部署
添加 context 元素 server.xml 中的 Host 加入一個(gè) Context(指定路徑和文件地址),例如:
<Host name="localhost">
<Context path="/myapp" docBase="/opt/work_tomcat/myapp.war" />
</Host>
即/myapp 這個(gè)虛擬路徑映射到了 /opt/work_tomcat/myapp 目錄下(war 會(huì)解壓成文件),修改完 server.xml 需要重啟 tomcat 服務(wù)器。
- 創(chuàng)建 xml 文件
在 conf/Catalina/localhost 中創(chuàng)建 xml 文件,訪(fǎng)問(wèn)路徑為文件名,例如:在 localhost 目錄下新建 demo.xml,內(nèi)容為:
<Context docBase="/opt/work_tomcat/myapp" />
不需要寫(xiě) path,虛擬目錄就是文件名 demo,path 默認(rèn)為/demo,添加 demo.xml 不需要重啟 tomcat 服務(wù)器。
2.2 Web應(yīng)用加載
Web應(yīng)用加載屬于Server啟動(dòng)的核心處理過(guò)程。Catalina對(duì)Web應(yīng)用的加載主要由
StandardHost、
HostConfig、
StandardContext、
ContextConfig、
StandardWrapper
這5個(gè)類(lèi)完成。
2.2.1 StandardHost
當(dāng)顯示部署時(shí),Context元素將會(huì)作為Host容器的子容器添加到Host實(shí)例當(dāng)中,并在Host啟動(dòng)時(shí),由生命周期管理接口的start()方法啟動(dòng)。
大多數(shù)情況下,我們使用的其實(shí)都是隱式部署。我們需要關(guān)注的是Digester解析器默認(rèn)為StandardHost容器添加了一個(gè)HostConfig監(jiān)聽(tīng)器。
@Override
publicvoid addRuleInstances(Digester digester) {
digester.addObjectCreate(prefix + "Host","org.apache.catalina.core.StandardHost",
"className");
digester.addSetProperties(prefix + "Host");
digester.addRule(prefix + "Host",
new CopyParentClassLoaderRule());
digester.addRule(prefix + "Host",
new LifecycleListenerRule("org.apache.catalina.startup.HostConfig",
"hostConfigClass"));
//省略部分代碼...
}
2.2.2 HostConfig
HostConfig處理的生命周期事件包括:
START_EVENT
PERIODIC_EVENT
STOP_EVENT
其中,前兩者都與Web應(yīng)用部署密切相關(guān),后者用于在Host停止時(shí)注銷(xiāo)其對(duì)應(yīng)的MBean。邏輯在Host啟動(dòng)時(shí)觸發(fā)START_EVENT事件,完成服務(wù)器啟動(dòng)過(guò)程中的Web應(yīng)用部署(只有當(dāng)Host的deployOnStartup屬性為true時(shí),服務(wù)器才會(huì)在啟動(dòng)過(guò)程中部署Web應(yīng)用,該屬性默認(rèn)值為true)。
public class HostConfig implements LifecycleListener {
/**
* Process a "start" event for this Host.
*/
public void start() {
//省略部分代碼...
if (host.getDeployOnStartup()) {
//部署當(dāng)前虛擬機(jī)的應(yīng)用
deployApps();
}
}
protected void deployApps() {
//默認(rèn)為¨E95EBASE/webappsFileappBase¨E61Ehost.getAppBaseFile();//默認(rèn)為CATALINA_BASE/webapps
File appBase = host.getAppBaseFile();
//默認(rèn)為CATALINA¨E95EBASE/webappsFileappBase¨E61Ehost.getAppBaseFile();//默認(rèn)為CATALINA_BASE/conf/<Engine名稱(chēng)>/<Host名稱(chēng)>
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase 描述文件部署
deployDescriptors(configBase, configBase.list());
// Deploy WARs War包部署
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders 目錄部署
deployDirectories(appBase, filteredAppPaths);
}
}
- Context描述文件部署
掃描$CATALINA_BASE/conf/<Engine名稱(chēng)>/<Host名稱(chēng)>目錄下的xml文件 。
部署描述文件應(yīng)用的詳見(jiàn)HostConfig.deployDescriptor。
- War包部署
過(guò)濾$CATALINA_BASE/webapps目錄下所有符合條件的WAR包:不符合deployIgnore的過(guò)濾規(guī)則、文件名不為META-INF和WEB-INF、以war作為擴(kuò)展名的文件。
部署WAR包應(yīng)用的過(guò)程詳見(jiàn)HostConfig.deployWAR。
- Web目錄部署
過(guò)濾CATALINA_BASE/webapps目錄下所有符合條件的WAR包:不符合deployIgnore的過(guò)濾規(guī)則、文件名不為META-INF和WEB-INF、以war作為擴(kuò)展名的文件。
部署Web目錄應(yīng)用的過(guò)程詳見(jiàn)HostConfig.deployDirectory。
邏輯對(duì)于上述自動(dòng)部署過(guò)程中,我們可以發(fā)現(xiàn),經(jīng)過(guò)一系列的條件判斷,最終工作就是構(gòu)建了一個(gè)Context實(shí)例,并添加ContextConfig生命周期監(jiān)聽(tīng)器。
通過(guò)Host的addChild()方法將Context實(shí)例添加到Host。并在Host啟動(dòng)時(shí)啟動(dòng)Context。并根據(jù)不同的部署方式添加文件到守護(hù)資源,以便文件發(fā)生變更時(shí)重新部署或者加載Web應(yīng)用。
邏輯在Container容器的backgroundProcess()定期掃描Web應(yīng)用發(fā)生變更,并從新加載處理完成之后觸發(fā)PERIODIC_EVENT事件。
在HostConfig中通過(guò)DeployedApplication維護(hù)了兩個(gè)守護(hù)資源列表:redeployeResources和reloadResources,前者用于守護(hù)導(dǎo)致應(yīng)用重新部署的資源,后者守護(hù)導(dǎo)致應(yīng)用重新加載的資源。兩個(gè)列表分別維護(hù)了資源及其最后修改的時(shí)間。
當(dāng)HostConfig接收到PERIODIC_EVENT事件后,會(huì)檢測(cè)守護(hù)資源的變更情況。如果發(fā)生變更,將重新加載或者部署應(yīng)用以及更新資源的最后修改時(shí)間。
2.2.3 StandardContext
**WebappLoader **
需要特別關(guān)注的是在StandardContext.startInternal()方法中,每個(gè)Context都創(chuàng)建了一個(gè)WebappLoader應(yīng)用類(lèi)加載器。那么它到底具備什么特殊的意義呢?
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {
protected synchronized void startInternal() throws LifecycleException {
//每個(gè)context新建一個(gè)應(yīng)用類(lèi)加載器
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader();
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
}
public void setLoader(Loader loader) {
if (getState().isAvailable() && (loader != null) &&
(loader instanceof Lifecycle)) {
try {
//執(zhí)行webapploader.starter ==> webappclassloader.startInternal
((Lifecycle) loader).start();
} catch (LifecycleException e) {
log.error(sm.getString("standardContext.setLoader.start"), e);
}
}
}
}
public abstract class WebappClassLoaderBase extends URLClassLoader
implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
//只加載當(dāng)前context下的類(lèi),應(yīng)用級(jí)別隔離
WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
}
//只加載當(dāng)前context下的jar包,應(yīng)用級(jí)別隔離
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
}
打破了雙親委派模型:
首先從JVM的Bootstrap類(lèi)加載器加載;
優(yōu)先加載WEB-INF/classes,WEB-INF/lib;
然后再按照System,Common,Shared的順序加載。

這么設(shè)計(jì)的目的主要是考慮到以下三個(gè)方面:
-
邏輯隔離性:
Web應(yīng)用類(lèi)庫(kù)相互隔離,避免依賴(lài)庫(kù)或者應(yīng)用包相互影響。比如有兩個(gè)應(yīng)用分別采用了Spring2.5和Spring5.0,如果應(yīng)用服務(wù)器使用同一個(gè)類(lèi)加載器加載,那么Web應(yīng)用將會(huì)由于Jar包覆蓋而導(dǎo)致無(wú)法啟動(dòng)成功。
-
靈活性:
既然Web應(yīng)用之間的類(lèi)加載器相互獨(dú)立,那么我們就能只針對(duì)一個(gè)Web應(yīng)用進(jìn)行重新部署,此事該Web應(yīng)用的類(lèi)加載器將會(huì)重新創(chuàng)建,而且不會(huì)影響其他Web應(yīng)用。如果共用一個(gè)類(lèi)加載器顯然無(wú)法實(shí)現(xiàn),因?yàn)橹挥幸粋€(gè)類(lèi)加載器的時(shí)候,類(lèi)質(zhì)檢的依賴(lài)是雜亂無(wú)章的,無(wú)法完整的移出某一個(gè)Web應(yīng)用的類(lèi)。
-
性能:
由于每個(gè)Web應(yīng)用都有一個(gè)類(lèi)加載器,因此Web應(yīng)用再加載類(lèi)時(shí),不會(huì)搜索其他應(yīng)用包含的Jar包,性能自然高于應(yīng)用服務(wù)器只有一個(gè)類(lèi)加載器的情況。
2.2.4 ContextConfig
當(dāng)我們?cè)趧?chuàng)建Context的時(shí)候會(huì)同時(shí)創(chuàng)建ContextConfig作為它的狀態(tài)監(jiān)聽(tīng)器,在Context執(zhí)行startInternal()方法時(shí),會(huì)發(fā)布一個(gè)Lifecycle.CONFIGURE_START_EVENT事件通知ContextConfig做后續(xù)的工作。
需要注意的是:
① 當(dāng)觸發(fā)AFTER_INIT_EVENT事件時(shí),解析ConfigFile文件,按優(yōu)先級(jí)順序從高到底依次為:
Web應(yīng)用配置(META-INF/context.xml)。
Host配置(conf/context.xml.default)。
Catalina配置(conf/context.xml)。
② 當(dāng)觸發(fā)BEFORE_START_EVENT事件時(shí),會(huì)執(zhí)行ExpandWar.expand方法去解壓war包。
③ 當(dāng)觸發(fā)CONFIGURE_START_EVENT事件時(shí),ContextConfig.webConfig()方法會(huì)解析web.xml,創(chuàng)建Servlet,F(xiàn)ilter,ServletContextListener等Web容器相關(guān)的對(duì)象從而完成初始化。
2.2.5 StandardWrapper
StandardWrapper具體維護(hù)了Servlet實(shí)例,當(dāng)ContextConfig完成初始化之后,會(huì)根據(jù)WebXml中的Servlet定義創(chuàng)建Wrapper。創(chuàng)建Servlet實(shí)例,執(zhí)行javax.servlet.Servlet.init()完成Servlet的初始化。
TIP:如果想要詳細(xì)了解服務(wù)啟動(dòng)及加載的流程圖可以查看官網(wǎng)提供的資料
http://tomcat.apache.org/tomcat-9.0-doc/architecture/startup/serverStartup.pdf
三、本文小結(jié)
我們發(fā)現(xiàn)Tomcat可以部署多個(gè)應(yīng)用,每個(gè)Context則對(duì)應(yīng)了一個(gè)應(yīng)用,應(yīng)用部署的方式可以是文件夾也可以是war包,如果是war包部署,它還會(huì)自動(dòng)幫我們將war包解壓出來(lái)。每個(gè)應(yīng)用中又有各自的Servlet。
至此我們可以說(shuō)Tomcat將應(yīng)用已經(jīng)部署完畢,下次我們將分析一個(gè)普通的HTTP請(qǐng)求是如何經(jīng)過(guò)網(wǎng)絡(luò)層,到達(dá)我們的Tomcat,再經(jīng)過(guò)我們的應(yīng)用處理,最后返回出請(qǐng)求結(jié)果。
程序員的核心競(jìng)爭(zhēng)力其實(shí)還是技術(shù),因此對(duì)技術(shù)還是要不斷的學(xué)習(xí),關(guān)注 “IT 巔峰技術(shù)” 公眾號(hào) ,該公眾號(hào)內(nèi)容定位:中高級(jí)開(kāi)發(fā)、架構(gòu)師、中層管理人員等中高端崗位服務(wù)的,除了技術(shù)交流外還有很多架構(gòu)思想和實(shí)戰(zhàn)案例。
作者是 《 消息中間件 RocketMQ 技術(shù)內(nèi)幕》 一書(shū)作者,同時(shí)也是 “RocketMQ 上海社區(qū)”聯(lián)合創(chuàng)始人,曾就職于拼多多、德邦等公司,現(xiàn)任上市快遞公司架構(gòu)負(fù)責(zé)人,主要負(fù)責(zé)開(kāi)發(fā)框架的搭建、中間件相關(guān)技術(shù)的二次開(kāi)發(fā)和運(yùn)維管理、混合云及基礎(chǔ)服務(wù)平臺(tái)的建設(shè)。