Tomcat 加載應(yīng)用過程

從上面那篇文章里面我們知道每個(gè)應(yīng)用(我們打包的war)對(duì)應(yīng)一個(gè)Context,每個(gè)Servlet對(duì)應(yīng)一個(gè)Wrapper。但是應(yīng)用、Servlet是怎么被裝載進(jìn)Tomcat?怎么被統(tǒng)一管理的?是如何對(duì)應(yīng)到Context、Warpper的?下面我們一起來分析下。

Context創(chuàng)建過程

按照正常思路,當(dāng)前容器一般應(yīng)該是由其父容器進(jìn)行加載跟啟動(dòng),于是可以看下Context的父容器StandardHost類,發(fā)現(xiàn)StandardHost類非常簡單,并沒有想象中的Context的發(fā)現(xiàn)、解析、裝載等。于是我們只能回頭看Catalina的啟動(dòng)過程,由于Tomcat是使用Digester來解析server.xml文件的,所有看Tomcat的啟動(dòng)過程主要分析Digester的生成即可(在Catalina#createStartDigester方法),我們看到HostRuleSet中會(huì)對(duì)添加一個(gè)LifecycleListenerRule的Rule

digester.addRule(prefix + "Host",
                         new LifecycleListenerRule
                         ("org.apache.catalina.startup.HostConfig",
                          "hostConfigClass"));

而LifecycleListenerRule做的事情其實(shí)就是初始化了HostConfig這個(gè)Listener,并且為StandardHost增加了這個(gè)Listener。

Container c = (Container) digester.peek();
......
// Instantiate a new LifecycleListener implementation object
Class<?> clazz = Class.forName(className);
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();

// Add this LifecycleListener to our associated component
c.addLifecycleListener(listener);

接著我們?cè)敿?xì)分析下HostConfig這個(gè)類。由于這個(gè)類是一個(gè)LifecycleListener接口的實(shí)現(xiàn)類,所以很容易找他的對(duì)外方法就是lifecycleEvent方法。

public void lifecycleEvent(LifecycleEvent event) {

    // Identify the host we are associated with
    try {
        host = (Host) event.getLifecycle();
        if (host instanceof StandardHost) {
            setCopyXML(((StandardHost) host).isCopyXML());
            setDeployXML(((StandardHost) host).isDeployXML());
            setUnpackWARs(((StandardHost) host).isUnpackWARs());
            setContextClass(((StandardHost) host).getContextClass());
        }
    } catch (ClassCastException e) {
        log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
        check();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.START_EVENT)) {
        start();
    } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
        stop();
    }
}

可以看到這里做了一些屬性的初始化,因?yàn)檫@些參數(shù)基本都是配置在Host節(jié)點(diǎn)上的,這里將屬性做了copy便于后續(xù)使用。

我們都知道整個(gè)容器的生命周期都是遵循Lifecycle接口規(guī)范的,在Host啟動(dòng)時(shí)候就會(huì)觸發(fā)Lifecycle.START_EVENT事件,所以我們主要看啟動(dòng)過程的start方法,而start方法里面主要就是調(diào)用的deployApps方法。

deployApps

protected void deployApps() {
        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);
    }
tomcat應(yīng)用的三種部署方式
  • deployDescriptors:按照描述文件方式部署,描述文件位置是$CATALINA_BASE/conf/[enginename]/[hostname]/[webappname].xml,會(huì)按照配置文件中的描述去創(chuàng)建Context,并添加資源信息。
  • deployWARs:會(huì)首先嘗試看是否已經(jīng)有解壓的app目錄,如果過有用已解壓的部署,否則會(huì)使用war包部署,會(huì)先嘗試使用META-INF/context.xml來創(chuàng)建Context,如果沒有這個(gè)文件則使用反射創(chuàng)建一個(gè)默認(rèn)的Context。
  • deployDirectories:可以看成deployWARs的簡化版本,去除了檢查使用war包部署的過程,其他流程一致。
應(yīng)用的部署流程

細(xì)看下這三種部署方式的代碼,各有差異,但整體的流程是一致的

  1. 創(chuàng)建Context。deployDescriptors這種方式只會(huì)根據(jù)描述文件去創(chuàng)建,否則會(huì)創(chuàng)建失敗。而其他兩種方式會(huì)首先嘗試根據(jù)描述文件去創(chuàng)建,如果找不到則會(huì)創(chuàng)建一個(gè)默認(rèn)的Context。在Tomcat的Document中說“Any Context Descriptors will be deployed first.”,可以看出跟我們之前分析一致,而Tomcat應(yīng)該也是推薦我們用描述文件的方式去創(chuàng)建。實(shí)際使用的時(shí)候我們大多不會(huì)去特意定義Context.xml,更多的是使用現(xiàn)在比較流行的“約定大于配置”思想去創(chuàng)建默認(rèn)的,其實(shí)這樣也帶來了統(tǒng)一性的好處。使用統(tǒng)一的配置無論是在后續(xù)的問題排查還是后續(xù)的維護(hù)都帶來明顯的好處。
  2. 設(shè)置基本屬性。包括創(chuàng)建LifecycleListener,這個(gè)LifecycleListener的作用跟HostConfig類似,設(shè)置Tomcat的名字、版本等信息。
  3. 添加到父容器。通過host.addChild(context)添加到父容器中去。
  4. 創(chuàng)建DeployedApplication對(duì)象。包括當(dāng)前app的名稱,是否有描述文件,需要檢測(cè)的資源信息(用與檢測(cè)變化重新部署),以及已經(jīng)部署標(biāo)識(shí),防止同一個(gè)應(yīng)用多次部署。

Context啟動(dòng)

到這里應(yīng)用也就是Context的創(chuàng)建就告一段落了,接下來看下Context的啟動(dòng)以及Servlet的創(chuàng)建過程,主要看下StandardContext#startInternal方法。

    // Post work directory
    postWorkDirectory();

    // Add missing components as necessary
    if (getResources() == null) {   // (1) Required by Loader
        if (log.isDebugEnabled())
            log.debug("Configuring default Resources");

        try {
            setResources(new StandardRoot(this));
        } catch (IllegalArgumentException e) {
            log.error(sm.getString("standardContext.resourcesInit"), e);
            ok = false;
        }
    }
    if (ok) {
        resourcesStart();
    }

首先設(shè)置了工作目錄,一般是:$CATALINA_BASE/work/[enginename]/[hostname]/[webappname]。用來存儲(chǔ)一些運(yùn)行過程中的一些數(shù)據(jù),比如JSP運(yùn)行時(shí)候創(chuàng)建的Java文件以及對(duì)應(yīng)的Class文件等。

接著創(chuàng)建了Resource對(duì)象,并且啟動(dòng),在這里會(huì)根據(jù)當(dāng)前Context的類型創(chuàng)建不同的WebResourceSet并且掃描應(yīng)用/WEB-INF/lib下的jar文件,創(chuàng)建相應(yīng)的ResourceSet。

    if (getLoader() == null) {
        WebappLoader webappLoader = new WebappLoader();
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }
    ... ...
        // Start our subordinate components, if any
        Loader loader = getLoader();
        if (loader instanceof Lifecycle) {
            ((Lifecycle) loader).start();
        }

        // since the loader just started, the webapp classloader is now
        // created.
        setClassLoaderProperty("clearReferencesRmiTargets",
                getClearReferencesRmiTargets());
        setClassLoaderProperty("clearReferencesStopThreads",
                getClearReferencesStopThreads());
        setClassLoaderProperty("clearReferencesStopTimerThreads",
                getClearReferencesStopTimerThreads());
        setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread",
                getClearReferencesHttpClientKeepAliveThread());
        setClassLoaderProperty("clearReferencesObjectStreamClassCaches",
                getClearReferencesObjectStreamClassCaches());
        setClassLoaderProperty("clearReferencesThreadLocals",
                getClearReferencesThreadLocals());

        // By calling unbindThread and bindThread in a row, we setup the
        // current Thread CCL to be the webapp classloader
        unbindThread(oldCCL);
        oldCCL = bindThread();

這里主要?jiǎng)?chuàng)建了一個(gè)ClassLoader(ParallelWebappClassLoader),并且啟動(dòng)、綁定到線程上,Tomcat為了應(yīng)用的隔離性,每個(gè)應(yīng)用都會(huì)創(chuàng)建一個(gè)獨(dú)有的ClassLoader,后續(xù)會(huì)專門講下這個(gè)ClassLocader。

Wrapper加載過程

    // Notify our interested LifecycleListeners
    fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

在往下我們就看到這個(gè)代碼塊了,觸發(fā)了一個(gè)CONFIGURE_START_EVENT時(shí)間,還記得上面創(chuàng)建Context時(shí)候添加的LifecycleListener嗎?現(xiàn)在到了看org.apache.catalina.startup.ContextConfig的時(shí)候了。

ContextConfig

我們到ContextConfig中找到lifecycleEvent方法,發(fā)現(xiàn)觸發(fā)這個(gè)事件的時(shí)候調(diào)用了configureStart方法,而這個(gè)方法里面最核心的調(diào)用方法webConfig。

    /**
     * Scan the web.xml files that apply to the web application and merge them
     * using the rules defined in the spec. For the global web.xml files,
     * where there is duplicate configuration, the most specific level wins. ie
     * an application's web.xml takes precedence over the host level or global
     * web.xml file.
     */

這是這個(gè)方法的注釋,簡單的來說就是對(duì)web.xml的掃描、解析、合并。

    WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
                context.getXmlValidation(), context.getXmlBlockExternal());

    Set<WebXml> defaults = new HashSet<>();
    defaults.add(getDefaultWebXmlFragment(webXmlParser));

    WebXml webXml = createWebXml();

    // Parse context level web.xml
    InputSource contextWebXml = getContextWebXmlSource();
    if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
        ok = false;
    }

這段比較簡單,就是把web.xml解析成了WebXml對(duì)象。接下來是對(duì)整個(gè)Servlet的加載過程,我也按照源碼中標(biāo)記的步驟逐步說明下。

  1. processJarsForWebFragments。 掃描/WEB-INF/lib 目錄下的每個(gè)jar文件中是否包含 /META-INF/web-fragment.xml文件,如果存在則解析并返回?;蛟S大家對(duì)/META-INF/web-fragment.xml比較陌生,原本一個(gè)web應(yīng)用的任何配置都需要放在web.xml中,當(dāng)項(xiàng)目比較大的時(shí)候會(huì)使得web.xml變得比較混亂。于是從Servlet 3.0開始就可以將每個(gè)Servlet、Filter、Listener打成jar包,放在WEB-INF\lib目錄下,每個(gè)模塊都有各自的配置文件,這個(gè)配置文件的名子就是 web-fragment.xml。但其實(shí)現(xiàn)在大家進(jìn)行web應(yīng)用開發(fā)的時(shí)候基本都是使用Spring MVC所以基本也不大會(huì)用到這個(gè)特性。
  2. WebXml.orderWebFragments。按照Servlet規(guī)范,在處理之前需要先對(duì)這些片段進(jìn)行排序。
  3. processServletContainerInitializers。掃描ServletContainerInitializer的實(shí)現(xiàn)類。
    前提:當(dāng)配置不完全的時(shí)候(metadata-complete=false),執(zhí)行如下步驟
  4. processClasses(webXml, orderedFragments)。這里分為兩步處理,掃描①/WEB-INF/classes下②每個(gè)fragment下,Servlet相關(guān)的注解。如WebServlet、WebFilter、WebListener,將其初始化為ServletDef對(duì)象,并加入到fragment中。
  5. webXml.merge(orderedFragments)。將每個(gè)單獨(dú)的fragment合并到主的webXml中。
  6. webXml.merge(defaults)。合并默認(rèn)的webXml,一般包含了JSP Servlet的定義,默認(rèn)的web.xml一般在$CATALINA_BASE/conf/web.xml。
  7. convertJsps(webXml)。將顯示指定的JSP轉(zhuǎn)換為Servlet,這里會(huì)用到JSP Servlet的定義,如果沒有也沒關(guān)系,會(huì)創(chuàng)建一個(gè)默認(rèn)的。
  8. configureContext(webXml)。這個(gè)是最重要的一個(gè)方法,看名字就是配置Context,方法內(nèi)基本上也是將web.xml解析完的數(shù)據(jù)賦值給Context對(duì)象,包括顯示名稱、介紹、版本等信息。也會(huì)將例如Filter、Lintener加入到Context中,當(dāng)然最重要的是將ServletDef對(duì)象轉(zhuǎn)化為Wrapper對(duì)象,并且添加為Context的子容器。有興趣的可以細(xì)看下這塊代碼,還有包括Session配置,歡迎頁面等的處理都在這塊代碼。
  9. processResourceJARs(resourceJars)。這里主要是加載fragment中的靜態(tài)資源,META-INF/resources/,主程序中的資源在前面已經(jīng)加載過了。
  10. context.addServletContainerInitializer(entry.getKey(), entry.getValue())。將ServletContainerInitializer的配置信息賦予Context。

Tomcat Web Application Deployment

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

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

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