欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
1. 组件启动和Web应用加载
组件启动satrt方法流程图:
详细注解分析见tomcat-9.0.60-src源码解析 。接下来我们直接进入web应用加载。Web应用加载属于Server启动的核心处理过程。
Catalina对Web应用的加载主要由StandardHost、HostConfig、StandardContext、ContextConfig、StandardWrapperi这5个类完成。如果以一张时序图来展示Catalina对Web应用的加载过程,对应的流程图为:
本文我们重点分析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的启动加载过程如下:
-
为Host添加一个Valve
实现ErrorReportValve(我们也可以通过修改Host的errorReportValveClass属性指定自己的错误处理Valve),该类主要用于在服务器处理异常时输出错误页面。如果我们没有在web.xml中添加错误处理页面,Tomcat返回的异常栈页面便是由ErrorReportValve生成的。 -
调用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解析:
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描述文件的部署过程步骤如下:
- 扫描Host配置文件基础目录,即
$CATALINA BASE/conf<Engine名称>/<Host名称>,对于该目录下的每个配置文件,由线程池完成解析部署。 - 对于每个文件的部署线程,进行如下操作。
- 使用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应用目录的过程如下:
-
对于Host的appBase目录(默认为
$CATALINA_BASE/webapps)下所有符合条件的目录(不符合deployIgnorel的过滤规则、目录名不为META-INF和WEB-INF),由线程池完成部署。 -
对于每个目录进行如下操作。
- 如果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的deployXML属性值为true(即通过Context描述文件部署),并且存在META-INF/context.xml文件,则使用Digesterf解析context.xml文件创建Context对象。如果Context的copyXML属性为true,则将描述文件复制到
通过Host的addChild()方法将Context实例添加到Host。该方法会判断Host是否已启动,如果是,则直接启动Context。分析方法如上,不在赘述。
3.1.3 WAR包部署
WAR包部署和Web目录部署基本类似,只是由于WAR包作为一个压缩文件,增加了部分针 对压缩文件的处理。
路径为HostConfig.deployWARs()->DeployWar.run()->HostConfig.deployWAR()源码分析方式如上,不在赘述,详见tomcat-9.0.60-src源码解析
其具体的部署过程如下:
-
对于Host的appBase目录(默认为SCATALINA BASE/webapps)下所有符合条件的WAR包(不符合deployIgnoref的过滤规则、文件名不为META-INF和WEB-NF、以war作为扩展名的文件),由线程池完成部署。
-
对于每个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应用。
- 如果Host的deployXML属性为true,且在WAR包同名目录(去除扩展名)下存在META-INF/context.xml文件,同时Context的copyXML属性为false,则使用该描述文件创建Context实例(用于WAR包解压目录位于部署目录的情况)。
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时处理):
-
对于每一个已部署的Web应用(不包含在serviced列表中,Serviced列表的具体作用参见下面的“注意”),检查用于重新部署的守护资源。对于每一个守护的资源文件或者目录,如果发生变更,那么就有以下几种情况。
- 如果资源对应为目录,则仅更新守护资源列表中的上次修改时间
- 如果Web应用存在Context描述文件并且当前变更的是WAR包文件,则得到原Context的docBase。如果docBase不以“.war”结尾(即Context指向的是WAR解压目录,删除解压目录并重新加载,否则直接重新加载。更新守护资源。
- 其他情况下,直接御载应用,并由接下来的处理步骤重新部署。
-
对于每个已部署的wb应用,检查用于重新加载的守护资源,如果资源发生变更,则重新加载Context对象。
-
如果Host配置为卸载I旧版本应用(undeployoldVersions,属性为true),则检查并卸载。
-
部署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的分析