Tomcat:應(yīng)用加載原理分析

前情回顧

上一篇文章主要了解了一下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)系。

image.png

二、應(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

  1. 當(dāng)顯示部署時(shí),Context元素將會(huì)作為Host容器的子容器添加到Host實(shí)例當(dāng)中,并在Host啟動(dòng)時(shí),由生命周期管理接口的start()方法啟動(dòng)。

  2. 大多數(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描述文件部署
  1. 掃描$CATALINA_BASE/conf/<Engine名稱(chēng)>/<Host名稱(chēng)>目錄下的xml文件 。

  2. 部署描述文件應(yīng)用的詳見(jiàn)HostConfig.deployDescriptor。

  • War包部署
  1. 過(guò)濾$CATALINA_BASE/webapps目錄下所有符合條件的WAR包:不符合deployIgnore的過(guò)濾規(guī)則、文件名不為META-INF和WEB-INF、以war作為擴(kuò)展名的文件。

  2. 部署WAR包應(yīng)用的過(guò)程詳見(jiàn)HostConfig.deployWAR。

  • Web目錄部署
  1. 過(guò)濾CATALINA_BASE/webapps目錄下所有符合條件的WAR包:不符合deployIgnore的過(guò)濾規(guī)則、文件名不為META-INF和WEB-INF、以war作為擴(kuò)展名的文件。

  2. 部署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;
    }
}

打破了雙親委派模型:

  1. 首先從JVM的Bootstrap類(lèi)加載器加載;

  2. 優(yōu)先加載WEB-INF/classes,WEB-INF/lib;

  3. 然后再按照System,Common,Shared的順序加載。

image.png

這么設(shè)計(jì)的目的主要是考慮到以下三個(gè)方面:

  1. 邏輯隔離性:

    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)成功。

  2. 靈活性:

    既然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)。

  3. 性能:

    由于每個(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í)順序從高到底依次為:

  1. Web應(yīng)用配置(META-INF/context.xml)。

  2. Host配置(conf/context.xml.default)。

  3. 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è)。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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