摘要:你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來(lái)程序會(huì)往哪里流動(dòng)。接下來(lái)再看看運(yùn)行時(shí)堆棧,看看一個(gè)請(qǐng)求的調(diào)用棧有多深。就是如此被自動(dòng)裝配進(jìn)的。
摘要: 神奇的SpringBoot。
原文:SpringBoot 究竟是如何跑起來(lái)的?
作者:老錢(qián)
Fundebug經(jīng)授權(quán)轉(zhuǎn)載,版權(quán)歸原作者所有。
不得不說(shuō) SpringBoot 太復(fù)雜了,我本來(lái)只想研究一下 SpringBoot 最簡(jiǎn)單的 HelloWorld 程序是如何從 main 方法一步一步跑起來(lái)的,但是這卻是一個(gè)相當(dāng)深的坑。你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來(lái)程序會(huì)往哪里流動(dòng)。這個(gè)不同于我研究過(guò)去的 Go 語(yǔ)言、Python 語(yǔ)言框架,它們通常都非常直接了當(dāng),設(shè)計(jì)上清晰易懂,代碼寫(xiě)起來(lái)簡(jiǎn)單,里面的實(shí)現(xiàn)同樣也很簡(jiǎn)單。但是 SpringBoot 不是,它的外表輕巧簡(jiǎn)單,但是它的里面就像一只巨大的怪獸,這只怪獸有千百只腳把自己纏繞在一起,把愛(ài)研究源碼的讀者繞的暈頭轉(zhuǎn)向。但是這 Java 編程的世界 SpringBoot 就是老大哥,你卻不得不服。即使你的心中有千萬(wàn)頭草泥馬在奔跑,但是它就是天下第一。如果你是一個(gè)學(xué)院派的程序員,看到這種現(xiàn)象你會(huì)懷疑人生,你不得不接受一個(gè)規(guī)則 —— 受市場(chǎng)最歡迎的未必就是設(shè)計(jì)的最好的,里面夾雜著太多其它的非理性因素。
經(jīng)過(guò)了一番痛苦的折磨,我還是把 SpringBoot 的運(yùn)行原理摸清楚了,這里分享給大家。
Hello World首先我們看看 SpringBoot 簡(jiǎn)單的 Hello World 代碼,就兩個(gè)文件 HelloControll.java 和 Application.java,運(yùn)行 Application.java 就可以跑起來(lái)一個(gè)簡(jiǎn)單的 RESTFul Web 服務(wù)器了。
// HelloController.java package hello; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; @RestController public class HelloController { @RequestMapping("/") public String index() { return "Greetings from Spring Boot!"; } } // Application.java package hello; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
當(dāng)我打開(kāi)瀏覽器看到服務(wù)器正常地將輸出呈現(xiàn)在瀏覽器的時(shí)候,我不禁大呼 —— SpringBoot 真他媽太簡(jiǎn)單了。
但是問(wèn)題來(lái)了,在 Application 的 main 方法里我壓根沒(méi)有任何地方引用 HelloController 類,那么它的代碼又是如何被服務(wù)器調(diào)用起來(lái)的呢?這就需要深入到 SpringApplication.run() 方法中看個(gè)究竟了。不過(guò)即使不看代碼,我們也很容易有這樣的猜想,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 開(kāi)頭的 jar 包有 15 個(gè)。說(shuō)這是依賴地獄真一點(diǎn)不為過(guò)。
批評(píng)到這里就差不多了,下面就要正是進(jìn)入主題了,看看 SpringBoot 的 main 方法到底是如何跑起來(lái)的。
SpringBoot 的堆棧了解 SpringBoot 運(yùn)行的最簡(jiǎn)單的方法就是看它的調(diào)用堆棧,下面這個(gè)啟動(dòng)調(diào)用堆棧還不是太深,我沒(méi)什么可抱怨的。
public class TomcatServer { @Override public void start() throws WebServerException { ... } }
接下來(lái)再看看運(yùn)行時(shí)堆棧,看看一個(gè) HTTP 請(qǐng)求的調(diào)用棧有多深。不看不知道一看嚇了一大跳!
我通過(guò)將 IDE 窗口全屏化,并將其它的控制臺(tái)窗口源碼窗口統(tǒng)統(tǒng)最小化,總算勉強(qiáng)一個(gè)屏幕裝下了整個(gè)調(diào)用堆棧。
不過(guò)轉(zhuǎn)念一想,這也不怪 SpringBoot,絕大多數(shù)都是 Tomcat 的調(diào)用堆棧,跟 SpringBoot 相關(guān)的只有不到 10 層。
探索 ClassLoaderSpringBoot 還有一個(gè)特色的地方在于打包時(shí)它使用了 FatJar 技術(shù)將所有的依賴 jar 包一起放進(jìn)了最終的 jar 包中的 BOOT-INF/lib 目錄中,當(dāng)前項(xiàng)目的 class 被統(tǒng)一放到了 BOOT-INF/classes 目錄中。
org.springframework.boot spring-boot-maven-plugin
這不同于我們平時(shí)經(jīng)常使用的 maven shade 插件,將所有的依賴 jar 包中的 class 文件解包出來(lái)后再密密麻麻的塞進(jìn)統(tǒng)一的 jar 包中。下面我們將 springboot 打包的 jar 包解壓出來(lái)看看它的目錄結(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 來(lái)加載 jar 包里面 lib 目錄中嵌套的 jar 包中的類。我們可以對(duì)比一下兩者的 MANIFEST 文件就可以看出明顯差異
// Generated by Maven Shade Plugin Manifest-Version: 1.0 Implementation-Title: gs-spring-boot Implementation-Version: 0.1.0 Built-By: qianwp Implementation-Vendor-Id: org.springframework Created-By: Apache Maven 3.5.4 Build-Jdk: 1.8.0_191 Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-boot Main-Class: hello.Application // Generated by SpringBootLoader Plugin Manifest-Version: 1.0 Implementation-Title: gs-spring-boot Implementation-Version: 0.1.0 Built-By: qianwp Implementation-Vendor-Id: org.springframework Spring-Boot-Version: 2.0.5.RELEASE Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: hello.Application Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Created-By: Apache Maven 3.5.4 Build-Jdk: 1.8.0_191 Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-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 具體干了什么
public class JarLauncher{ ... static void main(String[] args) { new JarLauncher().launch(args); } protected void launch(String[] args) { try { JarFile.registerUrlProtocolHandler(); ClassLoader cl = createClassLoader(getClassPathArchives()); launch(args, getMainClass(), cl); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } } protected void launch(String[] args, String mcls, ClassLoader cl) { Runnable runner = createMainMethodRunner(mcls, args, cl); Thread runnerThread = new Thread(runner); runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start(); } } class MainMethodRunner { @Override public void run() { 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) { throw new IllegalStateException(this.mainClassName + " does not have a main method"); } mm.invoke(null, new Object[] { this.args }); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } } }
從源碼中可以看出 JarLaucher 創(chuàng)建了一個(gè)特殊的 ClassLoader,然后由這個(gè) ClassLoader 來(lái)另啟一個(gè)多帶帶的線程來(lái)加載 MainClass 并運(yùn)行。
又一個(gè)問(wèn)題來(lái)了,當(dāng) JVM 遇到一個(gè)不認(rèn)識(shí)的類,BOOT-INF/lib 目錄里又有那么多 jar 包,它是如何知道去哪個(gè) jar 包里加載呢?我們繼續(xù)看這個(gè)特別的 ClassLoader 的源碼
class LaunchedURLClassLoader extends URLClassLoader { ... private Class> doLoadClass(String name) { if (this.rootClassLoader != null) { return this.rootClassLoader.loadClass(name); } findPackage(name); Class> cls = findClass(name); return cls; } }
這里的 rootClassLoader 就是雙親委派模型里的 ExtensionClassLoader ,JVM 內(nèi)置的類會(huì)優(yōu)先使用它來(lái)加載。如果不是內(nèi)置的就去查找這個(gè)類對(duì)應(yīng)的 Package。
private void findPackage(final String name) { int lastDot = name.lastIndexOf("."); if (lastDot != -1) { String packageName = name.substring(0, lastDot); if (getPackage(packageName) == null) { try { definePackage(name, packageName); } catch (Exception ex) { // Swallow and continue } } } } private final HashMappackages = new HashMap<>(); protected Package getPackage(String name) { 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; } } } } return pkg; } private void definePackage(String name, String packageName) { String path = name.replace(".", "/").concat(".class"); for (URL url : getURLs()) { try { if (url.getContent() instanceof JarFile) { JarFile jf= (JarFile) url.getContent(); if (jf.getJarEntryData(path) != null && jf.getManifest() != null) { definePackage(packageName, jf.getManifest(), url); return null; } } } catch (IOException ex) { // Ignore } } return null; }
ClassLoader 會(huì)在本地緩存包名和 jar包路徑的映射關(guān)系,如果緩存中找不到對(duì)應(yīng)的包名,就必須去 jar 包中挨個(gè)遍歷搜尋,這個(gè)就比較緩慢了。不過(guò)同一個(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
不過(guò)這個(gè)定制的 ClassLoader 只會(huì)用于打包運(yùn)行時(shí),在 IDE 開(kāi)發(fā)環(huán)境中 main 方法還是直接使用系統(tǒng)類加載器加載運(yùn)行的。
不得不說(shuō),SpringbootLoader 的設(shè)計(jì)還是很有意思的,它本身很輕量級(jí),代碼邏輯很獨(dú)立沒(méi)有其它依賴,它也是 SpringBoot 值得欣賞的點(diǎn)之一。
HelloController 自動(dòng)注冊(cè)還剩下最后一個(gè)問(wèn)題,那就是 HelloController 沒(méi)有被代碼引用,它是如何注冊(cè)到 Tomcat 服務(wù)中去的?它靠的是注解傳遞機(jī)制。
SpringBoot 深度依賴注解來(lái)完成配置的自動(dòng)裝配工作,它自己發(fā)明了幾十個(gè)注解,確實(shí)嚴(yán)重增加了開(kāi)發(fā)者的心智負(fù)擔(dān),你需要仔細(xì)閱讀文檔才能知道它是用來(lái)干嘛的。Java 注解的形式和功能是分離的,它不同于 Python 的裝飾器是功能性的,Java 的注解就好比代碼注釋,本身只有屬性,沒(méi)有邏輯,注解相應(yīng)的功能由散落在其它地方的代碼來(lái)完成,需要分析被注解的類結(jié)構(gòu)才可以得到相應(yīng)注解的屬性。
那注解是又是如何傳遞的呢?
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @ComponentScan public @interface SpringBootApplication { ... } public @interface ComponentScan { String[] basePackages() default {}; }
首先 main 方法可以看到的注解是 SpringBootApplication,這個(gè)注解又是由ComponentScan 注解來(lái)定義的,ComponentScan 注解會(huì)定義一個(gè)被掃描的包名稱,如果沒(méi)有顯示定義那就是當(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 注解。
@RestController public class HelloController { ... } @Controller public @interface RestController { }
而 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ì),特別是這種用注解來(lái)注解注解(繞口)的高級(jí)使用方法,這種方法要少用慎用。SpringBoot 中有大量的注解相關(guān)代碼,企圖理解這些代碼是乏味無(wú)趣的沒(méi)有必要的,它只會(huì)把你的本來(lái)清醒的腦袋搞暈。SpringBoot 對(duì)于習(xí)慣使用的同學(xué)來(lái)說(shuō)它是非常方便的,但是其內(nèi)部實(shí)現(xiàn)代碼不要輕易模仿,那絕對(duì)算不上模范 Java 代碼。
最后老錢(qián)表示自己真的很討厭 SpringBoot 這只怪獸,但是很無(wú)奈,這個(gè)世界人人都在使用它。這就好比老人們常常告誡年輕人的那句話:如果你改變不了世界,那就先適應(yīng)這個(gè)世界吧!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/72897.html
摘要:自己在前端的開(kāi)發(fā)中主要使用的框架,今天的這篇文章比較基礎(chǔ),我之前在剛剛接觸項(xiàng)目的時(shí)候并沒(méi)有思考過(guò)關(guān)于項(xiàng)目是究竟怎么運(yùn)行起來(lái)的,只知道項(xiàng)目就跑起來(lái)了,究竟我在輸入這行命令之后項(xiàng)目是怎么運(yùn)行的,分別走了哪幾步,怎么樣才走到生產(chǎn)環(huán)境,什么情況下又 自己在前端的開(kāi)發(fā)中主要使用vue.js的框架,今天的這篇文章比較基礎(chǔ),我之前在剛剛接觸vue項(xiàng)目的時(shí)候并沒(méi)有思考過(guò)關(guān)于項(xiàng)目是究竟怎么運(yùn)行起來(lái)的,只...
摘要:在回調(diào)隊(duì)列中,函數(shù)等待調(diào)用棧為空,因?yàn)槊總€(gè)語(yǔ)句都執(zhí)行一次。最后一個(gè)運(yùn)行,并且從調(diào)用棧中彈出。它將回調(diào)以先進(jìn)先出順序移動(dòng)到調(diào)用棧并執(zhí)行。 翻譯:瘋狂的技術(shù)宅原文: https://medium.freecodecamp.o... 本文首發(fā)微信公眾號(hào):前端先鋒歡迎關(guān)注,每天都給你推送新鮮的前端技術(shù)文章 Node.js 是一個(gè) JavaScript 運(yùn)行時(shí)環(huán)境。聽(tīng)起來(lái)還不錯(cuò),不過(guò)這究竟...
摘要:文章的第二部分涵蓋了內(nèi)存管理的概念,不久后將發(fā)布。的標(biāo)準(zhǔn)化工作是由國(guó)際組織負(fù)責(zé)的,相關(guān)規(guī)范被稱為或者。隨著分析器和編譯器不斷地更改字節(jié)碼,的執(zhí)行性能逐漸提高。 原文地址:How Does JavaScript Really Work? (Part 1) 原文作者:Priyesh Patel 譯者:Chor showImg(https://segmentfault.com/img...
摘要:究竟是什么是一個(gè)運(yùn)行時(shí)環(huán)境。對(duì)此請(qǐng)求的響應(yīng)需要時(shí)間,但兩個(gè)用戶數(shù)據(jù)請(qǐng)求可以獨(dú)立并同時(shí)執(zhí)行。所以這會(huì)使不太適合多線程任務(wù)。這種非阻塞消除了多線程的需要,因?yàn)榉?wù)器可以同時(shí)處理多個(gè)請(qǐng)求。該事件將等待毫秒,然后回調(diào)函數(shù)。系統(tǒng)事件來(lái)自庫(kù)的核心。 Node.js究竟是什么? Node.js是一個(gè)JavaScript運(yùn)行時(shí)環(huán)境。聽(tīng)起來(lái)不錯(cuò),但這是什么意思?這是如何運(yùn)作的? Node運(yùn)行時(shí)環(huán)境包含執(zhí)...
摘要:究竟是什么是一個(gè)運(yùn)行時(shí)環(huán)境。對(duì)此請(qǐng)求的響應(yīng)需要時(shí)間,但兩個(gè)用戶數(shù)據(jù)請(qǐng)求可以獨(dú)立并同時(shí)執(zhí)行。所以這會(huì)使不太適合多線程任務(wù)。這種非阻塞消除了多線程的需要,因?yàn)榉?wù)器可以同時(shí)處理多個(gè)請(qǐng)求。該事件將等待毫秒,然后回調(diào)函數(shù)。系統(tǒng)事件來(lái)自庫(kù)的核心。 Node.js究竟是什么? Node.js是一個(gè)JavaScript運(yùn)行時(shí)環(huán)境。聽(tīng)起來(lái)不錯(cuò),但這是什么意思?這是如何運(yùn)作的? Node運(yùn)行時(shí)環(huán)境包含執(zhí)...
閱讀 1626·2021-10-14 09:43
閱讀 5503·2021-09-07 10:21
閱讀 1275·2019-08-30 15:56
閱讀 2123·2019-08-30 15:53
閱讀 1231·2019-08-30 15:44
閱讀 2010·2019-08-30 15:44
閱讀 1320·2019-08-29 17:24
閱讀 752·2019-08-29 15:19