上一节我们研究了部分组件的启动,在host启动的时候,HostConfig监听了Host容器的启动事件。

Host的start事件

 public void org.apache.catalina.startup.HostConfig.start() {

        if (log.isDebugEnabled())
            log.debug(sm.getString("hostConfig.start"));

        try {
            //获取host的ObjectName,这个ObjectName是用于mbean的对象名
            ObjectName hostON = host.getObjectName();
            oname = new ObjectName
                (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
            //注册到mbean
            Registry.getRegistry(null, null).registerComponent
                (this, oname, this.getClass().getName());
        } catch (Exception e) {
            log.error(sm.getString("hostConfig.jmx.register", oname), e);
        }
        //如果指定的appBase不是一个目录,那么打印错误日志,并设置自动部署为false,
        //启动即部署为false
        if (!host.getAppBaseFile().isDirectory()) {
            log.error(sm.getString("hostConfig.appBase", host.getName(),
                    host.getAppBaseFile().getPath()));
            host.setDeployOnStartup(false);
            host.setAutoDeploy(false);
        }
        //是否在启动时就发布,默认为true
        if (host.getDeployOnStartup())
            //发布应用
            deployApps();

    }

发布应用

protected void org.apache.catalina.startup.HostConfig.deployApps() {
        //获取appBase,如果你的tomcat安装在D:\tomcat,并且appBasee配置为webapps,那么这个地址就是D:\tomcat\webapps
        File appBase = host.getAppBaseFile();
        //当前host的基本配置文件路径,一般就是D:\tomcat\conf\Catalina\host名(如果是本机,那么就是localhost)
        File configBase = host.getConfigBaseFile();
        //过滤掉不需要进行自动部署的文件目录
        //(*1*)
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        //从host的基本配置路径发布xml描述
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        //从应用基本路径发布war包
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        //发布扩展路径下的应用
        deployDirectories(appBase, filteredAppPaths);

    }
    
    //(*1*)
    protected String[] org.apache.catalina.startup.HostConfig.filterAppPaths(String[] unfilteredAppPaths) {
        //获取过滤的正则表达式
        Pattern filter = host.getDeployIgnorePattern();
        //如果没有设置那么就直接返回,可以在配置文件中指定deployIgnorePattern属性
        if (filter == null || unfilteredAppPaths == null) {
            return unfilteredAppPaths;
        }

        List<String> filteredList = new ArrayList<>();
        Matcher matcher = null;
        for (String appPath : unfilteredAppPaths) {
            if (matcher == null) {
                matcher = filter.matcher(appPath);
            } else {
                //重置匹配
                matcher.reset(appPath);
            }
            if (matcher.matches()) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("hostConfig.ignorePath", appPath));
                }
            } else {
                //记录不需要过滤的
                filteredList.add(appPath);
            }
        }
        return filteredList.toArray(new String[filteredList.size()]);
    }
    

从上面的代码中可以看到tomcat发布应用分成了三种情况,下面我们也分三步走

1、Deploy XML descriptors from configBase

第一步:发布基本配置路径的context的xml描述

 protected void org.apache.catalina.startup.HostConfig.deployDescriptors(File configBase, String[] files) {
        //假设路径:tomcat的安装目录/conf/catalina/localhost/context.xml
        if (files == null)
            return;
        //获取host容器的线程池
        ExecutorService es = host.getStartStopExecutor();//获取线程池
        List<Future<?>> results = new ArrayList<>();

        for (int i = 0; i < files.length; i++) {
            File contextXml = new File(configBase, files[i]);
            //只处理后缀为xml的xml文件
            if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
                //包装成ContextName对象,用于维护tomcat的名字,版本
                //(*1*)
                ContextName cn = new ContextName(files[i], true);
                //当前context是否已经被发布过了
                if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
                    continue;
                //使用线程池进行发布
                results.add(
                        es.submit(new DeployDescriptor(this, cn, contextXml)));
            }
        }

        for (Future<?> result : results) {
            try {
                //阻塞,等待子线程处理完毕
                result.get();
            } catch (Exception e) {
                log.error(sm.getString(
                        "hostConfig.deployDescriptor.threaded.error"), e);
            }
        }
    }
    
    //(*1*)
    public ContextName(String name, boolean stripFileExtension) {
        //对于configBase路径下的context名默认使用context配置文件的名字
        //比如Tomcat/Context.xml
        String tmp1 = name;

        // Convert Context names and display names to base names

        // Strip off any leading "/"
        if (tmp1.startsWith("/")) {
            //去掉开头的/
            tmp1 = tmp1.substring(1);
        }
        
        // Replace any remaining /
        //替换调用余下/,此时tmp1 = Tomcat#Context.xml
        tmp1 = tmp1.replaceAll("/", FWD_SLASH_REPLACEMENT);

        // Insert the ROOT name if required
        //如果是以##开头,或者已经没有名字了,那么tmp1就会标称ROOT+tmp的形式
        //假设tmp1的值为空字符,那么tmp1就会变成ROOT,如果tmp1原来是##1.5,那么就变成了ROOT##1.5
        if (tmp1.startsWith(VERSION_MARKER) || "".equals(tmp1)) {
            tmp1 = ROOT_NAME + tmp1;
        }

        // Remove any file extensions
        //是否是严格的文件后缀,如果是,那么截取掉后缀,比如tomcatContext.xml或者是tomcatContext.war
        //那么就会变成tomcatContext
        if (stripFileExtension &&
                (tmp1.toLowerCase(Locale.ENGLISH).endsWith(".war") ||
                        tmp1.toLowerCase(Locale.ENGLISH).endsWith(".xml"))) {
            tmp1 = tmp1.substring(0, tmp1.length() -4);
        }
        //设置为基础名
        baseName = tmp1;
        
        String tmp2;
        // Extract version number
        //是否存在##字符串
        int versionIndex = baseName.indexOf(VERSION_MARKER);
        if (versionIndex > -1) {
            //如果存在##,那么##后面的字符串作为版本号,比如ROOT##1.5,那么版本号就为1.5
            version = baseName.substring(versionIndex + 2);
            //如果baseName = ROOT##1.5,那么tmp2就变成了ROOT
            tmp2 = baseName.substring(0, versionIndex);
        } else {
            version = "";
            tmp2 = baseName;
        }
        //如果tmp2是ROOT(表示根路径)
        //那么应用路径设置为空,也就是我们常在项目中配置/
        if (ROOT_NAME.equals(tmp2)) {
            path = "";
        } else {
            //如果不是根路径,那么将#替换成/作为项目路径
            path = "/" + tmp2.replaceAll(FWD_SLASH_REPLACEMENT, "/");
        }
        //设置context名字
        ///比如context##1.5
        if (versionIndex > -1) {
            this.name = path + VERSION_MARKER + version;
        } else {
            this.name = path;
        }
    }
    

上面有段设置context名字,路径,版本的代码,可以分为以下几种情况

  • 假设传入的contextName是context.xml,那么最终的basename=context,version="",path=/context,name=/context
  • /tomcat/context.war 那么最终的basename=tomcat#context,version="",path=/tomcat/context,name=/tomcat/context
  • ##tomcat/context.xml 那么最终的basename=ROOT##tomcat#context,version=tomcat#context,path="",name=##tomcat#context
  • / 那么最终的basename=ROOT,version="",path="",name=""
  • tomcat#context##1.5.xml 那么最终的basename=tomcat#context##1.5,version=1.5,path=/tomcat/context,name=/tomcat/context##1.5
    -ROOT##1.5 那么最终的basename=ROOT##1.5,version=1.5,path="",name=##1.5

可以认为baseName是tomcat对web应用目录的规范(后面你会看到tomcat对war还有拷贝的context.xml文件的路径名就是baseName),version表示可以用于表示相同的path下的不同版本,name一看就是path与版本通过##连接的字符串,这个name在tomcat中是唯一的,如果出现相同的name的context,tomcat就不会加载,并打印错误日志告诉你哪一个context发生了冲突。

发布描述

//这是HostConfig的一个内部类
private static class DeployDescriptor implements Runnable {
        
        private HostConfig config;
        private ContextName cn;
        private File descriptor;

        public DeployDescriptor(HostConfig config, ContextName cn,
                File descriptor) {
            this.config = config;
            this.cn = cn;
            this.descriptor= descriptor;
        }

        @Override
        public void run() {
            //调用了HostConfig的发布描述方法
            config.deployDescriptor(cn, descriptor);
        }
    }
protected void org.apache.catalina.startup.HostConfig.deployDescriptor(ContextName cn, File contextXml) {
    	//发布应用,用于记录发布的context名,和是否有描述文件,一些可能被修改的文件的修改时间,用于日后检测是否需要重新加载
    	//第一个参数表示这个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;
        //是否外部的context war包
        boolean isExternalWar = false;
        //是否为外部context
        boolean isExternal = false;
        File expandedDocBase = null;
        //JDK7新增的语法糖,用于自动释放资源
        try (FileInputStream fis = new FileInputStream(contextXml)) {
            synchronized (digesterLock) {
                try {
                    //解析,并返回根对象Context,这个Digester在第二小节的时候进行分析,这是一个sax的xml处理器
                    //这个处理器在创建HostConfig的时候就随着构造器一起构造,内部自动添加了Context标签的规则,用于
                    //创建StandardContext对象,已经设置属性规则
                    context = (Context) digester.parse(fis);
                } catch (Exception e) {
                    log.error(sm.getString(
                            "hostConfig.deployDescriptor.error",
                            contextXml.getAbsolutePath()), e);
                } finally {
                    digester.reset();
                    if (context == null) {
                        context = new FailedContext();
                    }
                }
            }
            //获取配置的类对象,这个配置对象默认就是ContextConfig
            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
            //将监听器添加到这个创建出来的context容器中
            context.addLifecycleListener(listener);
            //设置context配置文件的url路径
            context.setConfigFile(contextXml.toURI().toURL());
            //设置它的名字,请求路径,版本
            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            // Add the associated docBase to the redeployed list if it's a WAR
            //是否设置docBase,docBase就是某个web应用的所在目录,就像是host指定的webapps目录下某个具体的文件夹
            if (context.getDocBase() != null) {
                File docBase = new File(context.getDocBase());
                //如果这个目录不是绝对路径,那么就把host指定的appBase路径拼接到这个docBase的前面
                if (!docBase.isAbsolute()) {
                    docBase = new File(host.getAppBaseFile(), context.getDocBase());
                }
                // If external docBase, register .xml as redeploy first
                //如果这个docBase不是以host指定的appBase作为开头的,那么认为这个docBase为外部的路径
                //对应的应用为外部应用
                if (!docBase.getCanonicalPath().startsWith(
                        host.getAppBaseFile().getAbsolutePath() + File.separator)) {
                    isExternal = true;
                    //将当前context的配置描述文件添加到重新部署资源map中
                    //资源绝对路径-》资源最后修改时间,在发生更改的情况下可以进行重新部署
                    deployedApp.redeployResources.put(
                            contextXml.getAbsolutePath(),
                            Long.valueOf(contextXml.lastModified()));
                    //将docBase目录记录为可重新发布资源
                    deployedApp.redeployResources.put(docBase.getAbsolutePath(),
                            Long.valueOf(docBase.lastModified()));
                    //要是这个docBase指定是一个context的war包,那么把isExternalWar置为true
                    if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
                        isExternalWar = true;
                    }
                } else {
                    log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified",
                             docBase));
                    // Ignore specified docBase
                    //如果这个docBase就是Host下的web应用,即使context描述文件中指定了docBase也不会设置docBase,因为这个Context将会
                    //在appBase中发布
                    context.setDocBase(null);
                }
            }
            //添加子容器
            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
            //这里之所以要进行扩展路径的监控,是因为tomcat可以配置把外部包解压到appBase路径下
            // default to appBase dir + name
            //主动设置扩展的docBase,假设我们简单点,我们在host基本配置路径下放了一个叫做TomcatContext.xml的context描述文件
            //并且appBase设置的目录为D:/tomcat/webapps,那么这个延伸的路径为D:/tomcat/webapps/TomcatContext
            expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName());
            //当前context不为空,也就是目录为外部目录(不在host指定的appBase之下的路径),并且不是war包
            if (context.getDocBase() != null
                    && !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
                // first assume docBase is absolute
                //那么使用配置文件指定的docBase覆盖扩展路径
                //因为对于这种不是war包的外部web项目,tomcat不会复制其内容到wabAppBase目录中
                expandedDocBase = new File(context.getDocBase());
                //如果不是绝对路径,那么用host的appBase路径进行拼接
                if (!expandedDocBase.isAbsolute()) {
                    // if docBase specified and relative, it must be relative to appBase
                    expandedDocBase = new File(host.getAppBaseFile(), context.getDocBase());
                }
            }
            //是否解压war包,默认是false,但是通常我们在server.xml中会配置为true
            boolean unpackWAR = unpackWARs;
            if (unpackWAR && context instanceof StandardContext) {
                //使用context的配置覆盖host的配置
                unpackWAR = ((StandardContext) context).getUnpackWAR();
            }

            // Add the eventual unpacked WAR and all the resources which will be
            // watched inside it
            //是否是外部的war包
            if (isExternalWar) {
                //是否进行解压
                if (unpackWAR) {
                    //监控扩展的路径(通常就是解压后的路径)
                    deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
                            Long.valueOf(expandedDocBase.lastModified()));
                    //添加需要监控的资源到deployedApp.redeployResources中,包括context标签中添加的watchedResource
                    addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
                } else {
                    //如果不允许发布时解压,那么只监控context标签中指定的路径的资源
                    //因为文件不解压,tomcat无法监控到war内部文件的变动,不过也没必要,只要监控了
                    //这个war包是否改动即可
                    addWatchedResources(deployedApp, null, context);
                }
            } else {
                // Find an existing matching war and expanded folder
                //如果不是外部的war也不是外部的web应用
                if (!isExternal) {
                    //那么添加war后缀
                    File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war");
                    //如果存在,监控这个war包
                    if (warDocBase.exists()) {
                        deployedApp.redeployResources.put(warDocBase.getAbsolutePath(),
                                Long.valueOf(warDocBase.lastModified()));
                    } else {
                        // Trigger a redeploy if a WAR is added
                        //如果不存在,也会添加这个路径的资源监控(因为有可能日后会将对应的war添加到这个目录),但是上次修改时间为0(不存在,当然是零)
                        deployedApp.redeployResources.put(
                                warDocBase.getAbsolutePath(),
                                Long.valueOf(0));
                    }
                }
                
                //这个操作和上面那一段是一样的
                if (unpackWAR) {
                    //监控扩展的路径
                    deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
                            Long.valueOf(expandedDocBase.lastModified()));
                    addWatchedResources(deployedApp,
                            expandedDocBase.getAbsolutePath(), context);
                } else {
                    addWatchedResources(deployedApp, null, context);
                }
                //如果不是外部web应用,补充监控context描述文件,对于放在appBase目录下的web应用,其docBase由tomcat根据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
            //添加全局的监控资源,configBase下的context.xml.default和conf下的Context.xml context描述文件
            addGlobalRedeployResources(deployedApp);
        }
        //如果不存在,那么记录当前context已被发布
        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资源加入监控,对于外部的资源,tomcat会对这个外部资源的context,docBase进行监控,同时,如果这个外部资源被配置为解压后/压缩后的目录在appBase下面,那么同样要进行监控。那这里就有个问题了,如果我指定了一个外部的war包,并且指定了器解压后放入appBase中,看上面的代码压根就没有进行解压啊,那finally块的解压目录的监控地址的上次修改时间就是零,tomcat肯定是进行了解压才这么做的,那么解压的逻辑放到哪了呢?没错,就是给host添加子容器的时候,会启动StandardContext,启动时会对docBase进行一次fix,如果是war包,那么就会进行解压

public void org.apache.catalina.core.StandardHost.addChild(Container child) {
        //添加内存泄露生命周期监听器
        child.addLifecycleListener(new MemoryLeakTrackingListener());
        
        if (!(child instanceof Context))
            throw new IllegalArgumentException
                (sm.getString("standardHost.notContext"));
        super.addChild(child);

    }
    
     public void org.apache.catalina.core.ContainerBase.addChild(Container child) {
        //是否开启了JDK的安全验证,如果开启了,那么需要通过方位控制器处理接下来的逻辑,
        //否则将可能被安全拦截
        if (Globals.IS_SECURITY_ENABLED) {
            PrivilegedAction<Void> dp =
                new PrivilegedAddChild(child);
            AccessController.doPrivileged(dp);
        } else {
            addChildInternal(child);
        }
    }
    
    
    private void org.apache.catalina.core.ContainerBase.addChildInternal(Container child) {

        if( log.isDebugEnabled() )
            log.debug("Add child " + child + " " + this);
        synchronized(children) {
        
            //如果已经存在相同名字的context,那么抛错
            if (children.get(child.getName()) != null)
                throw new IllegalArgumentException("addChild:  Child name '" +
                                                   child.getName() +
                                                      "' is not unique");
            //给子容器设置父容器
            child.setParent(this);  // May throw IAE
            //记录到父容器的集合中
            children.put(child.getName(), 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 {
            //调用context的start方法
            if ((getState().isAvailable() ||
                    LifecycleState.STARTING_PREP.equals(getState())) &&
                    startChildren) {
                child.start();
            }
        } catch (LifecycleException e) {
            log.error("ContainerBase.addChild: start: ", e);
            throw new IllegalStateException("ContainerBase.addChild: start: " + e);
        } finally {
            //触发子容器添加事件
            fireContainerEvent(ADD_CHILD_EVENT, child);
        }
    }

描述文件的发布就分析到这里,容器的启动非常复发,将用一小节来分析context的启动过程。

2、Deploy WARs

接下来进行第二步,host基础应用路径下的war包

protected void deployWARs(File appBase, String[] files) {
        //appBase下的文件
        if (files == null)
            return;

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

        for (int i = 0; i < files.length; i++) {
            //跳过META-INF文件夹
            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            //跳过WEB-INF文件夹
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            //获取war包
            File war = new File(appBase, files[i]);
            if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
                    war.isFile() && !invalidWars.contains(files[i]) ) {
                //不同于发布xml描述,这里采用的context名字是通过文件名进行解析
                ContextName cn = new ContextName(files[i], true);
                //如果已经被用于提供服务了,那么跳过
                if (isServiced(cn.getName())) {
                    continue;
                }
                //是否已经发布
                if (deploymentExists(cn.getName())) {
                    //获取旧得发布应用对象
                    DeployedApplication app = deployed.get(cn.getName());
                    boolean unpackWAR = unpackWARs;
                    //子容器覆盖父容器的unpackWAR属性
                    if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
                        unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
                    }
                    //如果允许解压的,直接跳过,说明已经解压过了,不允许解压,并且存在旧的发布应用
                    //目录不存的话,也是跳过,存在就打个日志告诉你在某个目录下存在冲突的context
                    if (!unpackWAR && app != null) {
                        // Need to check for a directory that should not be
                        // there
                        File dir = new File(appBase, cn.getBaseName());
                        if (dir.exists()) {
                            if (!app.loggedDirWarning) {
                                log.warn(sm.getString(
                                        "hostConfig.deployWar.hiddenDir",
                                        dir.getAbsoluteFile(),
                                        war.getAbsoluteFile()));
                                app.loggedDirWarning = true;
                            }
                        } else {
                            app.loggedDirWarning = false;
                        }
                    }
                    continue;
                }

                // Check for WARs with /../ /./ or similar sequences in the name
                //检查路径是否存在../和./这种字符串,如果存在,那么视为无效的war包
                if (!validateContextPath(appBase, cn.getBaseName())) {
                    log.error(sm.getString(
                            "hostConfig.illegalWarName", files[i]));
                    invalidWars.add(files[i]);
                    continue;
                }
                //发布war包
                results.add(es.submit(new DeployWar(this, cn, war)));
            }
        }

        for (Future<?> result : results) {
            try {
                //阻塞等待
                result.get();
            } catch (Exception e) {
                log.error(sm.getString(
                        "hostConfig.deployWar.threaded.error"), e);
            }
        }
    }
protected void deployWAR(ContextName cn, File war) {
        //设置解压后context的context.xml的路径META-INF/context.xml
        File xml = new File(host.getAppBaseFile(),
                cn.getBaseName() + "/" + Constants.ApplicationContextXml);
        //设置war包轨迹跟踪器路径/META-INF/war-tracker
        File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker);

        boolean xmlInWar = false;
        try (JarFile jar = new JarFile(war)) {
            //读取war里面的META-INF/context.xml文件
            JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
            if (entry != null) {
                //如果存在,那么xmlInWar设置为true,表示在war里面找到了context描述文件
                xmlInWar = true;
            }
        } catch (IOException e) {
            /* Ignore */
        }

        // If there is an expanded directory then any xml in that directory
        // should only be used if the directory is not out of date and
        // unpackWARs is true. Note the code below may apply further limits
        boolean useXml = false;
        // If the xml file exists then expandedDir must exists so no need to
        // test that here
        //如果在已经存在这个扩展的目录了(可能以前tomcat启动时解压过,后者认为的手动解压)并且允许解压的,还
        //有就是不存在tomcat设置的war包追踪器,或者追踪器没有发生修改,那么使用这个已经存在的context描述文件
        if (xml.exists() && unpackWARs &&
                (!warTracker.exists() || warTracker.lastModified() == war.lastModified())) {
            useXml = true;
        }

        Context context = null;
        //是否允许发布当前这个xml描述,如果配置了不允许,并且存在安全校验,那么通过JDK的安全校验读取
        //这个xml是否允许发布
        boolean deployThisXML = isDeployThisXML(war, cn);

        try {
            //copyXML表示是否拷贝这个描述文件到$CATALINA_BASE/conf/catalina/host/下面,也就是前面发布xml描述那小节读取的那个目录
            //是否发布xml并且使用已经存在的扩展目录中的context描述文件
            if (deployThisXML && useXml && !copyXML) {
                synchronized (digesterLock) {
                    try {
                        //解析
                        context = (Context) digester.parse(xml);
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "hostConfig.deployDescriptor.error",
                                war.getAbsolutePath()), e);
                    } finally {
                        digester.reset();
                        if (context == null) {
                            context = new FailedContext();
                        }
                    }
                }
                //保存描述文件来源
                context.setConfigFile(xml.toURI().toURL());
            //是否发布context描述并且war中是否存在这样的描述
            } else if (deployThisXML && xmlInWar) {
                synchronized (digesterLock) {
                    try (JarFile jar = new JarFile(war)) {
                        //读取war包中的context描述文件
                        JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
                        try (InputStream istream = jar.getInputStream(entry)) {
                            //解析
                            context = (Context) digester.parse(istream);
                        }
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "hostConfig.deployDescriptor.error",
                                war.getAbsolutePath()), e);
                    } finally {
                        digester.reset();
                        if (context == null) {
                            context = new FailedContext();
                        }
                        //构建context描述文件的url在war中的地址
                        context.setConfigFile(
                                UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
                    }
                }
                //如果不允许发布,并且这个war中确实又存在xml描述文件
            } else if (!deployThisXML && xmlInWar) {
                // Block deployment as META-INF/context.xml may contain security
                // configuration necessary for a secure deployment.
                //打印日志,表示发布某个xml描述受阻
                log.error(sm.getString("hostConfig.deployDescriptor.blocked",
                        cn.getPath(), Constants.ApplicationContextXml,
                        new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")));
            } else {
                //其他情况,直接反射创建context对象
                context = (Context) Class.forName(contextClass).getConstructor().newInstance();
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error",
                    war.getAbsolutePath()), t);
        } finally {
            if (context == null) {
                context = new FailedContext();
            }
        }

        boolean copyThisXml = false;
        if (deployThisXML) {
            if (host instanceof StandardHost) {
                copyThisXml = ((StandardHost) host).isCopyXML();
            }

            // If Host is using default value Context can override it.
            //子容器覆盖父容器属性
            if (!copyThisXml && context instanceof StandardContext) {
                copyThisXml = ((StandardContext) context).getCopyXML();
            }
            //如果允许拷贝描述文件,并且描述文件在war包中
            if (xmlInWar && copyThisXml) {
                // Change location of XML file to config base
                //设置将要拷贝的路径以及描述文件的名字
                xml = new File(host.getConfigBaseFile(),
                        cn.getBaseName() + ".xml");
                try (JarFile jar = new JarFile(war)) {
                    JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
                    try (InputStream istream = jar.getInputStream(entry);
                            FileOutputStream fos = new FileOutputStream(xml);
                            BufferedOutputStream ostream = new BufferedOutputStream(fos, 1024)) {
                        byte buffer[] = new byte[1024];
                        while (true) {
                            int n = istream.read(buffer);
                            if (n < 0) {
                                break;
                            }
                            //拷贝文件
                            ostream.write(buffer, 0, n);
                        }
                        ostream.flush();
                    }
                } catch (IOException e) {
                    /* Ignore */
                }
            }
        }
        //构建发布应用
        DeployedApplication deployedApp = new DeployedApplication(cn.getName(),
                xml.exists() && deployThisXML && copyThisXml);

        long startTime = 0;
        // Deploy the application in this WAR file
        if(log.isInfoEnabled()) {
            startTime = System.currentTimeMillis();
            log.info(sm.getString("hostConfig.deployWar",
                    war.getAbsolutePath()));
        }
        //开始监控资源
        try {
            // Populate redeploy resources with the WAR file
            //监控war包
            deployedApp.redeployResources.put
                (war.getAbsolutePath(), Long.valueOf(war.lastModified()));
            //监控拷贝后的xml描述文件
            if (deployThisXML && xml.exists() && copyThisXml) {
                deployedApp.redeployResources.put(xml.getAbsolutePath(),
                        Long.valueOf(xml.lastModified()));
            } else {
                // In case an XML file is added to the config base later
                //监控未拷贝描述文件的路径,屏蔽后期拷贝到那个路径却没有进行监控的可能性
                deployedApp.redeployResources.put(
                        (new File(host.getConfigBaseFile(),
                                cn.getBaseName() + ".xml")).getAbsolutePath(),
                        Long.valueOf(0));
            }
            //获取ContextConfig
            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());
            //后期进行解压后,目录名将叫这个baseName
            context.setDocBase(cn.getBaseName() + ".war");
            //添加并启动自容器
            host.addChild(context);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error",
                    war.getAbsolutePath()), t);
        } finally {
        
            /**
             * 与发布描述文件一样,添加解压后可能存在的扩展目录进行监控
             */
        
            // If we're unpacking WARs, the docBase will be mutated after
            // starting the context
            boolean unpackWAR = unpackWARs;
            if (unpackWAR && context instanceof StandardContext) {
                unpackWAR = ((StandardContext) context).getUnpackWAR();
            }
            if (unpackWAR && context.getDocBase() != null) {
                File docBase = new File(host.getAppBaseFile(), cn.getBaseName());
                deployedApp.redeployResources.put(docBase.getAbsolutePath(),
                        Long.valueOf(docBase.lastModified()));
                addWatchedResources(deployedApp, docBase.getAbsolutePath(),
                        context);
                if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) {
                    deployedApp.redeployResources.put(xml.getAbsolutePath(),
                            Long.valueOf(xml.lastModified()));
                }
            } else {
                // Passing null for docBase means that no resources will be
                // watched. This will be logged at debug level.
                addWatchedResources(deployedApp, null, 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.deployWar.finished",
                war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
        }
    }

3、Deploy expanded folders

发布目录,我们部署web应用的时候除了扔一个war到tomcat的appBase目录下之外,我们还可能直接将一个解压好的web应用扔到appBase目录下。

protected void org.apache.catalina.startup.HostConfig.deployDirectories(File appBase, String[] files) {
        //appBase下的目录
        if (files == null)
            return;

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

        for (int i = 0; i < files.length; i++) {

            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            File dir = new File(appBase, files[i]);
            if (dir.isDirectory()) {
                ContextName cn = new ContextName(files[i], false);
                //如果已经发布过了,就直接跳过
                if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
                    continue;
                //异步发布目录
                results.add(es.submit(new DeployDirectory(this, cn, dir)));
            }
        }

        for (Future<?> result : results) {
            try {
                //阻塞等待发布完成
                result.get();
            } catch (Exception e) {
                log.error(sm.getString(
                        "hostConfig.deployDir.threaded.error"), e);
            }
        }
    }

下面是具体的发布代码

 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;
        //META-INF/context.xml
        File xml = new File(dir, Constants.ApplicationContextXml);
        //context.xml将要拷贝到的目录
        File xmlCopy =
                new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");


        DeployedApplication deployedApp;
        //是否允许拷贝xml描述文件
        boolean copyThisXml = isCopyXML();
        //是否允许发布xml描述文件
        boolean deployThisXML = isDeployThisXML(dir, cn);

        try {
            //如果对应的xml描述文件存在,并且允许发布的话,那么通过xml描述文件进行发布
            if (deployThisXML && xml.exists()) {
                synchronized (digesterLock) {
                    try {
                        //解析xml描述文件
                        context = (Context) digester.parse(xml);
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "hostConfig.deployDescriptor.error",
                                xml), e);
                        //如果创建失败,那么就创建一个FailedContext对象,表示这个容器创建失败
                        context = new FailedContext();
                    } finally {
                        digester.reset();
                        if (context == null) {
                            context = new FailedContext();
                        }
                    }
                }
                //子容器覆盖父容器的copyThisXml属性
                if (copyThisXml == false && context instanceof StandardContext) {
                    // Host is using default value. Context may override it.
                    copyThisXml = ((StandardContext) context).getCopyXML();
                }
                //拷贝对应的描述文件
                if (copyThisXml) {
                    Files.copy(xml.toPath(), xmlCopy.toPath());
                    context.setConfigFile(xmlCopy.toURI().toURL());
                } else {
                    context.setConfigFile(xml.toURI().toURL());
                }
            } 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 {
                context = (Context) Class.forName(contextClass).getConstructor().newInstance();
            }
            //设置ContextConfig生命周期监听器
            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 {
            //和发布描述,发布war包是一样的操作,不再赘述
            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)));
        }
    }

到这里我们已经分析完了HostConfig这个生命周期监听器进行context的发布了,下面我进入context的启动,在进入context的启动之前,我们顺便提下HostConfig最后一个感兴趣的事件就是stop事件,这个stop事件主要工作就是把自己从mbeanserver中注销。