为什么关心Tomcat中一个web应用的加载过程?在前面的文章中看过多次Tomcat的组件结构图,这里再贴出来回顾一下:
之前的《Tomcat7启动分析》系列文章中看到Tomcat启动的时候将会解析server.xml,根据里面所配置的各个节点信息逐一初始化和启动相应组件(即分别调用它们的init和start方法),但浏览一下Tomcat7源码中的server.xml的内容,里面对应上图中的已经默认配置的各级组件包括Server、Service、Engine、Connector、Host、Valve。上图中的Context组件实际就是我们通常所说的一个web应用,有趣的是在server.xml中并没有配置该组件,而我们默认启动的时候实际上已经有好几个web应用可以访问了:
这个到底是怎么回事?
看过前面的《Tomcat7中一次请求处理的前世今生》系列文章的人应该知道,浏览器一次请求送到Tomcat服务器之后,最终会根据浏览器中的url路径找到相应的实际要访问的web应用的context对象(默认即org.apache.catalina.core.StandardContext类的实例)。比如访问的url为http://localhost:8080/,那么将会送到上图ROOT文件夹表示的web应用中,访问的url为http://localhost:8080/docs,那么将访问docs文件夹表示的web应用。所以能够猜到的是在Tomcat启动完成后,必定容器内部已经构造好了表示相应web应用的各个context对象。
本文就对这个问题一探究竟。在《Tomcat7服务器关闭原理》的开头提到,默认的配置下Tomcat启动完之后会看到后台实际上总共有6个线程在运行:
前面的几篇文章中涉及了main、http-bio-8080-Acceptor-0、http-bio-8080-AsyncTimeout、ajp-bio-8009-Acceptor-0、ajp-bio-8009-AsyncTimeout,已经谈到了这些线程的作用,它们是如何产生并响应请求的。但有一个线程没有说,即ContainerBackgroundProcessor[StandardEngine[Catalina]],而本文要解答的问题奥秘就在这个线程之中。
先看看这个线程是如何产生的,其实从命名就可以看出一些端倪,它叫做容器后台处理器,并且跟StandardEngine关联起来,它的产生于作用也同样如此。
Tomcat7中所有的默认容器组件(StandardEngine、StandardHost、StandardContext、StandardWrapper)都会继承父类org.apache.catalina.core.ContainerBase,在这些容器组件启动时将会调用自己内部的startInternal方法,在该方法内部一般会调用父类的startInternal方法(StandardContext类的实现除外),比如org.apache.catalina.core.StandardEngine类中的startInternal方法:
@Override
protected synchronized void startInternal() throws LifecycleException {
// Log our server identification information
if(log.isInfoEnabled())
log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());
// Standard container startup
super.startInternal();
}
最后的super.startInternal()即调用父类org.apache.catalina.core.ContainerBase的startInternal方法,在该方法最后:
// Start the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
setState(LifecycleState.STARTING);
// Start our thread
threadStart();
第6行设置了LifecycleState.STARTING状态(这样将向容器发布一个Lifecycle.START_EVENT事件),这一行的作用本文后面会提到,暂且按下不表。第9行调用threadStart方法,看看threadStart方法的代码:
// -------------------- Background Thread --------------------
/**
* Start the background thread that will periodically check for
* session timeouts.
*/
protected void threadStart() {
if (thread != null)
return;
if (backgroundProcessorDelay <= 0)
return;
threadDone = false;
String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
thread = new Thread(new ContainerBackgroundProcessor(), threadName);
thread.setDaemon(true);
thread.start();
}
这里可以看到如果两个前置校验条件通过的话将会启动一个线程,并且线程的名字即以" ContainerBackgroundProcessor[ "开头,线程名字后面取的是对象的toString方法,以StandardEngine为例,看看org.apache.catalina.core.StandardEngine的toString方法实现:
/**
* Return a String representation of this component.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder("StandardEngine[");
sb.append(getName());
sb.append("]");
return (sb.toString());
}
以上解释了这个后台线程的来历。
但这里有一个问题,既然StandardEngine、StandardHost都会调用super.startInternal()方法,按默认配置,后台理应产生两个后台线程,实际为什么只有一个?
回到org.apache.catalina.core.ContainerBase的threadStart方法,在启动线程代码之前有两个校验条件:
if (thread != null)
return;
if (backgroundProcessorDelay <= 0)
return;
容器组件对象初始化时thread为null,backgroundProcessorDelay是-1
/**
* The background thread.
*/
private Thread thread = null;
/**
* The processor delay for this component.
*/
protected int backgroundProcessorDelay = -1;
但org.apache.catalina.core.StandardEngine在其自身构造函数中做了一点修改:
public StandardEngine() {
super();
pipeline.setBasic(new StandardEngineValve());
/* Set the jmvRoute using the system property jvmRoute */
try {
setJvmRoute(System.getProperty("jvmRoute"));
} catch(Exception ex) {
log.warn(sm.getString("standardEngine.jvmRouteFail"));
}
// By default, the engine will hold the reloading thread
backgroundProcessorDelay = 10;
}
构造函数最后将父类的backgroundProcessorDelay的值由-1改成了10,所以Tomcat启动解析xml时碰到一个Engine节点就会对应产生一个后台处理线程。
讲完了这个后台处理线程的产生,看看这个线程所作的事情,再看下这个线程的启动代码:
thread = new Thread(new ContainerBackgroundProcessor(), threadName);
thread.setDaemon(true);
thread.start();
所以这个线程将会执行ContainerBase的内部类ContainerBackgroundProcessor的run方法,看下ContainerBackgroundProcessor的全部实现代码:
/**
* Private thread class to invoke the backgroundProcess method
* of this container and its children after a fixed delay.
*/
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
Container parent = (Container) getMappingObject();
ClassLoader cl =
Thread.currentThread().getContextClassLoader();
if (parent.getLoader() != null) {
cl = parent.getLoader().getClassLoader();
}
processChildren(parent, cl);
}
}
}
protected void processChildren(Container container, ClassLoader cl) {
try {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
container.backgroundProcess();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("Exception invoking periodic operation: ", t);
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i], cl);
}
}
}
}
在它的run方法暂停一段时间之后会调用processChildren方法,而processChildren方法做了两件事,一是调用容器组件自身的backgroundProcess方法,而是取出该容器组件的所有子容器组件并调用它们的processChildren方法。归结起来这个线程的实现就是定期通过递归的方式调用当前容器及其所有子容器的backgroundProcess方法。
而这个backgroundProcess方法在ContainerBase内部已经给出了实现:
@Override
public void backgroundProcess() {
if (!getState().isAvailable())
return;
if (cluster != null) {
try {
cluster.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);
}
}
if (loader != null) {
try {
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);
}
}
if (manager != null) {
try {
manager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);
}
}
Realm realm = getRealmInternal();
if (realm != null) {
try {
realm.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);
}
}
Valve current = pipeline.getFirst();
while (current != null) {
try {
current.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);
}
current = current.getNext();
}
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
这段代码就不一一解释了,概括起来说就是逐个调用与容器相关其它内部组件的backgroundProcess方法。最后注册一个Lifecycle.PERIODIC_EVENT事件。
上面就是Tomcat7的后台处理线程所作的事情的概述,在Tomcat的早期版本中有一些后台处理的事情原来是在各个组件内部分别自定义一个线程并启动,在Tomcat5中改成了所有后台处理共享同一线程的方式。
回到本文要解答的问题,web应用如何加载到容器中的?在ContainerBase类的backgroundProcess方法的最后:
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
向容器注册了一个PERIODIC_EVENT事件。前面说道默认的ContainerBackgroundProcessor[StandardEngine[Catalina]]线程会定期(默认为10秒)执行Engine、Host、Context、Wrapper各容器组件及与它们相关的其它组件的backgroundProcess方法,所以也会定期向Host组件发布一个PERIODIC_EVENT事件,这里看下StandardHost都会关联的一个监听器org.apache.catalina.startup.HostConfig(
在Tomcat启动解析xml时org.apache.catalina.startup.Catalina类的386行:digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"))
在HostRuleSet类的addRuleInstances方法中:
public void addRuleInstances(Digester digester) {
digester.addObjectCreate(prefix + "Host",
"org.apache.catalina.core.StandardHost",
"className");
digester.addSetProperties(prefix + "Host");
digester.addRule(prefix + "Host",
new CopyParentClassLoaderRule());
digester.addRule(prefix + "Host",
new LifecycleListenerRule
("org.apache.catalina.startup.HostConfig",
"hostConfigClass"));
digester.addSetNext(prefix + "Host",
"addChild",
"org.apache.catalina.Container");
第9到12行看到,所有Host节点都会添加一个org.apache.catalina.startup.HostConfig对象作为org.apache.catalina.core.StandardHost对象的监听器
),在HostConfig的lifecycleEvent方法中可以看到如果Host组件收到了Lifecycle.PERIODIC_EVENT事件的发布所作出的响应(如果对Tomcat7的Lifecycle机制不清楚可以看下《Tomcat7启动分析(五)Lifecycle机制和实现原理》):
public void lifecycleEvent(LifecycleEvent event) {
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
第17行,如果发布的事件是PERIODIC_EVENT将会执行check方法。第19行,如果发布的事件是START_EVENT则执行start方法。check方法和start方法最后都会调用deployApps()方法,看下这方法的实现:
/**
* Deploy applications for any directories or WAR files that are found
* in our "application root" directory.
*/
protected void deployApps() {
File appBase = appBase();
File configBase = configBase();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}
这里即各种不同方式发布web应用的代码。
本文前面提到默认情况下组件启动的时候会发布一个Lifecycle.START_EVENT事件(在org.apache.catalina.core.ContainerBase类的startInternal方法倒数第二行),回到HostConfig的lifecycleEvent方法中,所以默认启动时将会执行HostConfig的start方法,在该方法的最后:
if (host.getDeployOnStartup())
deployApps();
因为默认配置host.getDeployOnStartup()返回true,这样容器就会在启动的时候直接加载相应的web应用。
当然,如果在server.xml中Host节点的deployOnStartup属性设置为false,则容器启动时不会加载应用,启动完之后不能立即提供web应用的服务。但因为有上面提到的后台处理线程在运行,会定期执行HostConfig的check方法:
/**
* Check status of all webapps.
*/
protected void check() {
if (host.getAutoDeploy()) {
// Check for resources modification to trigger redeployment
DeployedApplication[] apps =
deployed.values().toArray(new DeployedApplication[0]);
for (int i = 0; i < apps.length; i++) {
if (!isServiced(apps[i].name))
checkResources(apps[i]);
}
// Check for old versions of applications that can now be undeployed
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
// Hotdeploy applications
deployApps();
}
}
如果Host节点的autoDeploy属性是true(默认设置即为true),可以看到check方法最后同样会加载web应用。