热部署
热部署指的是当Tomcat启动之后,如果将新的应用添加到webapp目录下面或者是修改了应用,那么不需要重新启动tomcat就能加载新的应用
热部署的时机
从上一篇热加载我们知道,StandardEngine会启动一个ContainerBase中定义的后台线程,来执行当前容器以及所有子容器的backgroundProcess方法,并且一般容器在调用完自己的backgroundProcess方法之后,还会调用父类的backgroundProcess,也就是ContainerBase中的backgroundProcess
public void backgroundProcess() {
if (!getState().isAvailable())
return;
Cluster cluster = getClusterInternal();
if (cluster != null) {
try {
cluster.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.cluster",
cluster), 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);
}
可以看到最后会触发PERIODIC_EVENT
从前面几篇分析文章知道,host容器有一个监听器HostConfig,我们看下该类如何对这个事件进行处理
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());
setContextClass(((StandardHost) host).getContextClass());
}
} 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.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
可以看到当接收到PERIODIC_EVENT事件后会执行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 (!isServiced(app.name))
// 检查应用的资源判断是否需要重新部署
checkResources(app, false);
}
// Check for old versions of applications that can now be undeployed
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
// Hotdeploy applications
deployApps();
}
}
检查应用是否需要重新部署
protected synchronized void checkResources(DeployedApplication app,
boolean skipFileModificationResolutionCheck) {
// 获取当前应用需要重新部署的资源
String[] resources =
app.redeployResources.keySet().toArray(new String[0]);
// Offset the current time by the resolution of File.lastModified()
// 修改时间必须在当前时间的前1s才会被识别为修改,如果发生修改的时间还不足1s,不会被识别为修改
long currentTimeWithResolutionOffset =
System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS;
// 下面检查应用是否需要重新部署redeploy
for (int i = 0; i < resources.length; i++) {
File resource = new File(resources[i]);
if (log.isDebugEnabled())
log.debug("Checking context[" + app.name +
"] redeploy resource " + resource);
// 获取当前正在使用的资源的修改时间
long lastModified =
app.redeployResources.get(resources[i]).longValue();
if (resource.exists() || lastModified == 0) {
// File.lastModified() has a resolution of 1s (1000ms). The last
// modified time has to be more than 1000ms ago to ensure that
// modifications that take place in the same second are not
// missed. See Bug 57765.
if (resource.lastModified() != lastModified && (!host.getAutoDeploy() ||
resource.lastModified() < currentTimeWithResolutionOffset ||
skipFileModificationResolutionCheck)) {
// 如果资源是目录,那么只是更新修改时间
if (resource.isDirectory()) {
// No action required for modified directory
app.redeployResources.put(resources[i],
Long.valueOf(resource.lastModified()));
} else if (app.hasDescriptor &&
resource.getName().toLowerCase(
Locale.ENGLISH).endsWith(".war")) {
// 针对使用描述符部署的war应用进行特殊处理
// Modified WAR triggers a reload if there is an XML
// file present
// The only resource that should be deleted is the
// expanded WAR (if any)
Context context = (Context) host.findChild(app.name);
String docBase = context.getDocBase();
if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
// This is an expanded directory
File docBaseFile = new File(docBase);
if (!docBaseFile.isAbsolute()) {
docBaseFile = new File(host.getAppBaseFile(),
docBase);
}
reload(app, docBaseFile, resource.getAbsolutePath());
} else {
reload(app, null, null);
}
// Update times
app.redeployResources.put(resources[i],
Long.valueOf(resource.lastModified()));
app.timestamp = System.currentTimeMillis();
boolean unpackWAR = unpackWARs;
if (unpackWAR && context instanceof StandardContext) {
unpackWAR = ((StandardContext) context).getUnpackWAR();
}
if (unpackWAR) {
addWatchedResources(app, context.getDocBase(), context);
} else {
addWatchedResources(app, null, context);
}
return;
} else {
// Everything else triggers a redeploy
// (just need to undeploy here, deploy will follow)
// 一般会走这个逻辑,这里只是卸载应用
// 将当前应用对应的Context对象从Host的子容器中移除
// 并且将当前应用从deployed这个map中移除
undeploy(app);
// 删除资源
deleteRedeployResources(app, resources, i, false);
return;
}
}
} else {
// There is a chance the the resource was only missing
// temporarily eg renamed during a text editor save
try {
Thread.sleep(500);
} catch (InterruptedException e1) {
// Ignore
}
// Recheck the resource to see if it was really deleted
if (resource.exists()) {
continue;
}
// Undeploy application
undeploy(app);
deleteRedeployResources(app, resources, i, true);
return;
}
}
// 下面检查应用是否需要reload
resources = app.reloadResources.keySet().toArray(new String[0]);
boolean update = false;
for (String s : resources) {
File resource = new File(s);
if (log.isDebugEnabled()) {
log.debug("Checking context[" + app.name + "] reload resource " + resource);
}
long lastModified = app.reloadResources.get(s).longValue();
// File.lastModified() has a resolution of 1s (1000ms). The last
// modified time has to be more than 1000ms ago to ensure that
// modifications that take place in the same second are not
// missed. See Bug 57765.
// 当多个文件修改了,那么这里只会触发一次reload
if ((resource.lastModified() != lastModified &&
(!host.getAutoDeploy() ||
resource.lastModified() < currentTimeWithResolutionOffset ||
skipFileModificationResolutionCheck)) ||
update) {
if (!update) {
// Reload application
reload(app, null, null);
update = true;
}
// Update times. More than one file may have been updated. We
// don't want to trigger a series of reloads.
app.reloadResources.put(s,
Long.valueOf(resource.lastModified()));
}
app.timestamp = System.currentTimeMillis();
}
}
可以看到上面主要将静态文件分成了两类,每类文件的变更会触发应用的不同操作:
- redeployResouce,主要包含下面几个文件,以应用demo为例,如果一下文件有修改,那么会触发应用的重新部署
(1)resource/webapps/demo.war
(2)resource/webapps/demo
(3)resource/conf/Catalina/localhost/demo.xml
(4)resource/webapps/demo/META-INF/context.xml
(5)resource/conf/context.xml - reloadResourc,主要包含下面几个文件,以应用demo为例,如果一个文件由修改,那么会触发应用的重新加载
(1)resource/webapps/demo/WEB-INF/web.xml
(2)resource/conf/web.xml
热部署的执行
redeploy
上面可以看到当属于deployResource的文件发生修改之后,会将当前应用卸载掉,然后回到HostConfig的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 (!isServiced(app.name))
// 如果识别应用需要重新部署,那么会在这里将应用卸载掉,并且将应用从deployed map中移除
checkResources(app, false);
}
// Check for old versions of applications that can now be undeployed
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
// Hotdeploy applications
// 这里会遍历当前所有的应用,然后通过判断应用是否在deployed这个map中,来判断是否需要进行部署,避免对一个已经部署的应用重复部署
deployApps();
}
}
reload
HostConfig的reload最终会调用StandardContext的reload
public synchronized void reload() {
// Validate our current component state
if (!getState().isAvailable())
throw new IllegalStateException
(sm.getString("standardContext.notStarted", getName()));
if(log.isInfoEnabled())
log.info(sm.getString("standardContext.reloadingStarted",
getName()));
// Stop accepting requests temporarily.
setPaused(true);
try {
stop();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.stoppingContext", getName()), e);
}
try {
start();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.startingContext", getName()), e);
}
setPaused(false);
if(log.isInfoEnabled())
log.info(sm.getString("standardContext.reloadingCompleted",
getName()));
}