Tomcat-Engine,Host和HostConfig


​ 从源码分析了ContainerBase,StandardEngine,StandardHost和HostConfig的实现逻辑。需要重点注意HostConfig的作用和部署流程

ContainerBase

​ 是所有容器(Engine、Host、Context、Wrapper)的核心抽象基类,定义并维护了容器通用的结构、生命周期管理机制和后台任务调度能力。核心职责如下:

  • init
    • 构建一个可过期的核心线程数为1的线程池(生命周期短,仅用来启动子容器),准备用于并发start子容器
  • start
    • 并发启动所有子容器,阻塞等待其完成
    • 启动自身的 Pipeline(包含所有 Valve
    • 根据 backgroundProcessorDelay 的配置,决定是否启动后台线程 ContainerBackgroundProcessor(默认仅 Engine 容器会设置该值(为 10 秒),其它容器默认不启动),用于周期性执行后台任务(如 session 过期清理)

核心源码

public abstract class ContainerBase extends LifecycleMBeanBase implements Container {
    // 子容器集合,key 为容器名,每个Container都有唯一name
    protected final HashMap<String, Container> children = new HashMap<>();
    /**
     * 后台处理线程执行间隔(秒),用于处理如 session 过期等后台任务。
     * - 默认值为 -1,表示不启动后台线程。
     * - 仅 Engine 容器会设置为 10,其它容器(Host、Context、Wrapper)默认不设置。
     * - ContainerBackgroundProcessor 线程由 Engine 启动,并递归处理其所有子容器的后台任务。
     */
    protected int backgroundProcessorDelay = -1;
    // 容器监听器(用于监听子容器结构或状态变更)
    protected final List<ContainerListener> listeners = new CopyOnWriteArrayList<>();
    // 当前容器名
    protected String name = null;
    // 当前容器的父容器(Engine为最顶层的容器,所以为null)
    protected Container parent = null;
    // 当前容器的 Pipeline,用于处理请求链路(Valve)
    protected final Pipeline pipeline = new StandardPipeline(this);

    // 后台线程引用(用于执行 backgroundProcessor)
    private Thread thread = null;
    // 启动/停止子容器的线程数(默认 1)
    private int startStopThreads = 1;
    // 启动或暂停当前容器的子容器线程池(默认线程数就为上面的1,且允许核心线程过期。一般只有在启动才需要用到,所以启动完毕后这个线程池就没啥用了,让里面的线程过期)
    protected ThreadPoolExecutor startStopExecutor;

    @Override
    protected void initInternal() throws LifecycleException {
        BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
        // 默认一个线程
        startStopExecutor = new ThreadPoolExecutor(
                getStartStopThreadsInternal(),
                getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
                startStopQueue,
                new StartStopThreadFactory(getName() + "-startStop-"));
        // 允许核心线程过期
        startStopExecutor.allowCoreThreadTimeOut(true);
        super.initInternal();
    }

    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // 先启动Cluster和Realm
        logger = null;
        getLogger();
        Cluster cluster = getClusterInternal();
        if (cluster instanceof Lifecycle) {
            ((Lifecycle) cluster).start();
        }
        Realm realm = getRealmInternal();
        if (realm instanceof Lifecycle) {
            ((Lifecycle) realm).start();
        }

        // 并发启动所有子容器
        Container children[] = findChildren();
        List<Future<Void>> results = new ArrayList<>();
        for (Container child : children) {
            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());
        }

        // 等所有子容器启动完毕,再启动当前容器的Pipeline(包括 Basic Valve)
        if (pipeline instanceof Lifecycle) {
            ((Lifecycle) pipeline).start();
        }

        // 触发当前组件里监听器lifecycleListeners的start事件
        setState(LifecycleState.STARTING);

        // 启动后台线程(如配置,线程也是daemon)
        threadStart();
    }
}

Engine

​ Engine是Tomcat最顶层的容器(没有父容器),和Service是一对一的关系,在他们内部互相保存了对方的引用,默认实现类时StandardEngine

​ 该容器本身不负责具体请求处理,核心职责是承载多个 Host 子容器,因此其初始化和启动过程直接复用了父类 ContainerBase 的逻辑

​ 需要注意的是,构造方法中设置了 backgroundProcessorDelay = 10,这会启动一个后台线程,周期性执行所有子容器的后台任务(如 session 过期清理等)。Tomcat 默认仅在 Engine 层启用此功能,从而递归驱动整个容器树的后台处理流程。

核心源码

public class StandardEngine extends ContainerBase implements Engine {

    public StandardEngine() {
        super();
        // 设置当前容器的basic value, 此 Valve 会作为 Pipeline 中的最后一个节点。
        pipeline.setBasic(new StandardEngineValve());

        // 设置后台处理线程的执行间隔(单位:秒)
        // 用于周期性执行容器级的后台任务,如 session 清理等,仅 Engine 层默认启用
        backgroundProcessorDelay = 10;
    }

    /**
     * 默认主机名。
     * 当请求url的host未匹配到任何具体的 Host 容器时,将路由到该默认 Host
     * 对应 server.xml 中 <Engine> 的 defaultHost 属性,默认值通常为 localhost
     */
    private String defaultHost = null;

    /**
     * 当前 Engine 所绑定的 Service(Engine 和 Service 是一对一关系)。
     */
    private Service service = null;
}

Host

​ Host 是 Engine 的子容器,默认实现为 StandardHost,表示对 HTTP 请求中 host 的抽象。请求到达时,Tomcat 会根据请求的 host 名(域名)查找匹配的 Host 实例;若无匹配项,则使用默认 Host(即 defaultHost,通常为 localhost

StandardHost 的初始化和启动逻辑沿用 ContainerBase 的通用流程,本身没有特殊处理。 真正负责扫描部署目录、创建并启动子容器 Context 的工作,实际上由其监听器 HostConfig 完成

核心源码

public class StandardHost extends ContainerBase implements Host {
    public StandardHost() {
        // StandardHostValve也会作为当前容器的pipeline的last value
        pipeline.setBasic(new StandardHostValve());

    }

    // 当前Host的别名,匹配别名也能匹配到当前Host
    private String[] aliases = new String[0];

    // 当前Host下的Web应用部署根目录,相对路径默认值为webapps
    private String appBase = "webapps";

    // 默认:${catalina.base}/webapps
    private volatile File appBaseFile = null;

    // 默认为:conf/Catalina/localhost目录
    private volatile File hostConfigBase = null;

    // 是否自动部署
    private boolean autoDeploy = true;

    // 添加到当前Host的子容器Context里的监听器(ContextConfig)
    private String configClass = "org.apache.catalina.startup.ContextConfig";

    // 当前Host的子容器Context的实现类
    private String contextClass = "org.apache.catalina.core.StandardContext";

    // 启动时部署Context
    private boolean deployOnStartup = true;

    // (应该被忽略的Context)默认为null
    private Pattern deployIgnore = null;

}

HostConfig

​ 在默认的 server.xml 配置中,<Host> 元素下并没有显式配置 <Context> 子元素,因此在解析 server.xml 时并不会为 Host 创建任何 Context 子容器。尽管如此,Tomcat 仍能在启动时自动部署 ${catalina.base}/webapps/ 目录下的应用,这正是由 HostConfig 这个 LifecycleListener 完成的。

  • 注册方式

    ​ 在 Tomcat 启动期间,通过 Digester 解析 server.xml 时,会由 HostRuleSet#addRuleInstances() 方法为每个 Host 元素注册默认的生命周期监听器org.apache.catalina.startup.HostConfig,其会被实例化添加为Host的LifecycleListener

  • 启动时机

    ​ StandardHost在startInternal()期间中的子容器启动完成后(默认没有子容器),但后台线程还未启动时,会触发lifecycle的START_EVENT事件,从而触发HostConfig的start,来解析目标文件夹下webapps的项目,自动部署所有符合规则的 Web 应用。

  • 三种部署方式

    • XML 配置部署(conf/Catalina/{hostname}/*.xml)
      • 每个xml文件即为一个Context(IDEA的war exploded就是这种方式)
    • WAR 包部署(webapps/*.war)
      • 每个war包即为一个Context
    • directory部署(webapps/{dir})
      • 每个目录被当作一个Context,是项目中最常用的一种
  • 路径问题

    ​ 上诉三种部署方式都遵循相同的路径规则。即Context的name和path为去除拓展名后的部分。对于特殊的ROOT,则表示为根路径/

核心源码


public class HostConfig implements LifecycleListener {

    // context默认实现类
    protected String contextClass = "org.apache.catalina.core.StandardContext";

    // 当前绑定的 Host 容器
    protected Host host = null;

    // 已部署的应用(用于记录当前 host 下部署的 context 状态)
    protected final Map<String, DeployedApplication> deployed = new ConcurrentHashMap<>();

    // 正在处理部署/卸载操作的 Context 名集合(用于防止重复并发部署)
    private Set<String> servicedSet = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

    /**
     * Host 生命周期监听方法,根据不同生命周期阶段执行对应操作:
     * - BEFORE_START:确认所需目录存在
     * - START:执行部署流程
     * - PERIODIC:周期性检查(autoDeploy 场景下可能重新部署)
     * - STOP:停止并清理部署
     */
    @Override
    public void lifecycleEvent(LifecycleEvent event) {

        if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
            // 周期任务(例如监测部署目录变更)
            check();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            // StandardHost启动前触发
            // 确保 ${catalina.base}/webapps和conf/Catalina/localhost目录存在(不存在也只会打日志)
            beforeStart();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
            // StanardHost正在启动
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }

    public void start() {
        // 默认:${catalina.base}/webapps
        if (!host.getAppBaseFile().isDirectory()) { // app文件目录不是文件夹,代表不需要部署
            log.error(sm.getString("hostConfig.appBase", host.getName(),
                    host.getAppBaseFile().getPath()));
            host.setDeployOnStartup(false);
            host.setAutoDeploy(false);
        }
        if (host.getDeployOnStartup()) {
            // 默认为true,开始部署
            deployApps();
        }
    }

    protected void deployApps() {
        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        // 过滤器默认为null,所以还是返回全部
        String[] filteredAppPaths = filterAppPaths(appBase.list()); // 过滤webapps文件夹下的所有文件
        // conf/Catalina/localhost目录下所有的xml方式部署(IDEA的war exploded就是这种方式)
        // 每一个xml其实就是一个context,文件名就是context的ptah。ROOT.xml就是默认的context,即path为空字符串
        deployDescriptors(configBase, configBase.list());
        // war包的形式部署
        // 部署${catalina.base}/webapps目录下的所有war包
        deployWARs(appBase, filteredAppPaths);
        // 直接以文件夹的形式部署
        // 部署${catalina.base}/webapps目录下的所有文件夹形式的context
        deployDirectories(appBase, filteredAppPaths);
    }

}

directory部署

​ 重点解析下directory部署方式,毕竟这是最常用的。其会在当前host的startStop线程池中异步部署,根据如下源码总结出核心流程

  1. 优先使用 META-INF/context.xml 配置(如果存在)
  2. 解析并构造出Context实例
  3. 设置Context的一些基础属性(name和path等,以项目目录名为准)
  4. 将Context添加为对应Host的子容器(此时会触发Context的start)

/**
 * 以文件夹方式部署应用(appBase 下的目录结构)
 * - 忽略 META-INF、WEB-INF
 * - 忽略已存在的部署
 * - 支持异步并发部署(通过 Host 提供的线程池)
 */
protected void deployDirectories(File appBase, String[] files) {

    if (files == null) {
        return;
    }

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

    for (String file : files) {
        // META-INF和WEB-INF文件加不部署
        if (file.equalsIgnoreCase("META-INF")) {
            continue;
        }
        if (file.equalsIgnoreCase("WEB-INF")) {
            continue;
        }

        File dir = new File(appBase, file);
        if (dir.isDirectory()) {
            // ROOT文件夹会被解析为 根context,也就是path为空字符串
            ContextName cn = new ContextName(file, false);

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

                    // 异步部署
                    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);
        }
    }
}

// ================ 开始部署 ==================
protected void deployDirectory(ContextName cn, File dir) {

    Context context = null;
    // Web 应用目录下的META-INF/context.xml 文件
    File xml = new File(dir, Constants.ApplicationContextXml);
    // 将META-INF/context.xml拷贝到的目标位置:conf/Catalina/localhost/${contextName}.xml
    File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");

    DeployedApplication deployedApp;
    // 是否拷贝 META-INF/context.xml 到 conf 目录(Host 默认 false,可被 Context 覆盖)
    boolean copyThisXml = isCopyXML();
    // 是否允许部署该 META-INF/context.xml(默认true)
    boolean deployThisXML = isDeployThisXML(dir, cn);

    try {
        if (deployThisXML && xml.exists()) {
            // 存在有效 META-INF/context.xml 且支持部署这个xml,则通过 digester 解析生成 Context
            synchronized (digesterLock) {
                context = (Context) digester.parse(xml);
            }

            // 如果 host 设置不允许拷贝xml,则再次通过 context的copyXML 判断一次
            if (copyThisXml == false && context instanceof StandardContext) {
                copyThisXml = ((StandardContext) context).getCopyXML();
            }

            // 根据是否拷贝,设置 context 的配置文件位置(conf 目录 or 原始目录)
            if (copyThisXml) {
                Files.copy(xml.toPath(), xmlCopy.toPath());
                context.setConfigFile(xmlCopy.toURI().toURL());
            } else {
                context.setConfigFile(xml.toURI().toURL());
            }
        } else if (!deployThisXML && xml.exists()) {
            // 禁止部署,但 META-INF/context.xml 存在 —— 为安全考虑,阻止启动(避免绕过配置)
            context = new FailedContext();
        } else {
            // 没有 META-INF/context.xml文件,则手动 new 一个 context 实例
            context = (Context) Class.forName(contextClass).getConstructor().newInstance();
        }
        // 为 Context 添加生命周期监听器(默认是 ContextConfig,用于触发解析 web.xml等)
        Class<?> clazz = Class.forName(host.getConfigClass());
        LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
        context.addLifecycleListener(listener);

        // 设置 Context 的一些基础信息
        context.setName(cn.getName());
        context.setPath(cn.getPath());
        context.setWebappVersion(cn.getVersion());
        context.setDocBase(cn.getBaseName());
        // 将 Context 添加到当前 Host,并自动触发生命周期(start/init)
        host.addChild(context);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("hostConfig.deployDir.error", dir.getAbsolutePath()), t);
    } finally {
        // 部署完成后记录已部署信息,构建 DeployedApplication(省略细节)
        // ...

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