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,是项目中最常用的一种
- XML 配置部署(conf/Catalina/{hostname}/*.xml)
路径问题
上诉三种部署方式都遵循相同的路径规则。即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线程池中异步部署,根据如下源码总结出核心流程
- 优先使用 META-INF/context.xml 配置(如果存在)
- 解析并构造出Context实例
- 设置Context的一些基础属性(name和path等,以项目目录名为准)
- 将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);
}