SpringBoot項目啓動過程源碼終於整體捋了一遍(四)

上篇分析了初始化SpringApplication時是如何推斷應用類型的,並且問題依然遺留着,即初始化SpringApplication的時候resourceLoader爲null怎麼拿類加載器。這篇帶着這個問題繼續往下擼源碼,還是把SpringApplication的構造方法貼一下:

繼續往下看:

 

 setInitializers()方法即設置初始化器,ApplicationContextInitializer即應用上下文的初始化器,ApplicationContextInitializer.class只是個類路徑,想設置至少得先拿到初始化器的對象吧,不出意料的話這個getSpringFactoriesInstances()方法應該是通過這個類路徑去拿初始化器,看看這個方法:

 直接看下面那個重載的方法吧,這裏參數type即ApplicationContextInitializer.class,另外兩個參數都是空數組,這個方法第一行就是去拿類加載器,好傢伙終於知道拿類加載器了,之前遺留的問題似乎可以去找答案了,看一下這個getClassLoader()方法:

 原來resourceLoader爲null也不要緊,還留了一手ClassUtils.getDefaultClassLoader(),意思是還有默認類加載器?看一下這個getDefaultClassLoader()方法:

 這裏可以簡單介紹一下類加載機制,java的類加載器大類分三種,即啓動類加載器(BootstrapClassLoader)、擴展類加載器(ExtClassLoader)和系統類加載器(AppClassLoader),也可以說有第四種用戶自定義類加載器,不過這個也屬於系統類加載器,java的類加載機制雙親委派模型可以去了解一下,之前也寫過相關博客(https://blog.csdn.net/weixin_42447959/article/details/81265888)。在這個方法裏,我們可以看到類加載器優先拿的是Thread.currentThread().getContextClassLoader(),如果拿不到那就拿加載當前類即ClassUtils的類加載器,如果還拿不到那就拿ClassLoader.getSystemClassLoader(),這個就是系統類加載器AppClassLoader。

這裏重點看一下Thread.currentThread().getContextClassLoader(),這個是去拿線程上下文加載器,線程創建者在創建線程之後用對應的setContextClassLoader()方法將適合的類加載器設置到線程中,那麼線程中的代碼就可以通過getContextClassLoader()獲取到這個類加載器來加載類或者資源,如果不設置默認是系統類加載器AppClassLoader。

那這裏的線程上下文加載器是哪個呢?這得看項目是怎麼啓動的。如果項目是在IDEA或者其他編輯工具中啓動,這時候的tomcat可能是編輯器自帶的也可能直接就是本地的tomcat,它們又不認識springboot不boot的,纔不會專門給線程設置上下文加載器,所以最後線程上下文加載器就是AppClassLoader。但是如果是在服務器中java -jar啓動就不一樣了,說明這時候項目已經被打成了fat jar,都知道fat jar裏面是內置tomcat的,這也是它爲什麼可直接執行,這個內置的tomcat可是自己人,它創建線程的時候還真去給線程設置了一個上下文加載器,要知道具體是哪種類加載器先去看看這個fat jar是怎麼啓動的,反編譯了一個可執行jar包,目錄結構如下:

這個MANIFEST.MF文件是jar包的描述文件,可知main-class是JarLauncher ,那就從這裏梳理一下fat jar的執行過程,看能不能發現什麼,點進這個類果然有個main()方法:

構造了JarLauncher對象然後調用這個launcher方法,繼續看這個方法:

只關注類加載器相關的,可以看到第二行是去拿類加載器了,幾個嵌套方法調用點進去最後可以看到:

 原來這個的classLoader是這個LaunchedURLClassLoader,繼續往下看構造好類加載器後緊接着調用了launch()方法,看一下這個方法:

果然,原來是這裏給線程設置了上下文加載器LaunchedURLClassLoader,子線程裏也都有這個上下文加載器了。

至於爲什麼要專門設置一個LaunchedURLClassLoader作爲上下文加載器呢,其實LaunchedURLClassLoader和默認的AppClassLoader都是繼承了URLClassLoader,只不過構造LaunchedURLClassLoader的時候指定的urls很特殊,這個和JarLauncher中這兩個常量有關:

我們都知道spring boot打包需要特定的打包插件 spring-boot-maven-plugin,這個打包插件其實就是在maven原來的基礎上二次打包,把項目依賴的jar包也打進去,所有可以直接運行,這兩個常量就是告訴LaunchedURLClassLoader這個類加載去classer路徑下找項目class文件,去lib路徑下去找外部jar包,所有fat jar的內部結構都是固定的,這個類加載器自然也是量身定製的。

所以java -jar啓動項目和在本地編輯器中啓動項目,線程的上下文加載器是不同的,這也解釋了爲什麼在代碼中如果想用類加載器,開發調試的時候還好好的,打成jar包後一部署就完了,因爲他們的類加載器可能不一樣。所以不建議在代碼中用XXX.class.getClassLoader(),這種方法拿到的只能是AppClassLoader。而建議用Thread.currentThread().getContextClassLoader(),這拿到的類加載器可不一定,但是肯定都可以加載項目資源文件。

瞭解這個線程上下文加載器的過程中,還發現它的來源其實挺有意思。還記得java類加載的雙親委派模型不?jdk1.2的時候引入了雙親委派模型的類加載機制,但是在jdk1.3的時候又引入了JNDI服務,JNDI目的就是對資源進行集中管理和查找,JDNI是一堆接口,需要由獨立的廠商去實現這些接口並部署在應用程序的ClassPath下,這堆接口的類是jdk自己的東西由啓動類加載器去加載,但啓動類加載器可不認識各廠商的實現類,別人的東西啓動類加載器不能加載。爲了解決這個問題,Java設計團隊只好引入了一個線程上下文類加載器的設計。這個類加載器可以通過Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,他將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是AppClassLoader。有了線程上下文加載器,JNDI服務就可以使用它去加載所需要的廠商實現類代碼。也就是在父類加載器加載的類中請求子類加載器去繼續完成類加載的動作,這種行爲實際上就是打破了雙親委派模型。Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

剛剛提到了java中的SPI,全稱是服務發現機制。當服務的提供者提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。基於這樣一個約定就能很好的找到服務接口的實現類,而不需要再代碼裏制定。jdk提供服務實現查找的一個工具類:java.util.ServiceLoader。應用場景直白點說就是:我(JDK)提供了一組服務接口(數據庫驅動、日誌等),但是這個服務具體怎麼幹活你可以自己去實現這個接口,前提是你要遵循約定(把類名寫在/META-INF裏)。這個接口是我自己的類加載器加載的,但是我自己的類加載器不能加載你的實現類,所以我需要藉助線程上下文類加載器。

本來想繼續看初始化SpringApplication的時候如何設置初始化器的,這又扯遠了,這就是爲什麼當時決定連載下去的原因,我也不知道需要連載幾篇,這篇就到這裏,例常總結一下:

這篇解決了之前的遺留問題,resourceLoader爲null時會有獲取默認類加載器的一套邏輯。然後也是總結了java的類加載相關,下篇再繼續看getSpringFactoriesInstances()方法,繼續看初始化SpringApplication的時候如何設置初始化器的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章