前言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官方文檔中默認是關閉的。
服務器
咱們一塊兒看下tomcat對這個點的源碼是怎樣設計的。首先來看下,如何能追蹤到這塊的源碼 ? 看下面的調用鏈架構
順着上圖的源碼調用鏈路,咱們看在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()方法的這個位置分佈式
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內置啓動和運行呢?正確的姿式以下:
步驟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爲了兼容多種服務器內置啓動和運行方式,把整個設計封裝得很深, 咱們來看一下整理的調用鏈路圖
2、相關源碼追蹤
下面具體來看看,關鍵的追蹤環節代碼
如下是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)總體設計確實比較宏達、封裝業務比較多、層級相對較深。越新的版本越能明顯看到這些特色。要作設計和真正地寫出高質量的代碼,建議多讀源碼,多瞭解大牛和架構師的設計以及設計中用到的高級技術內容和場景,提高本身的設計和代碼能力!後面會介紹更多經典項目源碼篇章,請繼續關注!