热部署
在Tomcat中可以通过Host标签设置热部署,当 autoDeploy为true时,在运行中的Tomcat中丢入一个war包,那么Tomcat不需要重启就可以自动加载该war包。
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true" >
Tomcat的容器中都包含有 backgroundProcessorDelay 属性和 backgroundProcess方法,默认的实现是,在每个容器启动时,当backgroundProcessorDelay大于1时(单位是秒),则会周期性的执行当前容器及所有子容器的backgroundProcess方法。
ContainerBase:
protected synchronized void startInternal() throws LifecycleException {
//...省略
threadStart();
}
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:
public void run() {
Throwable t = null;
String unexpectedDeathMessage = sm.getString(
"containerBase.backgroundProcess.unexpectedThreadDeath",
Thread.currentThread().getName());
try {
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();
}
//note 执行所有子容器的backgroundProcessorDelay方法
processChildren(parent, cl);
}
}
} catch (RuntimeException e) {
t = e;
throw e;
} catch (Error e) {
t = e;
throw e;
} finally {
if (!threadDone) {
log.error(unexpectedDeathMessage, t);
}
}
}
protected void processChildren(Container container, ClassLoader cl) {
try {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
//执行当前容器的backgroundProcess
container.backgroundProcess();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
//在执行子容器的backgroundProcess
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i], cl);
}
}
}
backgroundProcessorDelay的设置在Engine标签中,因为默认实现包含所有子容器,所有在最顶层的容器上配置一个即可。
<Engine name="Catalina" defaultHost="localhost" backgroundProcessorDelay="20">
我们知道war包的管理应该是属于它的上一层容器Host,在Host中并没有重写backgroundProcess方法。
StandardHost:
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);
}
Host父类的实现也没有包含自动部署相关的内容,那么只能是在最后通过事件的机制去实现了,最后会发送一个Host的Lifecycle.PERIODIC_EVENT,Host对应的监听器是HostConfig。
HostConfig:
public void lifecycleEvent(LifecycleEvent event) {
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;
}
//当Host的周期时间到达时,即Host中的backgroundProcess方法被调用时触发
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();
}
}
在HostConfig中,存在Host周期性事件的监听处理。
HostConfig:
protected void check() {
//Host的属性,必须设置server.xml中的Host标签autoDeploy属性为true才进行热部署检测
if (host.getAutoDeploy()) {
//检测已经部署的是否存在修改,存在删除应用文件夹和卸载Context(通过判断文件的修改时间与上次部署时是否一致),删除之后,下面会当做新增的重新部署
DeployedApplication[] apps =
deployed.values().toArray(new DeployedApplication[0]);
for (int i = 0; i < apps.length; i++) {
if (!isServiced(apps[i].name))
checkResources(apps[i], false);
}
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
//主要是部署新增的应用
deployApps();
}
}
protected synchronized void checkResources(DeployedApplication app,
boolean skipFileModificationResolutionCheck) {
String[] resources =
app.redeployResources.keySet().toArray(new String[0]);
long currentTimeWithResolutionOffset =
System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS;
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 && (!host.getAutoDeploy() ||
resource.lastModified() < currentTimeWithResolutionOffset ||
skipFileModificationResolutionCheck)) {
//...省略
//从已加载的容器中,卸载Context应用
undeploy(app);
//如果是解压为文件夹的,则删除
deleteRedeployResources(app, resources, i, false);
return;
}
//... 省略
}
}
protected void deployApps() {
File appBase = appBase();
File configBase = configBase();
//获取Host中appBase下所有的文件
String[] filteredAppPaths = filterAppPaths(appBase.list());
//部署xml
deployDescriptors(configBase, configBase.list());
//部署War包
deployWARs(appBase, filteredAppPaths);
//部署文件夹,也就是静态资源
deployDirectories(appBase, filteredAppPaths);
}
protected void deployWARs(File appBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<Future<?>>();
for (int i = 0; i < files.length; i++) {
//只处理war包,别的不管
if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File war = new File(appBase, files[i]);
if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
war.isFile() && !invalidWars.contains(files[i]) ) {
ContextName cn = new ContextName(files[i], true);
if (isServiced(cn.getName())) {
continue;
}
//是否已经部署过了
if (deploymentExists(cn.getName())) {
DeployedApplication app = deployed.get(cn.getName());
boolean unpackWAR = unpackWARs;
if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
}
if (!unpackWAR && app != null) {
// Need to check for a directory that should not be
// there
File dir = new File(appBase, cn.getBaseName());
if (dir.exists()) {
if (!app.loggedDirWarning) {
log.warn(sm.getString(
"hostConfig.deployWar.hiddenDir",
dir.getAbsoluteFile(),
war.getAbsoluteFile()));
app.loggedDirWarning = true;
}
} else {
app.loggedDirWarning = false;
}
}
//已经部署过了则跳过
continue;
}
//检测路径是否正确
if (!validateContextPath(appBase, cn.getBaseName())) {
log.error(sm.getString(
"hostConfig.illegalWarName", files[i]));
invalidWars.add(files[i]);
continue;
}
//没有部署过,则提交到线程池中异步处理
results.add(es.submit(new DeployWar(this, cn, war)));
}
}
//等待所有war包部署完成
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployWar.threaded.error"), e);
}
}
}
在HostConfig中,会周期性的检测部署的文件(主要是war包),是否发生了变化(通过与上次部署时的文件修改时间对比),发生变化则卸载应用并删除部署的应用文件夹;然后继续判断该war包是否已部署(对于修改的已经被删除,此时不算已部署),如果未部署,则通过线程池异步提交部署。
DeployWar:
public void run() {
config.deployWAR(cn, war);
}
HostConfig:
protected void deployWAR(ContextName cn, File war) {
//...省略了对war包的解析和获取conext.xml
Context context = null;
//创建出该war包的Conext容器
context = (Context) Class.forName(contextClass).newInstance();
//添加到子节点,然后启动容器
host.addChild(context);
//...省略
//添加到已部署应用目录中
deployed.put(cn.getName(), deployedApp);
}
StandardHost:
public void addChild(Container child) {
child.addLifecycleListener(new MemoryLeakTrackingListener());
if (!(child instanceof Context))
throw new IllegalArgumentException
(sm.getString("standardHost.notContext"));
super.addChild(child);
}
private void addChildInternal(Container child) {
//...省略
try {
if ((getState().isAvailable() ||
LifecycleState.STARTING_PREP.equals(getState())) &&
startChildren) {
//启动Conext容器
child.start();
}
} catch (LifecycleException e) {
log.error("ContainerBase.addChild: start: ", e);
throw new IllegalStateException("ContainerBase.addChild: start: " + e);
} finally {
fireContainerEvent(ADD_CHILD_EVENT, child);
}
}
在部署war中,异步创建了Conext应用容器并启动该容器,此时还为解压war中的内容,则最终对war包的处理应该是Context的应用容器中。
由于内容太长,此处省略Context启动的部分内容,在Context启动之前,会发送事件Lifecycle.BEFORE_START_EVENT至监听器ContextConfig中。
ContextConfig:
public void lifecycleEvent(LifecycleEvent event) {
try {
context = (Context) event.getLifecycle();
} catch (ClassCastException e) {
return;
}
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
//启动之前处理
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}
}
protected synchronized void beforeStart() {
try {
fixDocBase();
} catch (IOException e) {
}
antiLocking();
}
protected void fixDocBase()
throws IOException {
Host host = (Host) context.getParent();
//拿到上层目录
String appBase = host.getAppBase();
File canonicalAppBase = new File(appBase);
if (canonicalAppBase.isAbsolute()) {
canonicalAppBase = canonicalAppBase.getCanonicalFile();
} else {
canonicalAppBase =
new File(getBaseDir(), appBase)
.getCanonicalFile();
}
//war的文件夹名(即war包取掉后缀的名字)
String docBase = context.getDocBase();
if (docBase == null) {
// Trying to guess the docBase according to the path
String path = context.getPath();
if (path == null) {
return;
}
ContextName cn = new ContextName(path, context.getWebappVersion());
docBase = cn.getBaseName();
}
File file = new File(docBase);
//如果不是绝对路径,则转换为具体路径
if (!file.isAbsolute()) {
docBase = (new File(canonicalAppBase, docBase)).getPath();
} else {
docBase = file.getCanonicalPath();
}
file = new File(docBase);
String origDocBase = docBase;
ContextName cn = new ContextName(context.getPath(),
context.getWebappVersion());
String pathName = cn.getBaseName();
//是否解压war包
boolean unpackWARs = true;
if (host instanceof StandardHost) {
unpackWARs = ((StandardHost) host).isUnpackWARs();
if (unpackWARs && context instanceof StandardContext) {
unpackWARs = ((StandardContext) context).getUnpackWAR();
}
}
if (docBase.toLowerCase(Locale.ENGLISH).endsWith(".war") && !file.isDirectory()) {
if (unpackWARs) {
//加载war包
URL war = new URL("jar:" + (new File(docBase)).toURI().toURL() + "!/");
//解压war包到应用文件夹中
docBase = ExpandWar.expand(host, war, pathName);
file = new File(docBase);
docBase = file.getCanonicalPath();
if (context instanceof StandardContext) {
((StandardContext) context).setOriginalDocBase(origDocBase);
}
} else {
//不解压则校验war包即可(URL类加载器可以加载URL的war包)
URL war =
new URL("jar:" + (new File (docBase)).toURI().toURL() + "!/");
ExpandWar.validate(host, war, pathName);
}
}
}
//...省略
}
在ContextConfig中,监听了Context的启动,如果设置了解压war包,则在此处解压war包,完成最终的步骤。
热部署总结:
即Tomcat中存在一个定时任务,该定时任务会去扫描Host下的war包文件,然后文件 修改时间来判断是否存在修改,存在修改则删除卸载Context应用容器;并且当存在新增 时,则会异步的创建该Context应用容器,如果设置了解压最后还会解压成文件夹,从而完 成了热部署机制的实现。
热加载
在Tomcat中,可以通过Context标签设置热加载,reloadable设置为true时生效,一般调试时才使用,对性能消耗很大。
<Context docBase="app-test" reloadable="true" path=""/>
热加载是属于Conext应用容器的,也是在定时任务的backgroundProcess方法中实现,毕竟线程是属于重量级资源,能在一个线程实现就少创建一个线程。
StandardContext:
public void backgroundProcess() {
//...省略
if (loader != null) {
try {
//热加载war包下的所有资源(WEB-INF/classes、WEB-INF/lib)
loader.backgroundProcess();
} catch (Exception e) {
}
}
//...省略
}
WebappLoader:
public void backgroundProcess() {
//设置了server.xml中的Context中的reloadable为true时并且发生变化时才会重新加载
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (container instanceof StandardContext) {
//重新加载容器
((StandardContext) container).reload();
}
} finally {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
}
} else {
closeJARs(false);
}
}
public boolean modified() {
return classLoader != null ? classLoader.modified() : false ;
}
WebappClassLoader:
public boolean modified() {
int length = paths.length;
//最后一次文件修改时间的列表
int length2 = lastModifiedDates.length;
//如果当前文件夹内容多了,则使用久的文件夹长度遍历(避免数组越界)
if (length > length2)
length = length2;
//遍历当前war包文件夹的所有文件
for (int i = 0; i < length; i++) {
try {
//拿到当前文件的修改时间
long lastModified =
((ResourceAttributes) resources.getAttributes(paths[i]))
.getLastModified();
//和该文件上一次的修改时间对比,存在一个不同则为已修改
if (lastModified != lastModifiedDates[i]) {
return (true);
}
}
}
length = jarNames.length;
//判断war包是否被删除
if (getJarPath() != null) {
try {
//获取war包中文件列表
NamingEnumeration<Binding> enumeration =
resources.listBindings(getJarPath());
int i = 0;
while (enumeration.hasMoreElements() && (i < length)) {
NameClassPair ncPair = enumeration.nextElement();
String name = ncPair.getName();
//不是jar包则跳过
if (!name.endsWith(".jar"))
continue;
//如果jar包列表对不上,则表示jar包发生变化
if (!name.equals(jarNames[i])) {
return (true);
}
i++;
}
if (enumeration.hasMoreElements()) {
while (enumeration.hasMoreElements()) {
NameClassPair ncPair = enumeration.nextElement();
String name = ncPair.getName();
//添加了新的jar包也是已修改
if (name.endsWith(".jar")) {
return (true);
}
}
} else if (i < jarNames.length) {
return (true);
}
}
//...省略
}
return (false);
}
StandardContext:
public synchronized void reload() {
if (!getState().isAvailable())
throw new IllegalStateException
(sm.getString("standardContext.notStarted", getName()));
setPaused(true);
//停止容器
stop();
//再启动容器
start();
setPaused(false);
}
protected synchronized void stopInternal() throws LifecycleException {
//... 其它内容省略
//拿到旧的类加载器
ClassLoader old = bindThread();
//解绑类加载器
unbindThread(old);
if ((loader != null) && (loader instanceof Lifecycle)) {
//...停止类加载器,即清除所有的引用,下次加载类时则会重新加载
((Lifecycle) loader).stop();
}
}
在上述代码中,我们可以看到Tomcat是通过对比classes和lib文件夹中的jar包来判断是否发生了变化,主要是判断classes文件夹是否减少、或者发生修改;判断lib文件夹下的jar包是否新增、减少;如果发生了上述情况则属于修改,然后重新加载当前Context应用容器,并清理当前Context的类加载器中的所有引用,下次加载类时则重新加载,从而实现了热加载机制。
热加载总结:
在Tomcat的后台定时任务中,如果开启了热加载,会检测classes文件夹下和lib下的jar包的内容是否发生了变化,如果发生了变化则会重新加载Context应用容器,清除对应的类加载器的引用,从而实现热加载,因为会遍历classes下的所有文件,所以很消耗资源,线上环境最好别开启,开发时使用即可。