Tomcat源码分析 - Tomcat热加载的坑分析和嵌入式启动运行Tomcat解析

前言

很多朋友喜欢在Tomcat开启热加载,不管是在生产环境还是非生产环境,这样做合适吗,应该怎样看这个问题, 今天我们从Tomcat的源码中找到答案。另外,在分布式、微服务架构模式下越来越多的项目都偏向于嵌入式启动Tomcat,那今天也会来分析一下Spring Boot嵌入式Tomcat的源码。

本次进行源码解析的Tomcat版本是8.5.43,Spring Boot的版本是2.3.1


Tomcat热加载

我们在往Tomcat部署包的时候,为了省事和方便经常在Tomcat安装目录下的server.xml或context.xml文件中直接开启应用包上下文的热加载属性设置,这个设置在Tomcat官方文档中默认是关闭的

image.png

我们一起看下tomcat对这个点的源码是怎样设计的。首先来看下,如何能追踪到这块的源码 ? 看下面的调用链

image.png

顺着上图的源码调用链路,我们看在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<String,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()方法的这个位置

image.png

image.png

image.png

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和内存溢出的问题。再来看下官网的建议:

image.png

很明显,Tomcat官网推荐我们在开发环境这么干,但绝没推荐我们在生产环境也这么干!所以这个属性默认是不开启的。

到这里这个问题,大家应该都很明白了。下面来聊一下Tomcat嵌入式启动的话题


嵌入式Tomcat运行

在微服务架构大行其道的背景下,越来越多的项目偏向于内置Tomcat启动和运行的方式,SpringBoot就是一个典型的例子。这种方式至少有以下优点:

1.省去了我们对Tomcat单独的部署和繁琐的各种配置;

2.以嵌入式jar的形式和项目在一起打包部署更方便,同时又能兼顾解耦它们的工作,尤其式对于微服务化的虚拟容器更是尤为适合;

3.与单独部署tomcat服务相比,嵌入式部署更轻量级,体积更加的小,运行更轻快;

4.对于微服务化而言,它更契合一次打包到处部署的理念;

那如何玩Tomcat内置启动和运行呢?正确的姿势如下:

步骤一、在jar包依赖文件中引用嵌入式tomcat相关的jar包支持

<!--嵌入式Tomcat内核包 -->
<dependency>
 <groupId>
org.apache.tomcat.embed</groupId>
 <artifactId>
tomcat-embed-core</artifactId>
 <version>
8.5.43</version>
</dependency>
<!--嵌入式Tomcat的JSP支持包(这个如果不要求支持jsp可不引用)-->
<dependency>
 <groupId>
org.apache.tomcat.embed</groupId>
 <artifactId>
tomcat-embed-jasper</artifactId>
 <version>
8.5.43</version>
</dependency>

步骤二、在启动和运行的引导包中加入以下代码

/**
* 在项目中简单的嵌入式运行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为了兼容多种服务器内置启动和运行方式,把整个设计封装得很深, 我们来看一下整理的调用链路图

image.png

二、相关源码追踪

下面具体来看看,关键的追踪环节代码

以下是SpringApplication类中的关键跟踪片段

image.png

image.png

image.png

image.png

继续跟踪到关键跟踪位置AbstractApplicationContext抽象骨架类的refresh()方法

image.png

这里如上截图,如果你使用的版本是Spring  Boot 1.0的版本这个地方的跟踪入口是this.onRefresh()方法,Spring Boot2.0及以上版本跟踪入口是this.finishRefresh() 方法;

继续跟进到DefaultLifecycleProcessor#onRefresh实现类的onRefresh(0方法中

image.png

image.png

image.png

image.png

image.png

继续追踪到WebServerStartStopLifecycle实现类的start()方法

image.png

继续跟踪WebServerManager内部类start()方法

image.png

image.png

从以上位置源代码我们明显看出,SpringBoot在2的版本不仅支持Tomcat内置嵌入式,而且同时也支持Jetty、Netty和Undertow等服务器和模式的嵌入式运行。

image.png

在以上截图位置我们是不是看到了Tomcat原生接口和API的身影,至此Tomcat嵌入式启动和运行在Spring Boot中终于露出了真容!在Spring Boot的1.0级版本中,最后嵌入Tomcat的API代码逻辑更简单和直接!大家有兴趣可以多跟踪和比较一下在不同版本中它们的设计,然后思考一下Spring架构师大佬的想法和初衷。

总结

今天先解决这两大问题,关于Spring体系的源码(包括Spring Boot)整体设计确实比较宏达、封装业务比较多、层级相对较深。越新的版本越能明显看到这些特点。要做设计和真正地写出高质量的代码,建议多读源码,多了解大牛和架构师的设计以及设计中用到的高级技术内容和场景,提升自己的设计和代码能力!后面会介绍更多经典项目源码篇章,请继续关注!


猜你喜欢

转载自blog.51cto.com/14815984/2515824
今日推荐