开篇
本篇文章主要是是想从源码角度梳理下web应用加载的全过程,注意这里重点是梳理应用加载的整体过程,对于真正执行web应用加载的细节部分后面会有专门的文章进行梳理。
下图中我们只关注标记的红色边框里面的内容,这部分其实也就是war包的加载过程,希望能够讲明白这部分即可。
整个web应用的加载过程是容器StandardHost的start过程,所以整体加载过程包括StandardHost的启动和Web应用加载两个部分。
另外需要理解的是配置文件Server.xml当中关于Host的配置在Tomcat整体的init过程中生成HostConfig对象,所以才有StandardHost->ContainerBase->HostConfig的调用关系链,之前在HostConfig的生成当中纠结了很久最后才饶出来。
下图是StandardHost的类图,之所以把类图放这里是为了方便后面大家对代码的理解。
Web应用加载过程 - StandardHost
- 1、StandardHost的启动过程通过startInternal()方法开始并调用父类startInternal()方法。
- 2、super.startInternal()调用ContainerBase父类的方法。
public class StandardHost extends ContainerBase implements Host {
protected synchronized void startInternal() throws LifecycleException {
// 省略一些核心代码
super.startInternal();
}
}
Web应用加载过程 - ContainerBase
- 1、StandHost执行父类ContainerBase的startInternal() 执行一系列对象启动过程。
- 2、startInternal()方法的setState(LifecycleState.STARTING)设置启动状态。
public abstract class ContainerBase extends LifecycleMBeanBase
implements Container {
protected synchronized void startInternal() throws LifecycleException {
// Start our subordinate components, if any
logger = null;
getLogger();
Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
((Lifecycle) cluster).start();
}
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}
// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
MultiThrowable multiThrowable = new MultiThrowable();
for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
multiThrowable.add(e);
}
}
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
// 设置启动状态
setState(LifecycleState.STARTING);
// Start our thread
threadStart();
}
}
Web应用加载过程 - LifecycleBase
- 1、LifecycleBase的setState()方法传入LifecycleState.STARTING参数依次执行。
- 2、fireLifecycleEvent()内部执行listener.lifecycleEvent(event)中listener为HostConfig。
public abstract class LifecycleBase implements Lifecycle {
protected synchronized void setState(LifecycleState state)
throws LifecycleException {
setStateInternal(state, null, true);
}
private synchronized void setStateInternal(LifecycleState state,
Object data, boolean check) throws LifecycleException {
// 省略核心代码
this.state = state;
String lifecycleEvent = state.getLifecycleEvent();
if (lifecycleEvent != null) {
fireLifecycleEvent(lifecycleEvent, data);
}
}
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
// HostConfig对象
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
}
Web应用加载过程 - HostConfig
- 1、HostConfig的lifecycleEvent()方法内部通过start()方法启动web应用的部署。
- 2、start()方法内部执行deployApps()方法部署web应用。
- 3、deployApps()内部执行不同类型的web应用的部署。
public class HostConfig implements LifecycleListener {
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();
}
}
public void start() {
// 省略相关代码
if (host.getDeployOnStartup())
deployApps();
}
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
// todo filteredAppPaths是指已经过滤的文件
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// todo 部署War包 Deploy WARs
deployWARs(appBase, filteredAppPaths);
// todo 部署展开的目录 Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}
}
- 4、deployWARs方法内部提交DeployWar任务进行war包部署
- 5、deployDirectories内部提交DeployDirectory任务进行目录部署
- 6、deployDescriptors内部提交DeployDescriptor任务进行部署。
- 7、DeployWar、DeployDirectory、DeployDescriptor都实现Runnable接口。
public class HostConfig implements LifecycleListener {
protected void deployDescriptors(File configBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
File contextXml = new File(configBase, files[i]);
if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
ContextName cn = new ContextName(files[i], true);
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);
}
}
}
protected void deployWARs(File appBase, String[] files) {
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 war = new File(appBase, files[i]);
// 省略相关代码
results.add(es.submit(new DeployWar(this, cn, war)));
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {}
}
}
protected void deployDirectories(File appBase, String[] files) {
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);
results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {}
}
}
- 8、DeployDescriptor的run()方法通过执行deployDescriptor进行部署
- 9、DeployWar的run()方法通过执行deployDescriptor进行部署
- 10、DeployDirectory的run()方法通过执行deployDirectory进行部署
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() {
config.deployDescriptor(cn, descriptor);
}
}
private static class DeployWar implements Runnable {
private HostConfig config;
private ContextName cn;
private File war;
public DeployWar(HostConfig config, ContextName cn, File war) {
this.config = config;
this.cn = cn;
this.war = war;
}
@Override
public void run() {
config.deployWAR(cn, war);
}
}
private static class DeployDirectory implements Runnable {
private HostConfig config;
private ContextName cn;
private File dir;
public DeployDirectory(HostConfig config, ContextName cn, File dir) {
this.config = config;
this.cn = cn;
this.dir = dir;
}
@Override
public void run() {
config.deployDirectory(cn, dir);
}
}
}
Web应用加载过程 - 目录形式的项目加载
- 1、Directory类型的项目真正加载实现的过程。
- 2、这部分后面单独进行一篇文章进行讲解。
public class HostConfig implements LifecycleListener {
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);
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();
}
}
}
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();
}
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)));
}
}
}
Web应用加载过程 - War包形式的项目加载
- 1、War类型的项目真正加载实现的过程。
- 2、这部分后面单独进行一篇文章进行讲解。
public class HostConfig implements LifecycleListener {
protected void deployWAR(ContextName cn, File war) {
File xml = new File(host.getAppBaseFile(),
cn.getBaseName() + "/" + Constants.ApplicationContextXml);
File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker);
boolean xmlInWar = false;
try (JarFile jar = new JarFile(war)) {
JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
if (entry != null) {
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
if (xml.exists() && unpackWARs &&
(!warTracker.exists() || warTracker.lastModified() == war.lastModified())) {
useXml = true;
}
Context context = null;
boolean deployThisXML = isDeployThisXML(war, cn);
try {
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());
} else if (deployThisXML && xmlInWar) {
synchronized (digesterLock) {
try (JarFile jar = new JarFile(war)) {
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.setConfigFile(
UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
}
}
} else if (!deployThisXML && xmlInWar) {
// 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(), Constants.ApplicationContextXml,
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")));
} else {
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();
}
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
deployedApp.redeployResources.put
(war.getAbsolutePath(), Long.valueOf(war.lastModified()));
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));
}
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() + ".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)));
}
}
}
Web应用加载过程 - Descriptor形式的项目加载
- 1、Descriptor类型的项目真正加载实现的过程。
- 2、这部分后面单独进行一篇文章进行讲解。
public class HostConfig implements LifecycleListener {
protected void deployDescriptor(ContextName cn, File contextXml) {
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;
boolean isExternalWar = false;
boolean isExternal = false;
File expandedDocBase = null;
try (FileInputStream fis = new FileInputStream(contextXml)) {
synchronized (digesterLock) {
try {
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();
}
}
}
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);
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
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
if (!docBase.getCanonicalPath().startsWith(
host.getAppBaseFile().getAbsolutePath() + File.separator)) {
isExternal = true;
deployedApp.redeployResources.put(
contextXml.getAbsolutePath(),
Long.valueOf(contextXml.lastModified()));
deployedApp.redeployResources.put(docBase.getAbsolutePath(),
Long.valueOf(docBase.lastModified()));
if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
isExternalWar = true;
}
} else {
log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified",
docBase));
// Ignore specified docBase
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
// default to appBase dir + name
expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName());
if (context.getDocBase() != null
&& !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
// first assume docBase is absolute
expandedDocBase = new File(context.getDocBase());
if (!expandedDocBase.isAbsolute()) {
// if docBase specified and relative, it must be relative to 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
if (isExternalWar) {
if (unpackWAR) {
deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
Long.valueOf(expandedDocBase.lastModified()));
addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
} else {
addWatchedResources(deployedApp, null, context);
}
} else {
// Find an existing matching war and expanded folder
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
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);
}
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
addGlobalRedeployResources(deployedApp);
}
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)));
}
}
}