前言
很多朋友喜欢在Tomcat开启热加载,不管是在生产环境还是非生产环境,这样做合适吗,应该怎样看这个问题, 今天我们从Tomcat的源码中找到答案。另外,在分布式、微服务架构模式下越来越多的项目都偏向于嵌入式启动Tomcat,那今天也会来分析一下Spring Boot嵌入式Tomcat的源码。
本次进行源码解析的Tomcat版本是8.5.43,Spring Boot的版本是2.3.1
Tomcat热加载
我们在往Tomcat部署包的时候,为了省事和方便经常在Tomcat安装目录下的server.xml或context.xml文件中直接开启应用包上下文的热加载属性设置,这个设置在Tomcat官方文档中默认是关闭的。
我们一起看下tomcat对这个点的源码是怎样设计的。首先来看下,如何能追踪到这块的源码 ? 看下面的调用链
顺着上图的源码调用链路,我们看在WebappLoader类可看到如下的实现源码
@Override
public void backgroundProcess() {
/**
* 当设置reloadable=true并且应用包中的配置或文件有改变时
* 则执行如下代码
*/
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (context != null) {
/**
* 执行当前包应用上下文的重新加载
*/
context.reload();
}
} finally {
if (context != null && context.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
}
}
}
}
其中modified()方法的实现逻辑源码在WebappClassLoaderBase下
public boolean modified() {
/**
* 通过如下规则判定文件是否发生了改变
* (1).通文件的最后修改时间和上次缓存的修改时间对比,以此确定文件是否发生了改变
* (2).应用包或目录中的文件数量和上次缓存的数量进行对比,判定是否发生了改变;
*/
if (log.isDebugEnabled())
log.debug("modified()");
for (Entry, ResourceEntry> entry : resourceEntries.entrySet()) {
long cachedLastModified = entry.getValue().lastModified;
long lastModified = resources.getClassLoaderResource(
entry.getKey()).getLastModified();
/**
* 通过文件的最后修改时间和上次缓存的修改时间对比,以此确定文件是否发生了改变
*/
if (lastModified != cachedLastModified) {
if( log.isDebugEnabled() )
log.debug(sm.getString("webappClassLoader.resourceModified",
entry.getKey(),
new Date(cachedLastModified),
new Date(lastModified)));
return true;
}
}
// Check if JARs have been added or removed
WebResource[] jars = resources.listResources("/WEB-INF/lib");
// Filter out non-JAR resources
int jarCount = 0;
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
jarCount++;
Long recordedLastModified = jarModificationTimes.get(jar.getName());
/**
* 这里是新增了jar包
*/
if (recordedLastModified == null) {
// Jar has been added
log.info(sm.getString("webappClassLoader.jarsAdded",
resources.getContext().getName()));
return true;
}
/**
* 这里jar包也是如法炮制
*/
if (recordedLastModified.longValue() != jar.getLastModified()) {
// Jar has been changed
log.info(sm.getString("webappClassLoader.jarsModified",
resources.getContext().getName()));
return true;
}
}
}
/**
* 这里是检测到文件数量是否发生了改变
*/
if (jarCount < jarModificationTimes.size()){
log.info(sm.getString("webappClassLoader.jarsRemoved",
resources.getContext().getName()));
return true;
}
// No classes have been modified
return false;
}
以上的代码Tomcat会去扫描项目录下的每个应用jar包和每个文件(包括资源文件),当你的应用包比较大或文件较多时,会比较消耗服务器资源。如果看到这里,你还觉得没有太大的问题的话,那我们接着来看下面的重点:
StandardContext实现类下的startInternal()方法的这个位置
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
Throwable t = null;
String unexpectedDeathMessage = sm.getString(
"containerBase.backgroundProcess.unexpectedThreadDeath",
Thread.currentThread().getName());
try {
/**
* 性能瓶颈点1:若服务未停止,这里的循环就不停止
*/
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
/**
*性能瓶颈点2.这里面会执行明显的递归处理
*/
processChildren(ContainerBase.this);
}
}
} catch (RuntimeException|Error e) {
t = e;
throw e;
} finally {
if (!threadDone) {
log.error(unexpectedDeathMessage, t);
}
}
}
/**
* 递归处理容器任务和子任务
* @param container
*/
protected void processChildren(Container container) {
ClassLoader originalClassLoader = null;
try {
if (container instanceof Context) {
Loader loader = ((Context) container).getLoader();
// Loader will be null for FailedContext instances
if (loader == null) {
return;
}
// Ensure background processing for Contexts and Wrappers
// is performed under the web app's class loader
originalClassLoader = ((Context) container).bind(false, null);
}
container.backgroundProcess();
Container[] children = container.findChildren();
/**
* 这里:循环+递归
*/
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i]);
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("Exception invoking periodic operation: ", t);
} finally {
if (container instanceof Context) {
((Context) container).unbind(false, originalClassLoader);
}
}
}
}
看到上面的关键位置的代码就真相大白了。这里总结一下,也就是说Tomcat在处理和扫描后台jar和文件时有三大性能瓶颈点:
1.通过最后修改时间和包中文件数量确定是否需要重新加载应用,对jar和文件全量扫描,当包很大或文件较多的情况下,这里就成为了性能瓶颈点;
2.当把文件扫描打包成后台任务执行时,Tomcat会递归扫描容器和当前容器的子容器,当层级深度较大时,这里是明显的性能瓶颈点;
3.整个后台处理任务,只要服务不停止,一直会继续下去,也就是说会长期占用服务器的资源;
由于存在以上三大性能瓶颈, 在应用包文件频繁变更和reload情况下,服务器很容易发生频繁的Full GC和内存溢出的问题。再来看下官网的建议:
很明显,Tomcat官网推荐我们在开发环境这么干,但绝没推荐我们在生产环境也这么干!所以这个属性默认是不开启的。
到这里这个问题,大家应该都很明白了。下面来聊一下Tomcat嵌入式启动的话题
嵌入式Tomcat运行
在微服务架构大行其道的背景下,越来越多的项目偏向于内置Tomcat启动和运行的方式,SpringBoot就是一个典型的例子。这种方式至少有以下优点:
1.省去了我们对Tomcat单独的部署和繁琐的各种配置;
2.以嵌入式jar的形式和项目在一起打包部署更方便,同时又能兼顾解耦它们的工作,尤其式对于微服务化的虚拟容器更是尤为适合;
3.与单独部署tomcat服务相比,嵌入式部署更轻量级,体积更加的小,运行更轻快;
4.对于微服务化而言,它更契合一次打包到处部署的理念;
那如何玩Tomcat内置启动和运行呢?正确的姿势如下:
步骤一、在jar包依赖文件中引用嵌入式tomcat相关的jar包支持
org.apache.tomcat.embed
tomcat-embed-core
8.5.43
org.apache.tomcat.embed
tomcat-embed-jasper
8.5.43
步骤二、在启动和运行的引导包中加入以下代码
/**
* 在项目中简单的嵌入式运行Tomcat
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception{
//(1)实例化Tomcat类
Tomcat tomcat = new Tomcat();
//(2)设置应用工程包的url映射和包的加载路径
tomcat.addWebapp("/gis","D:\\workspace\\webgis");
//... 以上这种方式可以配置多个应用包,同时运行..
//(3)获取默认http1.1链接器,并绑定服务端口
tomcat.getConnector().setPort(8088);
//(4)Tomcat类加载、相关配置解析、初始化生命周期、各个部件、事件监听、状态监控和后台任务容器
tomcat.init();
//(5)运行各个组件、监听和后台任务
tomcat.start();
//(6)这里服务将阻塞直到,服务停止(持续监听、接收和响应)
tomcat.getServer().await();
}
以上仅为简单的Tomcat嵌入式启动和运行实现,不限于此,很多我们能在原始的tomcat中配置和优化的工作都可以在这种模式下完成;我们甚至可以动态生成和运行一个应用的所有servlet。
那么既然我们可以这样自定义做嵌入式Tomcat启动和运行, 那SpringBoot当然也可以。接下来我们就来看看Spring Boot是如何封装和嵌入Tomcat的功能的。
SpringBoot嵌入式Tomcat源码追踪
一、源码调用链路
首先我们如何能跟踪到关键代码 ?Spring为了兼容多种服务器内置启动和运行方式,把整个设计封装得很深, 我们来看一下整理的调用链路图
二、相关源码追踪
下面具体来看看,关键的追踪环节代码
以下是SpringApplication类中的关键跟踪片段
继续跟踪到关键跟踪位置AbstractApplicationContext抽象骨架类的refresh()方法
这里如上截图,如果你使用的版本是Spring Boot 1.0的版本这个地方的跟踪入口是this.onRefresh()方法,Spring Boot2.0及以上版本跟踪入口是this.finishRefresh() 方法;
继续跟进到DefaultLifecycleProcessor#onRefresh实现类的onRefresh(0方法中
继续追踪到WebServerStartStopLifecycle实现类的start()方法
继续跟踪WebServerManager内部类start()方法
从以上位置源代码我们明显看出,SpringBoot在2的版本不仅支持Tomcat内置嵌入式,而且同时也支持Jetty、Netty和Undertow等服务器和模式的嵌入式运行。
在以上截图位置我们是不是看到了Tomcat原生接口和API的身影,至此Tomcat嵌入式启动和运行在Spring Boot中终于露出了真容!在Spring Boot的1.0级版本中,最后嵌入Tomcat的API代码逻辑更简单和直接!大家有兴趣可以多跟踪和比较一下在不同版本中它们的设计,然后思考一下Spring架构师大佬的想法和初衷。
总结
今天先解决这两大问题,关于Spring体系的源码(包括Spring Boot)整体设计确实比较宏达、封装业务比较多、层级相对较深。越新的版本越能明显看到这些特点。要做设计和真正地写出高质量的代码,建议多读源码,多了解大牛和架构师的设计以及设计中用到的高级技术内容和场景,提升自己的设计和代码能力!后面会介绍更多经典项目源码篇章,请继续关注!