Tomcat的Catalina篇3-StandardHost和HostConfig(三种部署方式)

559 阅读10分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. 组件启动和Web应用加载

组件启动satrt方法流程图:

image.png

详细注解分析见tomcat-9.0.60-src源码解析 。接下来我们直接进入web应用加载。Web应用加载属于Server启动的核心处理过程

Catalina对Web应用的加载主要由StandardHost、HostConfig、StandardContext、ContextConfig、StandardWrapperi这5个类完成。如果以一张时序图来展示Catalina对Web应用的加载过程,对应的流程图为: image.png 本文我们重点分析StandardHost和HostConfig监听器。

2. StandardHost

StandardHost加载Web应用(即StandardContext)的入口有两个。其中一个入口是在Catalina构造Server实例时,如果Host元素存在Context子元素(server.xml中),那么Context元素将会作为Host容器的子容器添加到Host实例当中,并在Host启动时,由生命周期管理接口的start()方法启动(默认调用子容器的start()方法)。 此时,Context的配置一般如下所示:

<Host name="localhost"appBase="webapps"unpackWARs="true"autoDeploy="true">
    <Context docBase="myApp"path="/myApp"reloadable="true"></Context>
</Host>

其中,docBase为Web应用根目录的文件路径,path为Web应用的根请求地址。如上,假使我们的 Tomcat地址为htp:/127.0.0.1:8080,那么,Web应用的根请求地址为htp:/127.0.0.l:8080/myApp。

通过此方式加载,尽管Tomcat处理简单(当解析server.xml时一并完成Context的创建),但对于使用者来说却并不是一种好方式,毕竟,没有人愿意每次部署新的Wb应用或者删除旧应用时,都必须修改一下server.xml文件

另一个入口则是由HostConfig自动扫描部署目录,创建Context实例并启动。这是大多数Web应用的加载方式,此部分将在下面说明。

2.1 源码分析

我们直接看StandardHost.startInternal():

protected synchronized void startInternal() throws LifecycleException {

    // Set error report valve
    // 设置ErrorReportValve阀门
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        try {
            boolean found = false;
            Valve[] valves = getPipeline().getValves();
            for (Valve valve : valves) {
                if (errorValve.equals(valve.getClass().getName())) {
                    found = true;
                    break;
                }
            }
            if(!found) {
                Valve valve = ErrorReportValve.class.getName().equals(errorValve) ?
                    new ErrorReportValve() :
                    (Valve) Class.forName(errorValve).getConstructor().newInstance();
                getPipeline().addValve(valve);
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                    "standardHost.invalidErrorReportValveClass",
                    errorValve), t);
        }
    }
    super.startInternal();
}

ContainerBase.startInternal():

protected synchronized void startInternal() throws LifecycleException {

    // Start our subordinate components, if any
    logger = null;
    getLogger();
    // 1.如果配置了集群组件Cluster,则启动
    Cluster cluster = getClusterInternal();
    if (cluster instanceof Lifecycle) {
        // 在此启动集群组件
        ((Lifecycle) cluster).start();
    }
    // 2.如果配置了安全组件Realm,则启动
    Realm realm = getRealmInternal();
    if (realm instanceof Lifecycle) {
        ((Lifecycle) realm).start();
    }

    // Start our child containers, if any
    // 3.找到Container所有的孩子(Host,或Context,或Wrapper)
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (Container child : children) {
        // StartChild是一个Callable,交给线程池慢慢执行
        results.add(startStopExecutor.submit(new StartChild(child)));
    }

    MultiThrowable multiThrowable = null;

    for (Future<Void> result : results) {
        try {
            // 获取异步执行结果
            result.get();
        } catch (Throwable e) {
            log.error(sm.getString("containerBase.threadedStartFailed"), e);
            if (multiThrowable == null) {
                multiThrowable = new MultiThrowable();
            }
            multiThrowable.add(e);
        }

    }
    if (multiThrowable != null) {
        throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
                multiThrowable.getThrowable());
    }

    // Start the Valves in our pipeline (including the basic), if any
    // 4.管道启动
    if (pipeline instanceof Lifecycle) {
        // 管道启动,里面所有的阀门(Value)启动,阀门设置一个状态
        ((Lifecycle) pipeline).start();
    }

    // 设置组件状态STARTING,此时会触发START_EVENT生命周期事件。
    // HostConfig监听到该事件,扫描Web部署目录,对于部署描述文件,WAR包,目录会自动创建StandardContext实例,添加到Host并启动
    setState(LifecycleState.STARTING);

    // Start our thread
    // 启动层级的后台任务处理,包括Cluster后台任务处理(包括部署变更检测,心跳),Realm后台任务处理,
    // Pipeline中Value的后台任务处理
    if (backgroundProcessorDelay > 0) {
        monitorFuture = Container.getService(ContainerBase.this).getServer()
                .getUtilityExecutor().scheduleWithFixedDelay(
                        new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
    }
}

StandardHost的启动加载过程如下:

  1. 为Host添加一个Valve实现ErrorReportValve(我们也可以通过修改Host的errorReportValveClass属性指定自己的错误处理Valve),该类主要用于在服务器处理异常时输出错误页面。如果我们没有在web.xml中添加错误处理页面,Tomcat返回的异常栈页面便是由ErrorReportValve生成的。

  2. 调用StandardHost父类ContainerBase的startInternal()方法启动虚拟主机,其处理主要分为如下几步。

    • 如果配置了集群组件Cluster则启动。
    • 如果配置了安全组件Realm则启动。
    • 启动子节点(即通过server.xml中的\<Context>创建的StandardContext实例)
    • 启动Host持有的Pipeline组件。
    • 设置Host状态为STARTING,此时会触发START_EVENT生命周期事件HostConfig监听该事件,扫描Web部署目录,对于部署描述文件、WAR包、目录会自动创建StandardContext实例,添加到Host并启动,具体见下面小节。
    • 启动Host层级的后台任务处理:Cluster后台任务处理(包括部署变更检测、心跳)、Realm后台任务处理、Pipeline中Valve的后台任务处理(某些Valvei通过后台任务实现定期处理功能,如StuckThreadDetectionValve用于定时检测耗时请求并输出)

3. HostConfig监听器

实际上在大多数情况下,Wb应用部署并不需要配置多个基础目录,而是能够做到自动、灵活部署,这也是Tomcat的默认部署方式。

在默认情况下,server.xml并未包含Context相关配置,仅包含Host配置如下:

<Host name="localhost"appBase="webapps"unpackWARs="true"autoDeploy="true"></Host>

其中,appBase为Web应用部署的基础目录,所有需要部署的Web应用均需要复制到此目录下,默 认为$CATALINA_BASE/webapps。Tomcat通过HostConfig完成该目录下Web应用的自动部署

前面的时序图仅描述了HostConfig的基本的API调用,它实际的处理过程要复杂得多,接下来让我们进行仔细分析。

在讲解Server的创建时,我们曾讲到,HostConfig是一个LifecycleListener实现,并且由Catalina默认添加到Host实例上。详见Digester解析器中Host解析image.png

HostConfig处理的生命周期事件包括:START_EVENT、PERIODIC_EVENT、STOP_EVENT。其中,前两者都与Web应用部署密切相关,后者用于在Host停止时注销其对应的MBean。

3.1 启动事件监听器

具体代码路径为StandardHost.startInternal() -> InContainerBase.startInternal()如下:

    ...
    // 设置组件状态STARTING,此时会触发START_EVENT生命周期事件。
    // HostConfig监听到该事件,扫描Web部署目录,对于部署描述文件,WAR包,目录会自动创建StandardContext实例,添加到Host并启动
    setState(LifecycleState.STARTING);
    ...

接下来路径为LifecycleBase.setState() -> LifecycleBase.setStateInternal()->LifecycleBase.fireLifecycleEvent(),如下:

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

进入HostConfig.fireLifecycleEvent():
public void lifecycleEvent(LifecycleEvent event) {

     ...
    // 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();
    }
}

3.2 START_EVENT事件

该事件在Host启动时触发,完成服务器启动过程中的Wb应用部署(只有当Host的deployOnStartup)属性为true时,服务器才会在启动过程中部署Web应用,该属性默认为true)。

接着 3.1小节 的代码进行分析,由于是START_EVENT事件,代码执行statrt()方法:

public void start() {
    ...
    // 只有当deployOnStartup为true的时候才会执行   
    if (host.getDeployOnStartup()) {
        deployApps();
    }
}

接下来进入HostConfig.deployApps():

// 在监听到start事件类型,也就是StandardHost调用startInternal
protected void deployApps() {
    // 在server.xml中的Host标签指定appbase的属性为 webapps
    File appBase = host.getAppBaseFile();
    //这个值是在触发before_start时间时生成的,默认是tomcat安装目录+engine名+host名
    File configBase = host.getConfigBaseFile();

    //列出appBase下的所有文件、文件夹,进行过滤
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // Deploy XML descriptors from configBase
    deployDescriptors(configBase, configBase.list());
    // Deploy WARs
    // Deploy WARs 部署war包
    deployWARs(appBase, filteredAppPaths);
    // Deploy expanded folders web目录部署
    deployDirectories(appBase, filteredAppPaths);
}

从前面的时序图和代码可知,该事件处理包含了3部分:Contex描述文件部署Web目录部署WAR包部署,而且这3部分对应于Wb应用的3类不同的部署方式。

3.1.1 Context描述文件部署

Tomcat支持通过一个独立的Context描述文件来配置并启动Web应用,配置方式同server.xml中的<Context>元素。该配置文件的存储路径由Host的xmlBase属性指定。如果未指定,则默认值 为$CATALINA_BASE/conf<Engine:名称>/<Host名称>,因此,对于Tomcat默认的Host,Context 描述文件的路径为$CATALINA_BASE/conf/Catalina/localhost

例如我们在该目录下建立一个文件,名为“myApp.xml”,内容如下:

<Context docBase="test/myApp"path="/myApp"reloadable="false">
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>

与此同时,将目录名为myApp的Web应用复制到test目录下,Tomcat启动时便会自动部署该Web应用,根请求地址为http://127.0.0.1:8080/myApp。 此种方式与在server.xml中的配置相比要灵活得多,而且可以实现相同的部署需求。

HostConfig.deployDescriptors()源码如下:

protected void deployDescriptors(File configBase, String[] files) {

    if (files == null) {
        return;
    }

    //获取线程池
    ExecutorService es = host.getStartStopExecutor();
    List<Future<?>> results = new ArrayList<>();

    for (String file : files) {
        File contextXml = new File(configBase, file);

        if (file.toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
            //context命名,在构造函数里面进行设置,设置版本,path,命名。
            ContextName cn = new ContextName(file, true);

            if (tryAddServiced(cn.getName())) {
                try {
                    //是否已经部署过,如果已经部署过了,就不再进行部署
                    if (deploymentExists(cn.getName())) {
                        removeServiced(cn.getName());
                        continue;
                    }

                    // DeployDescriptor will call removeServiced
                    //异步发布context描述xml
                    results.add(es.submit(new DeployDescriptor(this, cn, contextXml)));
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    removeServiced(cn.getName());
                    throw t;
                }
            }
        }
    }

    for (Future<?> result : results) {
        try {
            //等待异步部署context描述文件结束
            result.get();
        } catch (Exception e) {
            log.error(sm.getString("hostConfig.deployDescriptor.threaded.error"), e);
        }
    }
}

直接看DeployDescriptor的run()方法,进入下面的
protected void deployDescriptor(ContextName cn, File contextXml) {

    //发布应用,用于记录发布的context名,和是否有描述文件,一些可能被修改的文件的修改时间,用于日后检测是否需要重新加载
    DeployedApplication deployedApp = new DeployedApplication(cn.getName(), true);

    long startTime = 0;
    // Assume this is a configuration descriptor and deploy it
    if (log.isInfoEnabled()) {
        startTime = System.currentTimeMillis();
        log.info(sm.getString("hostConfig.deployDescriptor", contextXml.getAbsolutePath()));
    }

    Context context = null;
    //是否为扩展war包
    boolean isExternalWar = false;
    //是否是扩展web 应用
    boolean isExternal = false;
    //记录扩展web应用的地址
    File expandedDocBase = null;

    try (FileInputStream fis = new FileInputStream(contextXml)) {
        synchronized (digesterLock) {
            try {
                //解析contextxml文件
                context = (Context) digester.parse(fis);
            } catch (Exception e) {
                log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), e);
            } finally {
                //释放内存
                digester.reset();
                //如果创建失败,就new出一个失败的context
                if (context == null) {
                    context = new FailedContext();
                }
            }
        }

        if (context.getPath() != null) {
            log.warn(sm.getString("hostConfig.deployDescriptor.path", context.getPath(),
                contextXml.getAbsolutePath()));
        }

        //host.getConfigClass() == org.apache.catalina.startup.ContextConfig
        Class<?> clazz = Class.forName(host.getConfigClass());
        //给context设置ContextConfig生命周期监听器
        LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
        context.addLifecycleListener(listener);

        context.setConfigFile(contextXml.toURI().toURL());
        context.setName(cn.getName());
        //一般我们在config/engine名+host名下的配置文件都是
        context.setPath(cn.getPath());
        context.setWebappVersion(cn.getVersion());
        // Add the associated docBase to the redeployed list if it's a WAR
        if (context.getDocBase() != null) {
            File docBase = new File(context.getDocBase());
            if (!docBase.isAbsolute()) {
                docBase = new File(host.getAppBaseFile(), context.getDocBase());
            }
            // If external docBase, register .xml as redeploy first
            // 如果是扩展的web应用 那么首先进行重发布处理
            if (!docBase.getCanonicalFile().toPath().startsWith(host.getAppBaseFile().toPath())) {
                isExternal = true;
                //设置应用配置xml为重发布资源,记录最后修改的时间
                deployedApp.redeployResources.put(
                    contextXml.getAbsolutePath(), Long.valueOf(contextXml.lastModified()));
                //设置应用目录为重发布资源,记录最后修改的时间
                deployedApp.redeployResources.put(
                    docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified()));
                //如果是war包,设置isExternalWar为true
                if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
                    isExternalWar = true;
                }
                // Check that a WAR or DIR in the appBase is not 'hidden'
                File war = new File(host.getAppBaseFile(), cn.getBaseName() + ".war");
                if (war.exists()) {
                    log.warn(sm.getString("hostConfig.deployDescriptor.hiddenWar",
                        contextXml.getAbsolutePath(), war.getAbsolutePath()));
                }
                File dir = new File(host.getAppBaseFile(), cn.getBaseName());
                if (dir.exists()) {
                    log.warn(sm.getString("hostConfig.deployDescriptor.hiddenDir",
                        contextXml.getAbsolutePath(), dir.getAbsolutePath()));
                }
            } else {
                log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified", docBase));
                // Ignore specified docBase
                context.setDocBase(null);
            }
        }

        // todo 将context添加到对应host容器中,这里会进行context的初始化,启动生命周期
        host.addChild(context);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), t);
    } finally {
        // Get paths for WAR and expanded WAR in appBase

        // default to appBase dir + name
        // default to appBase dir + name 默认是AppBase路径加上容器的继承名称
        expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName());
        //如果应用的docBase不为空,也就是设置了应用的位置,并且不是war包
        if (context.getDocBase() != null && !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
            // first assume docBase is absolute
            // first assume docBase is absolute 重新设置expandedDocBase
            expandedDocBase = new File(context.getDocBase());
            if (!expandedDocBase.isAbsolute()) {
                // if docBase specified and relative, it must be relative to appBase
                //如果是相对路径,都会认为相对appbase
                expandedDocBase = new File(host.getAppBaseFile(), context.getDocBase());
            }
        }

        boolean unpackWAR = unpackWARs;
        if (unpackWAR && context instanceof StandardContext) {
            unpackWAR = ((StandardContext) context).getUnpackWAR();
        }

        // Add the eventual unpacked WAR and all the resources which will be
        // watched inside it
        //如果是war包,并且允许解压,那么把war加入重新部署检测列表
        if (isExternalWar) {
            if (unpackWAR) {
                deployedApp.redeployResources.put(
                    expandedDocBase.getAbsolutePath(), Long.valueOf(expandedDocBase.lastModified()));
                //加入以expandedDocBase为基路径的资源重新加载监控
                //在配置文件中有WatchedResource这样的xml元素,可以配置需要重加载检测文件
                addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
            } else {
                //如果不允许解压,那么就直接用他们的相对路径进行资源修改监控
                addWatchedResources(deployedApp, null, context);
            }
        } else {
            // Find an existing matching war and expanded folder
            //如果不是war包,而又不是扩展目录应用,那么自动加上war后缀
            if (!isExternal) {
                File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war");
                if (warDocBase.exists()) {
                    deployedApp.redeployResources.put(
                        warDocBase.getAbsolutePath(), Long.valueOf(warDocBase.lastModified()));
                } else {
                    // Trigger a redeploy if a WAR is added
                    // 如果这个war后面被添加进来了,那么就触发重新加载
                    deployedApp.redeployResources.put(warDocBase.getAbsolutePath(), Long.valueOf(0));
                }
            }
            //这段代码和上面的war时的代码是一样的
            if (unpackWAR) {
                deployedApp.redeployResources.put(
                    expandedDocBase.getAbsolutePath(), Long.valueOf(expandedDocBase.lastModified()));
                addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
            } else {
                addWatchedResources(deployedApp, null, context);
            }
            if (!isExternal) {
                // For external docBases, the context.xml will have been
                // added above.
                deployedApp.redeployResources.put(
                    contextXml.getAbsolutePath(), Long.valueOf(contextXml.lastModified()));
            }
        }
        // Add the global redeploy resources (which are never deleted) at
        // the end so they don't interfere with the deletion process
        //添加全局重新部署资源,如conf/engine名称+host名/context.xml.default和conf/context.xml
        addGlobalRedeployResources(deployedApp);
    }

    //如果这个web应用已经成功添加到host中,那么记录这个应用已经被发布
    if (host.findChild(context.getName()) != null) {
        deployed.put(context.getName(), deployedApp);
    }

    if (log.isInfoEnabled()) {
        log.info(sm.getString("hostConfig.deployDescriptor.finished",
            contextXml.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
    }
}

Context描述文件的部署过程步骤如下:

  1. 扫描Host配置文件基础目录,即$CATALINA BASE/conf<Engine名称>/<Host名称>,对于该目录下的每个配置文件,由线程池完成解析部署。
  2. 对于每个文件的部署线程,进行如下操作。
    • 使用Digester解析配置文件,创建Context实例。
    • 更新Context实例的名称、路径(不考虑webapp Version的情况下,使用文件名),因此 <Context)元素中配置的path属性无效。
    • 为Context添加ContextConfig生命周期监听器
    • 通过Host的addChild()方法将Context实例添加到Host该方法会判断Host是否已启动,如果是,则直接启动Context
    • 将Context描述文件、Web应用目录及web.xml等添加到守护资源,以便文件发生变更时(使用资源文件的上次修改时间进行判断),重新部署或者加载Wb应用。

即便要对Web应用单独指定目录管理或者对Context的创建进行定制,我们也建议采用该方案或者随后讲到的配置文件备份的方案,而非直接在server.xml文件中配置。它们功能相同,但是前两者灵活性要高得多,而且对服务器的侵入要小。

我们在详细分析一下,Context描述文件部署加载Context类是怎么启动的?调用链为 StandardHost.addChild()-> Container.addChild()->addChildInternal(child):

private void addChildInternal(Container child) {

    ...
    synchronized(children) {
        if (children.get(child.getName()) != null) {
            throw new IllegalArgumentException(
                    sm.getString("containerBase.child.notUnique", child.getName()));
        }
        // todo
        child.setParent(this);  // May throw IAE 给子容器设置父容器,并且触发属性更改事件
        children.put(child.getName(), child); //名字做key,进行保存到map中
    }

    fireContainerEvent(ADD_CHILD_EVENT, child);

    // Start child
    // Don't do this inside sync block - start can be a slow process and
    // locking the children object can cause problems elsewhere
    try {
        // 在解析conf/server.xml时不会调用child.start()方法,因为此时child的state为NEW,
        // 并且available=false
        if ((getState().isAvailable() ||
                LifecycleState.STARTING_PREP.equals(getState())) &&
                startChildren) {
            // todo 启动context容器
            child.start();
        }
    } catch (LifecycleException e) {
        throw new IllegalStateException(sm.getString("containerBase.child.start"), e);
    }
}

child.start() 也就是启动Context。

3.1.2 Web目录部署

以目录的形式发布并部署Web应用是Tomcat中最常见的部署方式。我们只需要将包含Web应用所有资源文件(JavaScript、CSS、图片、JSP等)、Jar包、描述文件(WEB-NF/web.xml)的目录复制到Host指定appBase目录下即可完成部署。

注意:此时Host的deployIgnore,属性可以将符合某个正则表达式的Web应用目录忽略而不进行部署。如果不指定,则所有目录均进行部署。

此种部署方式下,Catalina同样支持通过配置文件来实例化Context(默认位于Web应用的 META-NF目录下,文件名为context.xml)。我们仍可以在配置文件中对Context进行定制,但是无 法覆盖name、path、webappVersion、docBasej这4个属性,这些均由Web目录的路径及名称确定(因 此,此种方式无法自定义Web应用的部署目录)。

我们直接看HostConfig.deployDirectories()如下:

protected void deployDirectories(File appBase, String[] files) {

    if (files == null) {
        return;
    }

    ExecutorService es = host.getStartStopExecutor();
    List<Future<?>> results = new ArrayList<>();

    for (String file : files) {
        if (file.equalsIgnoreCase("META-INF")) {
            continue;
        }
        if (file.equalsIgnoreCase("WEB-INF")) {
            continue;
        }

        File dir = new File(appBase, file);
        if (dir.isDirectory()) {
            ContextName cn = new ContextName(file, false);

            if (tryAddServiced(cn.getName())) {
                try {
                    if (deploymentExists(cn.getName())) {
                        removeServiced(cn.getName());
                        continue;
                    }

                    // DeployDirectory will call removeServiced
                    results.add(es.submit(new DeployDirectory(this, cn, dir)));
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    removeServiced(cn.getName());
                    throw t;
                }
            }
        }
    }

    for (Future<?> result : results) {
        try {
            result.get();
        } catch (Exception e) {
            log.error(sm.getString("hostConfig.deployDir.threaded.error"), e);
        }
    }
}

直接看 DeployDirectory.run()
protected void deployDirectory(ContextName cn, File dir) {

    long startTime = 0;
    // Deploy the application in this directory
    if( log.isInfoEnabled() ) {
        startTime = System.currentTimeMillis();
        log.info(sm.getString("hostConfig.deployDir", dir.getAbsolutePath()));
    }

    Context context = null;
    File xml = new File(dir, Constants.ApplicationContextXml);
    //当允许copyxml的时候会使用到
    File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");

    DeployedApplication deployedApp;
    boolean copyThisXml = isCopyXML();
    boolean deployThisXML = isDeployThisXML(dir, cn);

    try {
        if (deployThisXML && xml.exists()) {
            synchronized (digesterLock) {
                try {
                    context = (Context) digester.parse(xml);
                } catch (Exception e) {
                    log.error(sm.getString("hostConfig.deployDescriptor.error", xml), e);
                    context = new FailedContext();
                } finally {
                    digester.reset();
                    if (context == null) {
                        context = new FailedContext();
                    }
                }
            }

            //这里有点疑问,为什么host配置的copyThisXml为false的时候才允许context进行覆盖?
            if (copyThisXml == false && context instanceof StandardContext) {
                // Host is using default value. Context may override it.
                copyThisXml = ((StandardContext) context).getCopyXML();
            }

            //当允许copy时,进行复制,并把配置文件设置为新的目录
            if (copyThisXml) {
                Files.copy(xml.toPath(), xmlCopy.toPath());
                context.setConfigFile(xmlCopy.toURI().toURL());
            } else {
                context.setConfigFile(xml.toURI().toURL());
            }
            //如果不允许发布这个配置文件并且对应的xml存在的话,构造失败context,why???即使存在context.xml也可以构造一个默认的context
        } else if (!deployThisXML && xml.exists()) {
            // Block deployment as META-INF/context.xml may contain security
            // configuration necessary for a secure deployment.
            log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), xml, xmlCopy));
            context = new FailedContext();
        } else {
            // 创建StandardContext的实例
            context = (Context) Class.forName(contextClass).getConstructor().newInstance();
        }

        //下面的步骤和部署描述文件和部署war是一样的。不再赘述
        Class<?> clazz = Class.forName(host.getConfigClass());
        LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
        context.addLifecycleListener(listener);

        context.setName(cn.getName());
        context.setPath(cn.getPath());
        context.setWebappVersion(cn.getVersion());
        context.setDocBase(cn.getBaseName());
        host.addChild(context);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("hostConfig.deployDir.error", dir.getAbsolutePath()), t);
    } finally {
        deployedApp = new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml);

        // Fake re-deploy resource to detect if a WAR is added at a later
        // point
        deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war", Long.valueOf(0));
        deployedApp.redeployResources.put(dir.getAbsolutePath(), Long.valueOf(dir.lastModified()));
        if (deployThisXML && xml.exists()) {
            if (copyThisXml) {
                deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(xmlCopy.lastModified()));
            } else {
                deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified()));
                // Fake re-deploy resource to detect if a context.xml file is
                // added at a later point
                deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(0));
            }
        } else {
            // Fake re-deploy resource to detect if a context.xml file is
            // added at a later point
            deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(0));
            if (!xml.exists()) {
                deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(0));
            }
        }
        addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
        // Add the global redeploy resources (which are never deleted) at
        // the end so they don't interfere with the deletion process
        addGlobalRedeployResources(deployedApp);
    }

    deployed.put(cn.getName(), deployedApp);

    if( log.isInfoEnabled() ) {
        log.info(sm.getString("hostConfig.deployDir.finished",
            dir.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
    }
}

Catalina部署Web应用目录的过程如下:

  1. 对于Host的appBase目录(默认为$CATALINA_BASE/webapps)下所有符合条件的目录(不符合deployIgnorel的过滤规则、目录名不为META-INF和WEB-INF),由线程池完成部署

  2. 对于每个目录进行如下操作。

    • 如果Host的deployXML属性值为true(即通过Context描述文件部署),并且存在META-INF/context.xml文件,则使用Digesterf解析context.xml文件创建Context对象。如果Context的copyXML属性为true,则将描述文件复制到$CATALINA_BASE/conf<Engine名称>/<Host名称>目录下,文件名与Web应用目录名相同。
      • 如果deployXML属性值为false,但是存在META-INF/context.xml文件,则构造FailedContext实例(Catalina的空模式,用于表示Context部署失败)
      • 其他情况下,根据Host的contextClass属性指定的类型创建Context对象。如不指定,则为org,apache.catalina.core.StandardContext。此时,所有的Context属性均采用默认配置,除name、path、webappVersion、docBase会根据Web应用目录的路径及名称进行设置外。
    • 为Context实例添加ContextConfig生命周期监听器。
    • 通过Host的addChild()方法将Context实例添加到Host。该方法会判断Host是否已启动,如果是,则直接启动Context。
    • 将Context描述文件、Web应用目录及web.xml等添加到守护资源,以便文件发生变更时重新部署或者加载Web应用。守护文件因deployXML和copyXML的配置稍有不同。

通过Host的addChild()方法将Context实例添加到Host。该方法会判断Host是否已启动,如果是,则直接启动Context。分析方法如上,不在赘述。

3.1.3 WAR包部署

WAR包部署和Web目录部署基本类似,只是由于WAR包作为一个压缩文件增加了部分针 对压缩文件的处理

路径为HostConfig.deployWARs()->DeployWar.run()->HostConfig.deployWAR()源码分析方式如上,不在赘述,详见tomcat-9.0.60-src源码解析 

其具体的部署过程如下:

  1. 对于Host的appBase目录(默认为SCATALINA BASE/webapps)下所有符合条件的WAR包(不符合deployIgnoref的过滤规则、文件名不为META-INF和WEB-NF、以war作为扩展名的文件),由线程池完成部署。

  2. 对于每个WAR包进行如下操作

    • 如果Host的deployXML属性为true,且在WAR包同名目录(去除扩展名)下存在META-INF/context.xml文件,同时Context的copyXML属性为false,则使用该描述文件创建Context实例(用于WAR包解压目录位于部署目录的情况)。
      • 如果Host的deployXML属性为true,且在WAR包压缩文件下存在META-INF/context..xml文件,则使用该描述文件创建Context对象。
      • 如果deployXML属性值为false,但是在WAR包压缩文件下存在META-INF/context.xml文件,则构造FailedContext实例(Catalina的空模式,用于表示Context部署失败)。
      • 其他情况下,根据Host的contextClass,属性指定的类型创建Context对象。如不指定,则为org.apache.catalina.core.StandardContext。此时,所有的Context)属性均采用默认配置,除name、path、webappVersion、docBase会根据WAR包的路径及名称进行设置外。
    • 如果deployXML为true,且META-NF/context.xml存在于WAR包中,同时Context的copyXML属性为true,则将context.xml文件复制到$CATALINA BASE/conf<Engine名称>/<Host名称>目录下,文件名称同WAR包名称(去除扩展名)。
    • 为Context实例添加ContextConfig生命周期监听器。
    • 通过Host的addChild()方法将Context实例添加到Host。该方法会判断Host是否已启动,如果是,则直接启动Context。
    • 将Context描述文件、WAR包及web.xml等添加到守护资源,以便文件发生变更时重新部署或者加载Web应用。

3.2 PERIODIC_EVENT事件

如前所述,Catalina的容器支持定期执行自身及其子容器的后台处理过程(该机制位于所有容器的父类ContainerBase中,默认情况下由Engine维护后台任务处理线程)。具体处理过程在容器的backgroundProcess()方法中定义。该机制常用于定时扫描Web应用的变更,并进行重新加载。后台任务处理完成后,将触发PERIODIC EVENT事件。这个看一下源码: StandardHost.startInternal()->ContainerBase.startInternal()

...
// Start our thread
// 启动层级的后台任务处理,包括Cluster后台任务处理(包括部署变更检测,心跳),Realm后台任务处理,
// Pipeline中Value的后台任务处理
if (backgroundProcessorDelay > 0) {
    monitorFuture = Container.getService(ContainerBase.this).getServer()
            .getUtilityExecutor().scheduleWithFixedDelay(
                    new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
}

接下来ContainerBackgroundProcessorMonitor.run()->ContainerBase.threadStart()->ContainerBackgroundProcessor.run()->processChildren()->container.backgroundProcess()->ContainerBase.backgroundProcess():

public void backgroundProcess() {
    ...
    // 设置PERIODIC_EVENT事件,重新加载
    fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}

在HostConfig中通过DeployedApplication维护了两个守护资源列表:redeployResources和 reloadResources,前者用于守护导致应用重新部署的资源后者守护导致应用重新加载的资源。两个列表分别维护了资源及其最后修改时间。

当HostConfig接收到PERIODIC_EVENT事件后,会检测守护资源的变更情况。如果发生变更,将重新加载或者部署应用以及更新资源的最后修改时间

注意:重新加载和重新部署的区别在于,前者是针对同一个Context对象的重启,而后者是重新创建了一个Context对象。Catalina中,同时守护两类资源以区别是重新加载应用还是重新部署应用。如Context描述文件变更时,需要重新部署应用;而web.xml文件变更时,则只需要重新加载Context即可

HostConfig.lifecycleEvent()源码如下:

public void lifecycleEvent(LifecycleEvent event) {

    ...
    // 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();
    }
}
check()方法:
protected void check() {

    // 开启自动部署
    if (host.getAutoDeploy()) {
        // Check for resources modification to trigger redeployment
        // 每一个已部署的应用
        DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]);
        for (DeployedApplication app : apps) {
            if (tryAddServiced(app.name)) {
                try {
                    // 检验资源(需要重新部署和重新加载的资源)
                    checkResources(app, false);
                } finally {
                    removeServiced(app.name);
                }
            }
        }

        // Check for old versions of applications that can now be undeployed
        if (host.getUndeployOldVersions()) {
            checkUndeploy();
        }

        // Hotdeploy applications
        // 热部署应用
        deployApps();
    }
}
checkResources()方法太长,不在展示,请看github详细注释

其具体的部署过程如下(只有当Host的autoDeploy属性为true时处理):

  1. 对于每一个已部署的Web应用(不包含在serviced列表中,Serviced列表的具体作用参见下面的“注意”),检查用于重新部署的守护资源。对于每一个守护的资源文件或者目录,如果发生变更,那么就有以下几种情况。

    • 如果资源对应为目录,则仅更新守护资源列表中的上次修改时间
    • 如果Web应用存在Context描述文件并且当前变更的是WAR包文件,则得到原Context的docBase。如果docBase不以“.war”结尾(即Context指向的是WAR解压目录,删除解压目录并重新加载,否则直接重新加载。更新守护资源。
    • 其他情况下,直接御载应用,并由接下来的处理步骤重新部署。
  2. 对于每个已部署的wb应用,检查用于重新加载的守护资源,如果资源发生变更,则重新加载Context对象。

  3. 如果Host配置为卸载I旧版本应用(undeployoldVersions,属性为true),则检查并卸载。

  4. 部署Web应用(新增以及处于卸载状态的描述文件、Wb应用目录、WAR包),部署过程同上面叙述。

注意: HostConfig的serviced,属性维护了一个Web应用列表,该列表会由Tomcat的管理程序通过MBean进行配置。当Tomcat修改某个Web应用(如重新部署)时,会先通过同步的addServiced()将其添加到serviced列表,并且在操作完毕后,通过同步的removeServiced()方法将其移除。通过此方式,避免后台定时任务与Tomcat管理工具的冲突。因此,在部署HostConfig中的描述文件、Web应用目录、WAR包时,均需要确认serviced列表中不存在同名应用。

3.3 总结

回顾上述Web应用的部署方式,无论是Context描述文件,还是Web目录以及WAR包,归结起来,Catalina支持Web应用以文件目录或者WAR包的形式发布;同时,如果希望定制Context,那 么可以通过$CATALINA_BASE/conf<Engine名称>/<Host名称>目录下的描述文件或者Web应用 的META-INF/context.xml来进行自定义

因此,从这个角度来看,基本可以将Catalina的Web应用部署分为目录和WAR包两类,每一类进一步支持Context的定制化。而默认情况下,Catalina会根据发布包的路径及名称自动创建一个Context对象。

参考文章

tomcat-9.0.60-src源码解析 
Tomcat架构解析
Context创建过程解析(一)之deployDescriptors
Context创建过程解析(二)之deployWARs
Context创建过程解析(三)之deployDirectories
Tomcat源码阅读之StandardHost与HostConfig的分析