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

二、應(yīng)用部署與加載流程分析
下面就針對應(yīng)用部署與加載流程展開分析
2.1 部署方式
- 隱式部署
直接丟文件夾、war、jar 到 webapps 目錄,tomcat 會根據(jù)文件夾名稱自動生成虛擬路徑,簡單,但是需要重啟 Tomcat 服務(wù)器,包括要修改端口和訪問路徑的也需要重啟。
- 顯示部署
添加 context 元素 server.xml 中的 Host 加入一個 Context(指定路徑和文件地址),例如:
<Host name="localhost">
<Context path="/myapp" docBase="/opt/work_tomcat/myapp.war" />
</Host>
即/myapp 這個虛擬路徑映射到了 /opt/work_tomcat/myapp 目錄下(war 會解壓成文件),修改完 server.xml 需要重啟 tomcat 服務(wù)器。
- 創(chuàng)建 xml 文件
在 conf/Catalina/localhost 中創(chuàng)建 xml 文件,訪問路徑為文件名,例如:在 localhost 目錄下新建 demo.xml,內(nèi)容為:
<Context docBase="/opt/work_tomcat/myapp" />
不需要寫 path,虛擬目錄就是文件名 demo,path 默認(rèn)為/demo,添加 demo.xml 不需要重啟 tomcat 服務(wù)器。
2.2 Web應(yīng)用加載
Web應(yīng)用加載屬于Server啟動的核心處理過程。Catalina對Web應(yīng)用的加載主要由
StandardHost、
HostConfig、
StandardContext、
ContextConfig、
StandardWrapper
這5個類完成。
2.2.1 StandardHost
當(dāng)顯示部署時,Context元素將會作為Host容器的子容器添加到Host實例當(dāng)中,并在Host啟動時,由生命周期管理接口的start()方法啟動。
大多數(shù)情況下,我們使用的其實都是隱式部署。我們需要關(guān)注的是Digester解析器默認(rèn)為StandardHost容器添加了一個HostConfig監(jiān)聽器。
@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停止時注銷其對應(yīng)的MBean。邏輯在Host啟動時觸發(fā)START_EVENT事件,完成服務(wù)器啟動過程中的Web應(yīng)用部署(只有當(dāng)Host的deployOnStartup屬性為true時,服務(wù)器才會在啟動過程中部署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名稱>/<Host名稱>
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名稱>/<Host名稱>目錄下的xml文件 。
部署描述文件應(yīng)用的詳見HostConfig.deployDescriptor。
- War包部署
過濾$CATALINA_BASE/webapps目錄下所有符合條件的WAR包:不符合deployIgnore的過濾規(guī)則、文件名不為META-INF和WEB-INF、以war作為擴(kuò)展名的文件。
部署WAR包應(yīng)用的過程詳見HostConfig.deployWAR。
- Web目錄部署
過濾CATALINA_BASE/webapps目錄下所有符合條件的WAR包:不符合deployIgnore的過濾規(guī)則、文件名不為META-INF和WEB-INF、以war作為擴(kuò)展名的文件。
部署Web目錄應(yīng)用的過程詳見HostConfig.deployDirectory。
邏輯對于上述自動部署過程中,我們可以發(fā)現(xiàn),經(jīng)過一系列的條件判斷,最終工作就是構(gòu)建了一個Context實例,并添加ContextConfig生命周期監(jiān)聽器。
通過Host的addChild()方法將Context實例添加到Host。并在Host啟動時啟動Context。并根據(jù)不同的部署方式添加文件到守護(hù)資源,以便文件發(fā)生變更時重新部署或者加載Web應(yīng)用。
邏輯在Container容器的backgroundProcess()定期掃描Web應(yīng)用發(fā)生變更,并從新加載處理完成之后觸發(fā)PERIODIC_EVENT事件。
在HostConfig中通過DeployedApplication維護(hù)了兩個守護(hù)資源列表:redeployeResources和reloadResources,前者用于守護(hù)導(dǎo)致應(yīng)用重新部署的資源,后者守護(hù)導(dǎo)致應(yīng)用重新加載的資源。兩個列表分別維護(hù)了資源及其最后修改的時間。
當(dāng)HostConfig接收到PERIODIC_EVENT事件后,會檢測守護(hù)資源的變更情況。如果發(fā)生變更,將重新加載或者部署應(yīng)用以及更新資源的最后修改時間。
2.2.3 StandardContext
**WebappLoader **
需要特別關(guān)注的是在StandardContext.startInternal()方法中,每個Context都創(chuàng)建了一個WebappLoader應(yīng)用類加載器。那么它到底具備什么特殊的意義呢?
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {
protected synchronized void startInternal() throws LifecycleException {
//每個context新建一個應(yīng)用類加載器
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下的類,應(yīng)用級別隔離
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)用級別隔離
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類加載器加載;
優(yōu)先加載WEB-INF/classes,WEB-INF/lib;
然后再按照System,Common,Shared的順序加載。

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