前言
单独部署的tomcat服务器在运行中,当开发人员或者运维人员将开发工程的war包部署到服务目录时,服务器会自动进行war包的解包和类的加载运行,整个spring mvc项目就能在服务器上工作了。本文作为tomcat+spring mvc原理系列tomcat部分的原理的最后一讲,就主要分析一下tomcat的这个功能是如何实现的。需要注意的是,这个功能在spring boot的嵌入式tomcat(tomcat-embed)中并没有起作用,因为tomcat-embed只需要针对spring boot的一个spring mvc项目进行处理,是jar包加载运行,并不涉及到多个war包的热部署。
监控的启动原理
在原理(二)中我们讲了针对容器的状态名和状态事件监听,但是我们没有讲具体的应用。这里补充一下,这个war包的解压缩和部署就用到了原理(二)提到的容器状态监听的原理。
整个功能的实现是在HostConfig类中,HostConfig类实现了LifecycleListener。顾名思义,我们也能猜测出HostConfig是Host容器的状态监听器。其实,Engine也有响应的状态监听器,但是里面并没有包括什么作用比较大的代码,只是日志相关的打印。为什么会选中Host这个层次做WAR解压和部署相关的工作?答案在前面几篇的文章中有相应的表述。主要是因为
Context和Wrapper是动态添加的,我们在tomcat的指定目录下每添加一个war包,tomcat加载war包时,就可以添加Context和Servlet。原理(一)
状态监听
public class HostConfig implements LifecycleListener {
......
@Override
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();
}
}
......
}
根据状态监听器的基本运作原理(原理(二)),HostConfig是需要实现lifecycleEvent方法的。每个容器状态变化的时候,就会调用相应的监听器中的lifecycleEvent方法来相应相应的变化。可以看到HostConfig主要处理了Host的四种状态,PERIODIC_EVENT、BEFORE_START_EVENT、START_EVENT、STOP_EVENT。
- check()方法响应PERIODIC_EVENT周期性检查事件,主要是检查资源是否被更改,如果被更改就会重新解压、重新加载。
- beforeStart()响应BEFORE_START_EVENT状态,主要是设置资源目录等相关配置,为START_EVENT做准备。
- start()响应START_EVENT状态,这里就是启动是读取配置、解压包、重新加载资源的操作。下面也主要以这个方法为例。
- stop()大家都也都懂的= =。
部署项目
在start()主要处理了注册和类本身状态的设置,最后调用了HostConfig的deployApps方法。
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
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);
}
首先是deployDescriptors()方法的作用,上面也有注释,是用来获取xml文件中配置。这个方法主要讲各个配置文件目录中的xml文件的内容读取出来,使配置的内容最终设置到容器中(比如Context的配置),这样配置内容就生效了。deployDirectories()部署扩展的文件夹中的内容,比如一些扩展的webapp文件,其中xml配置文件也需要加载、web应用也要启动,和deployWARS方法的内容基本一致,这里不做具体介绍。
deployWARs()这个方法的名字就很明显了。这个方法的第一步是筛选出目录中有效的war文件,然后进行部署。
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]);
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;
}
//省略内容:筛选掉不合格的直接continue
......
results.add(es.submit(new DeployWar(this, cn, war)));
}
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployWar.threaded.error"), e);
}
}
}
因为对于多个服务而言,war包可能不止一个,所以用了一个for循环。这里for循环中使用了多线程加载war包,最后是在循环外使用result.get()进行阻塞,保证所有war包加载完之后才能继续。
循环里面跳过了META-INF目录和WEB-INF目录,因为这两个目录都是配置文件和其他资源文件目录,还有很多这种文件筛选的逻辑我没贴出来。
最后是用DeployWar类实现多线程,其实这个类也是一个工具人,里面调用的逻辑只是使用HostConfig类的deployWAR()方法部署单个war包。
@Override
public void run() {
//config为Hostconfig
config.deployWAR(cn, war);
}
deployWar里面做的事情就非常多了,代码也比较长。主要实现了
- war包解析,解压缩
- 配置文件读取
- Context类加载,类配置。可以是标准StandardContext,也可以是war中自己定义的。
- war中自定义的状态监听器的加载。
- Host使用addChild将context实例加入到tomcat的Host配置中。
- 对解压缩的war包进行监控,发生变化就会触发上面说到的check()。
当然里面还有其他内容,就不一一列举了,感兴趣的同学可以自行阅读。当Host调用addChild之后,调用的是一切又回到了我们熟悉的原理(二)中介绍的内容:
需要特别注意的是,Container启动子容器的时候不一定是通过init()或者start()中调用相应子容器的生命周期函数。在容器的addChild方法中,也会调用子容器的start()方法,初始化加载和启动子容器,比如host的addChild(context)方法会调用context的start方法。
这个war包配置的Context、Wrapper容器的start()等生命周期方法会被调用,最后这个spring mvc项目部署完成,就可以正常工作了。
比较有意思的是,代码里显示war包本身也是一种jar包格式,使用了JarFile类进行解析。这就是代码里的挂羊头卖狗肉吧。
本系列文章:
tomcat + spring mvc原理(一):tomcat原理综述和静态架构
tomcat + spring mvc原理(二):tomcat容器初始化加载和启动
tomcat + spring mvc原理(三):tomcat网络请求的监控与处理1
tomcat + spring mvc原理(四):tomcat网络请求的监控与处理2
tomcat + spring mvc原理(五):tomcat Filter组件实现原理