SpringBoot這隻怪物到底是如何跑起來的?
來源:碼洞(ID:codehole)
不得不說 SpringBoot 太複雜了,我本來只想研究一下 SpringBoot 最簡單的 HelloWorld 程式是如何從 main 方法一步一步跑起來的,但是這卻是一個相當深的坑。你可以試著沿著呼叫棧程式碼一層一層的深入進去,如果你不打斷點,你根本不知道接下來程式會往哪裡流動。這個不同於我研究過去的 Go 語言、Python 語言框架,它們通常都非常直接了當,設計上清晰易懂,程式碼寫起來簡單,裡面的實現同樣也很簡單。但是 SpringBoot 不是,它的外表輕巧簡單,但是它的裡面就像一隻巨大的怪獸,這隻怪獸有千百隻腳把自己纏繞在一起,把愛研究原始碼的讀者繞的暈頭轉向。但是這 Java 程式設計的世界 SpringBoot 就是老大哥,你卻不得不服。即使你的心中有千萬頭草泥馬在奔跑,但是它就是天下第一。如果你是一個學院派的程式設計師,看到這種現象你會懷疑人生,你不得不接受一個規則 —— 受市場最歡迎的未必就是設計的最好的,裡面夾雜著太多其它的非理性因素。
經過了一番痛苦的折磨,我還是把 SpringBoot 的執行原理摸清楚了,這裡分享給大家。
一、Hello World首先我們看看 SpringBoot 簡單的 Hello World 程式碼,就兩個檔案 HelloControll.java 和 Application.java,執行 Application.java 就可以跑起來一個簡單的 RESTFul Web 伺服器了。
當我開啟瀏覽器看到伺服器正常地將輸出呈現在瀏覽器的時候,我不禁大呼 —— SpringBoot 真他媽太簡單了。
但是問題來了,在 Application 的 main 方法裡我壓根沒有任何地方引用 HelloController 類,那麼它的程式碼又是如何被伺服器呼叫起來的呢?這就需要深入到 SpringApplication.run() 方法中看個究竟了。不過即使不看程式碼,我們也很容易有這樣的猜想,SpringBoot 肯定是在某個地方掃描了當前的 package,將帶有 RestController 註解的類作為 MVC 層的 Controller 自動註冊進了 Tomcat Server。
還有一個讓人不爽的地方是 SpringBoot 啟動太慢了,一個簡單的 Hello World 啟動居然還需要長達 5 秒,要是再複雜一些的專案這樣龜漫的啟動速度那真是不好想象了。
再抱怨一下,這個簡單的 HelloWorld 雖然 pom 裡只配置了一個 maven 依賴,但是傳遞下去,它一共依賴了 36 個 jar 包,其中以 spring 開頭的 jar 包有 15 個。說這是依賴地獄真一點不為過。
批評到這裡就差不多了,下面就要正是進入主題了,看看 SpringBoot 的 main 方法到底是如何跑起來的。
二、SpringBoot 的堆疊瞭解 SpringBoot 執行的最簡單的方法就是看它的呼叫堆疊,下面這個啟動呼叫堆疊還不是太深,我沒什麼可抱怨的。
接下來再看看執行時堆疊,看看一個 HTTP 請求的呼叫棧有多深。不看不知道一看嚇了一大跳!
我通過將 IDE 視窗全屏化,並將其它的控制檯視窗原始碼視窗統統最小化,總算勉強一個螢幕裝下了整個呼叫堆疊。
不過轉念一想,這也不怪 SpringBoot,絕大多數都是 Tomcat 的呼叫堆疊,跟 SpringBoot 相關的只有不到 10 層。
三、探索 ClassLoaderSpringBoot 還有一個特色的地方在於打包時它使用了 FatJar 技術將所有的依賴 jar 包一起放進了最終的 jar 包中的 BOOT-INF/lib 目錄中,當前專案的 class 被統一放到了 BOOT-INF/classes 目錄中。
這不同於我們平時經常使用的 maven shade 外掛,將所有的依賴 jar 包中的 class 檔案解包出來後再密密麻麻的塞進統一的 jar 包中。下面我們將 springboot 打包的 jar 包解壓出來看看它的目錄結構。
這種打包方式的優勢在於最終的 jar 包結構很清晰,所有的依賴一目瞭然。如果使用 maven shade 會將所有的 class 檔案混亂堆積在一起,是無法看清其中的依賴。而最終生成的 jar 包在體積上兩也者幾乎是相等的。
在執行機制上,使用 FatJar 技術執行程式是需要對 jar 包進行改造的,它還需要自定義自己的 ClassLoader 來載入 jar 包裡面 lib 目錄中巢狀的 jar 包中的類。我們可以對比一下兩者的 MANIFEST 檔案就可以看出明顯差異:
SpringBoot 將 jar 包中的 Main-Class 進行了替換,換成了 JarLauncher。還增加了一個 Start-Class 引數,這個引數對應的類才是真正的業務 main 方法入口。我們再看看這個 JarLaucher 具體幹了什麼:
從原始碼中可以看出 JarLaucher 建立了一個特殊的 ClassLoader,然後由這個 ClassLoader 來另啟一個單獨的執行緒來載入 MainClass 並執行。
又一個問題來了,當 JVM 遇到一個不認識的類,BOOT-INF/lib 目錄裡又有那麼多 jar 包,它是如何知道去哪個 jar 包里載入呢?我們繼續看這個特別的 ClassLoader 的原始碼:
這裡的 rootClassLoader 就是雙親委派模型裡的 ExtensionClassLoader ,JVM 內建的類會優先使用它來載入。如果不是內建的就去查詢這個類對應的 Package。
ClassLoader 會在本地快取包名和 jar包路徑的對映關係,如果快取中找不到對應的包名,就必須去 jar 包中挨個遍歷搜尋,這個就比較緩慢了。不過同一個包名只會搜尋一次,下一次就可以直接從快取中得到對應的內嵌 jar 包路徑。
深層 jar 包的內嵌 class 的 URL 路徑長下面這樣,使用感嘆號 ! 分割:
jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class
不過這個定製的 ClassLoader 只會用於打包執行時,在 IDE 開發環境中 main 方法還是直接使用系統類載入器載入執行的。
不得不說,SpringbootLoader 的設計還是很有意思的,它本身很輕量級,程式碼邏輯很獨立沒有其它依賴,它也是 SpringBoot 值得欣賞的點之一。
四、HelloController 自動註冊還剩下最後一個問題,那就是 HelloController 沒有被程式碼引用,它是如何註冊到 Tomcat 服務中去的?它靠的是註解傳遞機制。
SpringBoot 深度依賴註解來完成配置的自動裝配工作,它自己發明了幾十個註解,確實嚴重增加了開發者的心智負擔,你需要仔細閱讀文件才能知道它是用來幹嘛的。Java 註解的形式和功能是分離的,它不同於 Python 的裝飾器是功能性的,Java 的註解就好比程式碼註釋,本身只有屬性,沒有邏輯,註解相應的功能由散落在其它地方的程式碼來完成,需要分析被註解的類結構才可以得到相應註解的屬性。
那註解是又是如何傳遞的呢?
首先 main 方法可以看到的註解是 SpringBootApplication,這個註解又是由ComponentScan 註解來定義的,ComponentScan 註解會定義一個被掃描的包名稱,如果沒有顯示定義那就是當前的包路徑。SpringBoot 在遇到 ComponentScan 註解時會掃描對應包路徑下面的所有 Class,根據這些 Class 上標註的其它註解繼續進行後續處理。當它掃到 HelloController 類時發現它標註了 RestController 註解。
而 RestController 註解又標註了 Controller 註解。SpringBoot 對 Controller 註解進行了特殊處理,它會將 Controller 註解的類當成 URL 處理器註冊到 Servlet 的請求處理器中,在建立 Tomcat Server 時,會將請求處理器傳遞進去。HelloController 就是如此被自動裝配進 Tomcat 的。
掃描處理註解是一個非常繁瑣骯髒的活計,特別是這種用註解來註解註解(繞口)的高階使用方法,這種方法要少用慎用。SpringBoot 中有大量的註解相關程式碼,企圖理解這些程式碼是乏味無趣的沒有必要的,它只會把你的本來清醒的腦袋搞暈。SpringBoot 對於習慣使用的同學來說它是非常方便的,但是其內部實現程式碼不要輕易模仿,那絕對算不上模範 Java 程式碼。
最後老錢表示自己真的很討厭 SpringBoot 這隻怪獸,但是很無奈,這個世界人人都在使用它。這就好比老人們常常告誡年輕人的那句話:如果你改變不了世界,那就先適應這個世界吧!
(完)
Java團長
專注於Java乾貨分享
掃描上方二維碼獲取更多Java乾貨
相關文章
- SpringBoot 究竟是如何跑起來的?Spring Boot
- 讀《程式是如何跑起來的》
- Spring Boot如何跑起來Spring Boot
- 我服了!SpringBoot升級後這服務我一個星期都沒跑起來!(下)Spring Boot
- Weex 是如何在 iOS 客戶端上跑起來的iOS客戶端
- StackExchange.Redis跑起來,為什麼這麼溜?Redis
- 10年來HTML5如何給Flash這隻病貓蓋棺?HTML
- MVC 框架中的路由器(Router)是如何跑起來的MVC框架路由器
- 程式是怎樣跑起來的
- 萬字圖文 | 你寫的程式碼是如何跑起來的?
- 如何5分鐘跑起來一個完整專案?
- Go 程式是怎樣跑起來的Go
- C#是怎麼跑起來的C#
- 全面封殺!10年來HTML5如何給Flash這隻病貓蓋棺?HTML
- App 竟然是這樣跑起來的 —— Android App/Activity 啟動流程分析APPAndroid
- 計算機是怎樣跑起來的計算機
- 來聊聊,這個Java到底是什麼東西?Java
- Demo分享丨看ModelArts與HiLens是如何讓車自己跑起來的
- 競速類遊戲,快跑起來了遊戲
- SpringBoot是如何動起來的Spring Boot
- 程式是怎麼跑起來的第二章
- 程式是怎麼跑起來的第五章
- 《程式是怎樣跑起來的》第二章
- 程式是怎麼跑起來的第七章
- 《程式是怎樣跑起來的》第五章
- 《程式是怎樣跑起來的》第七章
- 《程式是怎樣跑起來的》第十一章
- 【Spring】原來SpringBoot是這樣玩的Spring Boot
- 那些年的開源專案,你跑起來了嗎?
- 讀《計算機是怎樣跑起來的》收穫計算機
- 程式是怎麼跑起來的第四章
- 程式是怎麼跑起來的第九章
- 《程式是怎樣跑起來的》第一章
- 程式是怎麼跑起來的第六章
- 《程式是怎樣跑起來的》第十章
- 《程式是怎樣跑起來的》第六章
- 《程式是怎樣跑起來的》第九章
- 《程式是怎樣跑起來的》第八章