使用eclipse和tomcat做web应用开发的童鞋都知道,有一个很强大的功能就是--我们的文件改变的时候Tomcat会自动的重新部署应用,给我们的开发调试带来了很大的便利,但是它是怎么实现的呢?我们下面来关注一下原理和Tomcat的处理办法。
我们都知道,我们修改了文件,并且保存了它,这个文件的修改时间就会被改变,当然了我们的应用处理逻辑也可能发生了变化,这时候我们就应该重新发布一下应用,让它切换到最新的逻辑上面。那么我们现在我们可以归结出Tomcat需要做两件事:
1.检测部署的应用文件是否被修改。
2.如果修改了那么需要重新部署应用。
关于如何检测文件状态的变化可以参考《使用commons-vfs监听文件系统》 使用了里面的第二种方法。可以看Tomcat的源代码,这里使用的是8.X:
这里的检测是在org.apache.catalina.startup.HostConfig中完成的:
首先添加要观察的文件:
/**
* Add watched resources to the specified Context.
* @param app HostConfig deployed app
* @param docBase web app docBase
* @param context web application context
*/
protected void addWatchedResources(DeployedApplication app, String docBase,
Context context) {
// FIXME: Feature idea. Add support for patterns (ex: WEB-INF/*,
// WEB-INF/*.xml), where we would only check if at least one
// resource is newer than app.timestamp
File docBaseFile = null;
if (docBase != null) {
docBaseFile = new File(docBase);
if (!docBaseFile.isAbsolute()) {
docBaseFile = new File(host.getAppBaseFile(), docBase);
}
}
String[] watchedResources = context.findWatchedResources();
for (int i = 0; i < watchedResources.length; i++) {
File resource = new File(watchedResources[i]);
if (!resource.isAbsolute()) {
if (docBase != null) {
resource = new File(docBaseFile, watchedResources[i]);
} else {
if(log.isDebugEnabled())
log.debug("Ignoring non-existent WatchedResource '" +
resource.getAbsolutePath() + "'");
continue;
}
}
if(log.isDebugEnabled())
log.debug("Watching WatchedResource '" +
resource.getAbsolutePath() + "'");
app.reloadResources.put(resource.getAbsolutePath(),
Long.valueOf(resource.lastModified()));
}
}
其次是检测文件时间戳:
/**
* Check resources for redeployment and reloading.
*/
protected synchronized void checkResources(DeployedApplication app) {
String[] resources =
app.redeployResources.keySet().toArray(new String[0]);
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) {
if (resource.lastModified() > lastModified) {
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")) {
// 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);
}
ExpandWar.delete(docBaseFile);
// Reset the docBase to trigger re-expansion of the
// WAR
context.setDocBase(resource.getAbsolutePath());
}
reload(app);
// 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)
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;
}
if (lastModified == 0L) {
continue;
}
// Undeploy application
undeploy(app);
deleteRedeployResources(app, resources, i, true);
return;
}
}
resources = app.reloadResources.keySet().toArray(new String[0]);
boolean update = false;
for (int i = 0; i < resources.length; i++) {
File resource = new File(resources[i]);
if (log.isDebugEnabled()) {
log.debug("Checking context[" + app.name + "] reload resource " + resource);
}
long lastModified = app.reloadResources.get(resources[i]).longValue();
if (resource.lastModified() != lastModified || update) {
if (!update) {
// Reload application
reload(app);
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(resources[i],
Long.valueOf(resource.lastModified()));
}
app.timestamp = System.currentTimeMillis();
}
}
可以看到它使用的是File的lastModified()方法。
最后这个检测过程是什么时候触发呢?我们知道配置
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
Host的autoDeploy=true的时候才会自动发布,就在这里体现。
/**
* 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();
}
}
我现在告诉你这个check()是周期性执行的话,你肯定会问如何周期性执行,接着看:
可以看到一个周期性事件触发了这个过程。那么谁会周期性的去发送这个周期性事件呢?
org.apache.catalina.core.StandardEngine 就是我们Engine标签的实际代表者,这个类继承了org.apache.catalina.core.ContainerBase,其中有一段:
在容器启动的时候会创建一个守护线程去周期性的发出周期性事件
那么肯定还有最后一个问题,这个周期性事件的间隔是多少,怎么设置?
默认是10秒 如果我们想改变这个值怎么办?
打开你的server.xml 然后在Engine标签中添加属性
<Engine name="Catalina" defaultHost="localhost" backgroundProcessorDelay="20">
然后我们改动一下代码:
然后我们可以看到改变了周期性事件的间隔。
目前这个周期性事件是从engine触发的,如果我们想让其他容器触发怎么办?
对应的标签添加属性backgroundProcessorDelay。只要继承了org.apache.catalina.core.ContainerBase这个容器的对应标签都可以出发出周期性事件。