Tomcat源碼分析 - Tomcat熱加載的坑分析和嵌入式啓動運行Tomcat解析

前言web

不少朋友喜歡在Tomcat開啓熱加載,不論是在生產環境仍是非生產環境,這樣作合適嗎,應該怎樣看這個問題, 今天咱們從Tomcat的源碼中找到答案。另外,在分佈式、微服務架構模式下愈來愈多的項目都偏向於嵌入式啓動Tomcat,那今天也會來分析一下Spring Boot嵌入式Tomcat的源碼。apache

本次進行源碼解析的Tomcat版本是8.5.43,Spring Boot的版本是2.3.1
緩存


Tomcat熱加載tomcat

咱們在往Tomcat部署包的時候,爲了省事和方便常常在Tomcat安裝目錄下的server.xml或context.xml文件中直接開啓應用包上下文的熱加載屬性設置,這個設置在Tomcat官方文檔中默認是關閉的
服務器

image.png

咱們一塊兒看下tomcat對這個點的源碼是怎樣設計的。首先來看下,如何能追蹤到這塊的源碼 ? 看下面的調用鏈架構

image.png

順着上圖的源碼調用鏈路,咱們看在WebappLoader類可看到以下的實現源碼app

@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下webapp

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包和每一個文件(包括資源文件),當你的應用包比較大或文件較多時,會比較消耗服務器資源。若是看到這裏,你還以爲沒有太大的問題的話,那咱們接着來看下面的重點:
jsp

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內置啓動和運行呢?正確的姿式以下:

步驟1、在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>

步驟2、在啓動和運行的引導包中加入如下代碼

/**
* 在項目中簡單的嵌入式運行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源碼追蹤

1、源碼調用鏈路

首先咱們如何能跟蹤到關鍵代碼 ?Spring爲了兼容多種服務器內置啓動和運行方式,把整個設計封裝得很深, 咱們來看一下整理的調用鏈路圖

image.png

2、相關源碼追蹤

下面具體來看看,關鍵的追蹤環節代碼

如下是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)總體設計確實比較宏達、封裝業務比較多、層級相對較深。越新的版本越能明顯看到這些特色。要作設計和真正地寫出高質量的代碼,建議多讀源碼,多瞭解大牛和架構師的設計以及設計中用到的高級技術內容和場景,提高本身的設計和代碼能力!後面會介紹更多經典項目源碼篇章,請繼續關注!

相關文章
相關標籤/搜索