SpringBoot 究竟是如何跑起來的?

不得不說 SpringBoot 太復(fù)雜了,我本來只想研究一下 SpringBoot 最簡(jiǎn)單的 HelloWorld 程序是如何從 main 方法一步一步跑起來的,但是這卻是一個(gè)相當(dāng)深的坑。你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來程序會(huì)往哪里流動(dòng)。這個(gè)不同于我研究過去的 Go 語(yǔ)言、Python 語(yǔ)言框架,它們通常都非常直接了當(dāng),設(shè)計(jì)上清晰易懂,代碼寫起來簡(jiǎn)單,里面的實(shí)現(xiàn)同樣也很簡(jiǎn)單。但是 SpringBoot 不是,它的外表輕巧簡(jiǎn)單,但是它的里面就像一只巨大的怪獸,這只怪獸有千百只腳把自己纏繞在一起,把愛研究源碼的讀者繞的暈頭轉(zhuǎn)向。但是這 Java 編程的世界 SpringBoot 就是老大哥,你卻不得不服。即使你的心中有千萬(wàn)頭草泥馬在奔跑,但是它就是天下第一。如果你是一個(gè)學(xué)院派的程序員,看到這種現(xiàn)象你會(huì)懷疑人生,你不得不接受一個(gè)規(guī)則 —— 受市場(chǎng)最歡迎的未必就是設(shè)計(jì)的最好的,里面夾雜著太多其它的非理性因素。

如果想學(xué)習(xí)Java工程化、高性能及分布式、深入淺出。微服務(wù)、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級(jí)交流:854630135,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費(fèi)分享給大家。

經(jīng)過了一番痛苦的折磨,我還是把 SpringBoot 的運(yùn)行原理摸清楚了,這里分享給大家。

Hello World

首先我們看看 SpringBoot 簡(jiǎn)單的 Hello World 代碼,就兩個(gè)文件 HelloControll.java 和 Application.java,運(yùn)行 Application.java 就可以跑起來一個(gè)簡(jiǎn)單的 RESTFul Web 服務(wù)器了。

// HelloController.javapackagehello;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.bind.annotation.RequestMapping;@RestControllerpublicclassHelloController{@RequestMapping("/")publicString index() {return"Greetings from Spring Boot!"; }}// Application.javapackagehello;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublicclassApplication{publicstatic void main(String[] args) { SpringApplication.run(Application.class, args); }}

當(dāng)我打開瀏覽器看到服務(wù)器正常地將輸出呈現(xiàn)在瀏覽器的時(shí)候,我不禁大呼 —— SpringBoot 真他媽太簡(jiǎn)單了。

但是問題來了,在 Application 的 main 方法里我壓根沒有任何地方引用 HelloController 類,那么它的代碼又是如何被服務(wù)器調(diào)用起來的呢?這就需要深入到 SpringApplication.run() 方法中看個(gè)究竟了。不過即使不看代碼,我們也很容易有這樣的猜想,SpringBoot 肯定是在某個(gè)地方掃描了當(dāng)前的 package,將帶有 RestController 注解的類作為 MVC 層的 Controller 自動(dòng)注冊(cè)進(jìn)了 Tomcat Server。

還有一個(gè)讓人不爽的地方是 SpringBoot 啟動(dòng)太慢了,一個(gè)簡(jiǎn)單的 Hello World 啟動(dòng)居然還需要長(zhǎng)達(dá) 5 秒,要是再?gòu)?fù)雜一些的項(xiàng)目這樣龜漫的啟動(dòng)速度那真是不好想象了。

再抱怨一下,這個(gè)簡(jiǎn)單的 HelloWorld 雖然 pom 里只配置了一個(gè) maven 依賴,但是傳遞下去,它一共依賴了 36 個(gè) jar 包,其中以 spring 開頭的 jar 包有 15 個(gè)。說這是依賴地獄真一點(diǎn)不為過。

批評(píng)到這里就差不多了,下面就要正是進(jìn)入主題了,看看 SpringBoot 的 main 方法到底是如何跑起來的。

SpringBoot 的堆棧

了解 SpringBoot 運(yùn)行的最簡(jiǎn)單的方法就是看它的調(diào)用堆棧,下面這個(gè)啟動(dòng)調(diào)用堆棧還不是太深,我沒什么可抱怨的。

publicclassTomcatServer{@Overridepublicvoidstart()throwsWebServerException{ ... }}

接下來再看看運(yùn)行時(shí)堆棧,看看一個(gè) HTTP 請(qǐng)求的調(diào)用棧有多深。不看不知道一看嚇了一大跳!

我通過將 IDE 窗口全屏化,并將其它的控制臺(tái)窗口源碼窗口統(tǒng)統(tǒng)最小化,總算勉強(qiáng)一個(gè)屏幕裝下了整個(gè)調(diào)用堆棧。

不過轉(zhuǎn)念一想,這也不怪 SpringBoot,絕大多數(shù)都是 Tomcat 的調(diào)用堆棧,跟 SpringBoot 相關(guān)的只有不到 10 層。

探索 ClassLoader

SpringBoot 還有一個(gè)特色的地方在于打包時(shí)它使用了 FatJar 技術(shù)將所有的依賴 jar 包一起放進(jìn)了最終的 jar 包中的 BOOT-INF/lib 目錄中,當(dāng)前項(xiàng)目的 class 被統(tǒng)一放到了 BOOT-INF/classes 目錄中。

org.springframework.bootspring-boot-maven-plugin

這不同于我們平時(shí)經(jīng)常使用的 maven shade 插件,將所有的依賴 jar 包中的 class 文件解包出來后再密密麻麻的塞進(jìn)統(tǒng)一的 jar 包中。下面我們將 springboot 打包的 jar 包解壓出來看看它的目錄結(jié)構(gòu)。

├──BOOT-INF│ ├──classes│ │ └──hello│ └──lib│ ├──classmate-1.3.4.jar│ ├──hibernate-validator-6.0.12.Final.jar│ ├──jackson-annotations-2.9.0.jar│ ├──jackson-core-2.9.6.jar│ ├──jackson-databind-2.9.6.jar│ ├──jackson-datatype-jdk8-2.9.6.jar│ ├──jackson-datatype-jsr310-2.9.6.jar│ ├──jackson-module-parameter-names-2.9.6.jar│ ├──javax.annotation-api-1.3.2.jar│ ├──jboss-logging-3.3.2.Final.jar│ ├──jul-to-slf4j-1.7.25.jar│ ├──log4j-api-2.10.0.jar│ ├──log4j-to-slf4j-2.10.0.jar│ ├──logback-classic-1.2.3.jar│ ├──logback-core-1.2.3.jar│ ├──slf4j-api-1.7.25.jar│ ├──snakeyaml-1.19.jar│ ├──spring-aop-5.0.9.RELEASE.jar│ ├──spring-beans-5.0.9.RELEASE.jar│ ├──spring-boot-2.0.5.RELEASE.jar│ ├──spring-boot-autoconfigure-2.0.5.RELEASE.jar│ ├──spring-boot-starter-2.0.5.RELEASE.jar│ ├──spring-boot-starter-json-2.0.5.RELEASE.jar│ ├──spring-boot-starter-logging-2.0.5.RELEASE.jar│ ├──spring-boot-starter-tomcat-2.0.5.RELEASE.jar│ ├──spring-boot-starter-web-2.0.5.RELEASE.jar│ ├──spring-context-5.0.9.RELEASE.jar│ ├──spring-core-5.0.9.RELEASE.jar│ ├──spring-expression-5.0.9.RELEASE.jar│ ├──spring-jcl-5.0.9.RELEASE.jar│ ├──spring-web-5.0.9.RELEASE.jar│ ├──spring-webmvc-5.0.9.RELEASE.jar│ ├──tomcat-embed-core-8.5.34.jar│ ├──tomcat-embed-el-8.5.34.jar│ ├──tomcat-embed-websocket-8.5.34.jar│ └──validation-api-2.0.1.Final.jar├──META-INF│ ├──MANIFEST.MF│ └──maven│ └──org.springframework└──org└──springframework└──boot

這種打包方式的優(yōu)勢(shì)在于最終的 jar 包結(jié)構(gòu)很清晰,所有的依賴一目了然。如果使用 maven shade 會(huì)將所有的 class 文件混亂堆積在一起,是無(wú)法看清其中的依賴。而最終生成的 jar 包在體積上兩也者幾乎是相等的。

在運(yùn)行機(jī)制上,使用 FatJar 技術(shù)運(yùn)行程序是需要對(duì) jar 包進(jìn)行改造的,它還需要自定義自己的 ClassLoader 來加載 jar 包里面 lib 目錄中嵌套的 jar 包中的類。我們可以對(duì)比一下兩者的 MANIFEST 文件就可以看出明顯差異

// Generated by Maven Shade PluginManifest-Version:1.0Implementation-Title:gs-spring-bootImplementation-Version:0.1.0Built-By:qianwpImplementation-Vendor-Id:org.springframeworkCreated-By:Apache Maven3.5.4Build-Jdk:1.8.0_191Implementation-URL:https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/gs-spring-bootMain-Class:hello.Application// Generated by SpringBootLoader PluginManifest-Version:1.0Implementation-Title:gs-spring-bootImplementation-Version:0.1.0Built-By:qianwpImplementation-Vendor-Id:org.springframeworkSpring-Boot-Version:2.0.5.RELEASEMain-Class:org.springframework.boot.loader.JarLauncherStart-Class:hello.ApplicationSpring-Boot-Classes:BOOT-INF/classes/Spring-Boot-Lib:BOOT-INF/lib/Created-By:Apache Maven3.5.4Build-Jdk:1.8.0_191Implementation-URL:https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/gs-spring-boot

SpringBoot 將 jar 包中的 Main-Class 進(jìn)行了替換,換成了 JarLauncher。還增加了一個(gè) Start-Class 參數(shù),這個(gè)參數(shù)對(duì)應(yīng)的類才是真正的業(yè)務(wù) main 方法入口。我們?cè)倏纯催@個(gè) JarLaucher 具體干了什么

如果想學(xué)習(xí)Java工程化、高性能及分布式、深入淺出。微服務(wù)、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級(jí)交流:854630135,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費(fèi)分享給大家。

publicclassJarLauncher{ ...staticvoidmain(String[] args){newJarLauncher().launch(args); }protectedvoidlaunch(String[] args){try{ JarFile.registerUrlProtocolHandler(); ClassLoader cl = createClassLoader(getClassPathArchives()); launch(args, getMainClass(), cl); }catch(Exception ex) { ex.printStackTrace(); System.exit(1); } }protectedvoidlaunch(String[] args, String mcls, ClassLoader cl){ Runnable runner = createMainMethodRunner(mcls, args, cl); Thread runnerThread =newThread(runner); runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start(); }}classMainMethodRunner{@Overridepublicvoidrun(){try{ Thread th = Thread.currentThread(); ClassLoader cl = th.getContextClassLoader(); Class mc = cl.loadClass(this.mainClassName); Method mm = mc.getDeclaredMethod("main", String[].class);if(mm ==null) {thrownewIllegalStateException(this.mainClassName +" does not have a main method"); } mm.invoke(null,newObject[] {this.args }); }catch(Exception ex) { ex.printStackTrace(); System.exit(1); } }}

從源碼中可以看出 JarLaucher 創(chuàng)建了一個(gè)特殊的 ClassLoader,然后由這個(gè) ClassLoader 來另啟一個(gè)單獨(dú)的線程來加載 MainClass 并運(yùn)行。

又一個(gè)問題來了,當(dāng) JVM 遇到一個(gè)不認(rèn)識(shí)的類,BOOT-INF/lib 目錄里又有那么多 jar 包,它是如何知道去哪個(gè) jar 包里加載呢?我們繼續(xù)看這個(gè)特別的 ClassLoader 的源碼

classLaunchedURLClassLoaderextendsURLClassLoader{ ...privateClass doLoadClass(Stringname) {if(this.rootClassLoader !=null) {returnthis.rootClassLoader.loadClass(name); } findPackage(name);Class cls = findClass(name);returncls; }}

這里的 rootClassLoader 就是雙親委派模型里的 ExtensionClassLoader ,JVM 內(nèi)置的類會(huì)優(yōu)先使用它來加載。如果不是內(nèi)置的就去查找這個(gè)類對(duì)應(yīng)的 Package。

privatevoidfindPackage(finalStringname) {intlastDot = name.lastIndexOf('.');if(lastDot !=-1) {StringpackageName = name.substring(0, lastDot);if(getPackage(packageName) ==null) {try{ definePackage(name, packageName); }catch(Exception ex) {// Swallow and continue} } }}privatefinalHashMap packages =newHashMap<>();protectedPackage getPackage(Stringname) { Package pkg;synchronized(packages) { pkg = packages.get(name); }if(pkg ==null) {if(parent !=null) { pkg = parent.getPackage(name); }else{ pkg = Package.getSystemPackage(name); }if(pkg !=null) {synchronized(packages) { Package pkg2 = packages.get(name);if(pkg2 ==null) { packages.put(name, pkg); }else{ pkg = pkg2; } } } }returnpkg;}privatevoiddefinePackage(Stringname,StringpackageName) {Stringpath = name.replace('.','/').concat(".class");for(URL url : getURLs()) {try{if(url.getContent()instanceofJarFile) { JarFile jf= (JarFile) url.getContent();if(jf.getJarEntryData(path) !=null&& jf.getManifest() !=null) { definePackage(packageName, jf.getManifest(), url);returnnull; } } }catch(IOException ex) {// Ignore} }returnnull;}

ClassLoader 會(huì)在本地緩存包名和 jar包路徑的映射關(guān)系,如果緩存中找不到對(duì)應(yīng)的包名,就必須去 jar 包中挨個(gè)遍歷搜尋,這個(gè)就比較緩慢了。不過同一個(gè)包名只會(huì)搜尋一次,下一次就可以直接從緩存中得到對(duì)應(yīng)的內(nèi)嵌 jar 包路徑。

深層 jar 包的內(nèi)嵌 class 的 URL 路徑長(zhǎng)下面這樣,使用感嘆號(hào) ! 分割

jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class

不過這個(gè)定制的 ClassLoader 只會(huì)用于打包運(yùn)行時(shí),在 IDE 開發(fā)環(huán)境中 main 方法還是直接使用系統(tǒng)類加載器加載運(yùn)行的。

不得不說,SpringbootLoader 的設(shè)計(jì)還是很有意思的,它本身很輕量級(jí),代碼邏輯很獨(dú)立沒有其它依賴,它也是 SpringBoot 值得欣賞的點(diǎn)之一。

HelloController 自動(dòng)注冊(cè)

還剩下最后一個(gè)問題,那就是 HelloController 沒有被代碼引用,它是如何注冊(cè)到 Tomcat 服務(wù)中去的?它靠的是注解傳遞機(jī)制。

SpringBoot 深度依賴注解來完成配置的自動(dòng)裝配工作,它自己發(fā)明了幾十個(gè)注解,確實(shí)嚴(yán)重增加了開發(fā)者的心智負(fù)擔(dān),你需要仔細(xì)閱讀文檔才能知道它是用來干嘛的。Java 注解的形式和功能是分離的,它不同于 Python 的裝飾器是功能性的,Java 的注解就好比代碼注釋,本身只有屬性,沒有邏輯,注解相應(yīng)的功能由散落在其它地方的代碼來完成,需要分析被注解的類結(jié)構(gòu)才可以得到相應(yīng)注解的屬性。

那注解是又是如何傳遞的呢?

@SpringBootApplicationpublic class Application {publicstaticvoidmain(String[] args) {SpringApplication.run(Application.class, args); }}@ComponentScanpublic@interfaceSpringBootApplication{...}public@interfaceComponentScan {String[]basePackages()default{};}

首先 main 方法可以看到的注解是 SpringBootApplication,這個(gè)注解又是由ComponentScan 注解來定義的,ComponentScan 注解會(huì)定義一個(gè)被掃描的包名稱,如果沒有顯示定義那就是當(dāng)前的包路徑。SpringBoot 在遇到 ComponentScan 注解時(shí)會(huì)掃描對(duì)應(yīng)包路徑下面的所有 Class,根據(jù)這些 Class 上標(biāo)注的其它注解繼續(xù)進(jìn)行后續(xù)處理。當(dāng)它掃到 HelloController 類時(shí)發(fā)現(xiàn)它標(biāo)注了 RestController 注解。

@RestControllerpublic class HelloController {...}@Controllerpublic@interfaceRestController {}

而 RestController 注解又標(biāo)注了 Controller 注解。SpringBoot 對(duì) Controller 注解進(jìn)行了特殊處理,它會(huì)將 Controller 注解的類當(dāng)成 URL 處理器注冊(cè)到 Servlet 的請(qǐng)求處理器中,在創(chuàng)建 Tomcat Server 時(shí),會(huì)將請(qǐng)求處理器傳遞進(jìn)去。HelloController 就是如此被自動(dòng)裝配進(jìn) Tomcat 的。

掃描處理注解是一個(gè)非常繁瑣骯臟的活計(jì),特別是這種用注解來注解注解(繞口)的高級(jí)使用方法,這種方法要少用慎用。SpringBoot 中有大量的注解相關(guān)代碼,企圖理解這些代碼是乏味無(wú)趣的沒有必要的,它只會(huì)把你的本來清醒的腦袋搞暈。SpringBoot 對(duì)于習(xí)慣使用的同學(xué)來說它是非常方便的,但是其內(nèi)部實(shí)現(xiàn)代碼不要輕易模仿,那絕對(duì)算不上模范 Java 代碼。

最后表示自己真的很討厭 SpringBoot 這只怪獸,但是很無(wú)奈,這個(gè)世界人人都在使用它。這就好比老人們常常告誡年輕人的那句話:如果你改變不了世界,那就先適應(yīng)這個(gè)世界吧!

歡迎工作一到八年的Java工程師朋友們加入Java高級(jí)交流:854630135

本群提供免費(fèi)的學(xué)習(xí)指導(dǎo) 架構(gòu)資料 以及免費(fèi)的解答

不懂得問題都可以在本群提出來 之后還會(huì)有直播平臺(tái)和講師直接交流噢

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • SpringMVC原理分析 Spring Boot學(xué)習(xí) 5、Hello World探究 1、POM文件 1、父項(xiàng)目...
    jack_jerry閱讀 1,484評(píng)論 0 1
  • Spring Boot特點(diǎn) 1. 創(chuàng)建獨(dú)立的Spring應(yīng)用程序 2. 嵌入的Tomcat,無(wú)需部署WAR文件(此...
    酒紅色的小貓一閱讀 821評(píng)論 0 0
  • 今天放學(xué)后,老師說老大這次考的一點(diǎn)都不好,光漏題就減了10分,回到家我一看試卷,可把我氣死了。每次考試都囑咐她認(rèn)真...
    楊海諾閱讀 172評(píng)論 0 0
  • 基本思想: 先從數(shù)列中取出一個(gè)數(shù)作為基準(zhǔn)數(shù)。 分區(qū)過程,將比這個(gè)數(shù)大的數(shù)全放到它的右邊,小于或等于它的數(shù)全放到它的...
    無(wú)敵的肉包閱讀 190評(píng)論 0 0
  • 我的感恩1 感恩今天自己完成了,記得作業(yè)讓我感受到了自己的認(rèn)真做事。2感恩今天我們?nèi)ヅ懿?,讓我感受到了我們的熱愛運(yùn)...
    麗人d閱讀 174評(píng)論 0 0

友情鏈接更多精彩內(nèi)容