天天看點

spring boot應用啟動原理分析

在spring boot裡,很吸引人的一個特性是可以直接把應用打包成為一個jar/war,然後這個jar/war是可以直接啟動的,不需要另外配置一個web server。

如果之前沒有使用過spring boot可以通過下面的demo來感受下。

下面以這個工程為例,示範如何啟動spring boot項目:

如果使用的ide是spring sts或者idea,可以通過向導來建立spring boot項目。

也可以參考官方教程:

http://docs.spring.io/spring-boot/docs/current-snapshot/reference/htmlsingle/#getting-started-first-application

剛開始接觸spring boot時,通常會有這些疑問

spring boot如何啟動的?

spring boot embed tomcat是如何工作的? 靜态檔案,jsp,網頁模闆這些是如何加載到的?

下面來分析spring boot是如何做到的。

maven打包之後,會生成兩個jar檔案:

其中demo-0.0.1-snapshot.jar.original是預設的maven-jar-plugin生成的包。

demo-0.0.1-snapshot.jar是spring boot maven插件生成的jar包,裡面包含了應用的依賴,以及spring boot相關的類。下面稱之為fat jar。

先來檢視spring boot打好的包的目錄結構(不重要的省略掉):

依次來看下這些内容。

可以看到有main-class是org.springframework.boot.loader.jarlauncher ,這個是jar啟動的main函數。

還有一個start-class是com.example.springbootdemoapplication,這個是我們應用自己的main函數。

這下面放的是應用的.class檔案。

這裡存放的是應用的maven依賴的jar封包件。

比如spring-beans,spring-mvc等jar。

這下面存放的是spring boot loader的.class檔案。

archive即歸檔檔案,這個概念在linux下比較常見

通常就是一個tar/zip格式的壓縮包

jar是zip格式

在spring boot裡,抽象出了archive的概念。

一個archive可以是一個jar(jarfilearchive),也可以是一個檔案目錄(explodedarchive)。可以了解為spring boot抽象出來的統一通路資源的層。

上面的demo-0.0.1-snapshot.jar 是一個archive,然後demo-0.0.1-snapshot.jar裡的/lib目錄下面的每一個jar包,也是一個archive。

可以看到archive有一個自己的url,比如:

還有一個getnestedarchives函數,這個實際傳回的是demo-0.0.1-snapshot.jar/lib下面的jar的archive清單。它們的url是:

從manifest.mf可以看到main函數是jarlauncher,下面來分析它的工作流程。

jarlauncher類的繼承結構是:

jarlauncher先找到自己所在的jar,即demo-0.0.1-snapshot.jar的路徑,然後建立了一個archive。

下面的代碼展示了如何從一個類找到它的加載的位置的技巧:

jarlauncher建立好archive之後,通過getnestedarchives函數來擷取到demo-0.0.1-snapshot.jar/lib下面的所有jar檔案,并建立為list。

注意上面提到,archive都是有自己的url的。

擷取到這些archive的url之後,也就獲得了一個url[]數組,用這個來構造一個自定義的classloader:launchedurlclassloader。

建立好classloader之後,再從manifest.mf裡讀取到start-class,即com.example.springbootdemoapplication,然後建立一個新的線程來啟動應用的main函數。

launchedurlclassloader和普通的urlclassloader的不同之處是,它提供了從archive裡加載.class的能力。

結合archive提供的getentries函數,就可以擷取到archive裡的resource。當然裡面的細節還是很多的,下面再描述。

看到這裡,可以總結下spring boot應用的啟動流程:

spring boot應用打包之後,生成一個fat jar,裡面包含了應用依賴的jar包,還有spring boot loader相關的類

fat jar的啟動main函數是jarlauncher,它負責建立一個launchedurlclassloader來加載/lib下面的jar,并以一個新線程啟動應用的main函數。

代碼位址:https://github.com/spring-projects/spring-boot/tree/master/spring-boot-tools/spring-boot-loader

spring boot能做到以一個fat jar來啟動,最重要的一點是它實作了jar in jar的加載方式。

jdk原始的jarfile url的定義可以參考這裡:

http://docs.oracle.com/javase/7/docs/api/java/net/jarurlconnection.html

原始的jarfile url是這樣子的:

jar包裡的資源的url:

可以看到對于jar裡的資源,定義以’!/’來分隔。原始的jarfile url隻支援一個’!/’。

spring boot擴充了這個協定,讓它支援多個’!/’,就可以表示jar in jar,jar in directory的資源了。

比如下面的url表示demo-0.0.1-snapshot.jar這個jar裡lib目錄下面的spring-beans-4.2.3.release.jar裡面的manifest.mf:

在構造一個url時,可以傳遞一個handler,而jdk自帶有預設的handler類,應用可以自己注冊handler來處理自定義的url。

參考:

https://docs.oracle.com/javase/8/docs/api/java/net/url.html#url-java.lang.string-java.lang.string-int-java.lang.string-

spring boot通過注冊了一個自定義的handler類來處理多重jar in jar的邏輯。

這個handler内部會用softreference來緩存所有打開過的jarfile。

在處理像下面這樣的url時,會循環處理’!/’分隔符,從最上層出發,先構造出demo-0.0.1-snapshot.jar這個jarfile,再構造出spring-beans-4.2.3.release.jar這個jarfile,然後再構造出指向manifest.mf的jarurlconnection。

對于一個classloader,它需要哪些能力?

查找資源

讀取資源

對應的api是:

上面提到,spring boot構造launchedurlclassloader時,傳遞了一個url[]數組。數組裡是lib目錄下面的jar的url。

對于一個url,jdk或者classloader如何知道怎麼讀取到裡面的内容的?

實際上流程是這樣子的:

launchedurlclassloader.loadclass

url.getcontent()

url.openconnection()

handler.openconnection(url)

最終調用的是jarurlconnection的getinputstream()函數。

從一個url,到最終讀取到url裡的内容,整個過程是比較複雜的,總結下:

spring boot注冊了一個handler來處理”jar:”這種協定的url

spring boot擴充了jarfile和jarurlconnection,内部處理jar in jar的情況

在處理多重jar in jar的url時,spring boot會循環處理,并緩存已經加載到的jarfile

對于多重jar in jar,實際上是解壓到了臨時目錄來處理,可以參考jarfilearchive裡的代碼

在擷取url的inputstream時,最終擷取到的是jarfile裡的jarentrydata

這裡面的細節很多,隻列出比較重要的一些點。

然後,urlclassloader是如何getresource的呢?

urlclassloader在構造時,有url[]數組參數,它内部會用這個數組來構造一個urlclasspath:

在 urlclasspath 内部會為這些urls 都構造一個loader,然後在getresource時,會從這些loader裡一個個去嘗試擷取。

如果擷取成功的話,就像下面那樣包裝為一個resource。

從代碼裡可以看到,實際上是調用了url.openconnection()。這樣完整的鍊條就可以連接配接起來了。

注意,urlclasspath這個類的代碼在jdk裡沒有自帶,在這裡看到 http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/misc/urlclasspath.java#506

在上面隻提到在一個fat jar裡啟動spring boot應用的過程,下面分析ide裡spring boot是如何啟動的。

在ide裡,直接運作的main函數是應用自己的main函數:

其實在ide裡啟動spring boot應用是最簡單的一種情況,因為依賴的jar都讓ide放到classpath裡了,是以spring boot直接啟動就完事了。

還有一種情況是在一個開放目錄下啟動spring boot啟動。所謂的開放目錄就是把fat jar解壓,然後直接啟動應用。

這時,spring boot會判斷目前是否在一個目錄裡,如果是的,則構造一個explodedarchive(前面在jar裡時是jarfilearchive),後面的啟動流程類似fat jar的。

spring boot在啟動時,先通過一個簡單的查找servlet類的方式來判斷是不是在web環境:

如果是的話,則會建立annotationconfigembeddedwebapplicationcontext,否則spring context就是annotationconfigapplicationcontext:

spring boot通過擷取embeddedservletcontainerfactory來啟動對應的web伺服器。

常用的兩個實作類是tomcatembeddedservletcontainerfactory和jettyembeddedservletcontainerfactory。

啟動tomcat的代碼:

會為tomcat建立一個臨時檔案目錄,如:

/tmp/tomcat.2233614112516545210.8080,做為tomcat的basedir。裡面會放tomcat的臨時檔案,比如work目錄。

還會初始化tomcat的一些servlet,比如比較重要的default/jsp servlet:

當spring boot應用被打包為一個fat jar時,是如何通路到web resource的?

實際上是通過archive提供的url,然後通過classloader提供的通路classpath resource的能力來實作的。

比如需要配置一個index.html,這個可以直接放在代碼裡的src/main/resources/static目錄下。

對于index.html歡迎頁,spring boot在初始化時,就會建立一個viewcontroller來處理:

像頁面模闆檔案可以放在src/main/resources/template目錄下。但這個實際上是模闆的實作類自己處理的。比如thymeleafproperties類裡的:

jsp頁面和template類似。實際上是通過spring mvc内置的jstlview來處理的。

可以通過配置spring.view.prefix來設定jsp頁面的目錄:

對于錯誤頁面,spring boot也是通過建立一個basicerrorcontroller來統一處理的。

對應的view是一個簡單的html提醒:

spring boot的這個做法很好,避免了傳統的web應用來出錯時,預設抛出異常,容易洩密。

先通過maven-shade-plugin生成一個包含依賴的jar,再通過spring-boot-maven-plugin插件把spring boot loader相關的類,還有manifest.mf打包到jar裡。

當在shell裡啟動spring boot應用時,會發現它的logger輸出是有顔色的,這個特性很有意思。

可以通過這個設定來關閉:

原理是通過ansioutputapplicationlistener ,這個來擷取這個配置,然後設定logback在輸出時,加了一個 colorconverter,通過org.springframework.boot.ansi.ansioutput ,對一些字段進行了渲染。

可以參考launchedurlclassloader裡的lockprovider

inputargumentsjavaagentdetector,原理是檢測jar的url是否有”-javaagent:”的字首。

applicationpid,可以擷取pid。

spring boot裡自己包裝了一套logger,支援java, log4j, log4j2, logback,以後有需要自己包裝logger時,可以參考這個。

在org.springframework.boot.logging包下面。

通過堆棧裡擷取的方式,判斷main函數,找到原始啟動的main函數。

當spring boot應用以一個fat jar方式運作時,會遇到一些問題。以下是個人看法:

日志不知道放哪,預設是輸出到stdout的

資料目錄不知道放哪, jenkinns的做法是放到 ${user.home}/.jenkins 下面

相對目錄api不能使用,servletcontext.getrealpath(“/”) 傳回的是null

spring boot應用喜歡把配置都寫到代碼裡,有時會帶來混亂。一些簡單可以用xml來表達的配置可能會變得難讀,而且淩亂。

spring boot通過擴充了jar協定,抽象出archive概念,和配套的jarfile,jarurlconnection,launchedurlclassloader,進而實作了上層應用無感覺的all in one的開發體驗。盡管executable war并不是spring提出的概念,但spring boot讓它發揚光大。

spring boot是一個驚人的項目,可以說是spring的第二春,spring-cloud-config, spring-session, metrics, remote shell等都是深愛開發者喜愛的項目、特性。幾乎可以肯定設計者是有豐富的一線開發經驗,深知開發人員的痛點。