摘要:你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來程序會(huì)往哪里流動(dòng)。接下來再看看運(yùn)行時(shí)堆棧,看看一個(gè)請求的調(diào)用棧有多深。就是如此被自動(dòng)裝配進(jìn)的。
摘要: 神奇的SpringBoot。
原文:SpringBoot 究竟是如何跑起來的?
作者:老錢
Fundebug經(jīng)授權(quán)轉(zhuǎn)載,版權(quán)歸原作者所有。
不得不說 SpringBoot 太復(fù)雜了,我本來只想研究一下 SpringBoot 最簡單的 HelloWorld 程序是如何從 main 方法一步一步跑起來的,但是這卻是一個(gè)相當(dāng)深的坑。你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來程序會(huì)往哪里流動(dòng)。這個(gè)不同于我研究過去的 Go 語言、Python 語言框架,它們通常都非常直接了當(dāng),設(shè)計(jì)上清晰易懂,代碼寫起來簡單,里面的實(shí)現(xiàn)同樣也很簡單。但是 SpringBoot 不是,它的外表輕巧簡單,但是它的里面就像一只巨大的怪獸,這只怪獸有千百只腳把自己纏繞在一起,把愛研究源碼的讀者繞的暈頭轉(zhuǎn)向。但是這 Java 編程的世界 SpringBoot 就是老大哥,你卻不得不服。即使你的心中有千萬頭草泥馬在奔跑,但是它就是天下第一。如果你是一個(gè)學(xué)院派的程序員,看到這種現(xiàn)象你會(huì)懷疑人生,你不得不接受一個(gè)規(guī)則 —— 受市場最歡迎的未必就是設(shè)計(jì)的最好的,里面夾雜著太多其它的非理性因素。
經(jīng)過了一番痛苦的折磨,我還是把 SpringBoot 的運(yùn)行原理摸清楚了,這里分享給大家。
Hello World首先我們看看 SpringBoot 簡單的 Hello World 代碼,就兩個(gè)文件 HelloControll.java 和 Application.java,運(yùn)行 Application.java 就可以跑起來一個(gè)簡單的 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)我打開瀏覽器看到服務(wù)器正常地將輸出呈現(xiàn)在瀏覽器的時(shí)候,我不禁大呼 —— SpringBoot 真他媽太簡單了。
但是問題來了,在 Application 的 main 方法里我壓根沒有任何地方引用 HelloController 類,那么它的代碼又是如何被服務(wù)器調(diào)用起來的呢?這就需要深入到 SpringApplication.run() 方法中看個(gè)究竟了。不過即使不看代碼,我們也很容易有這樣的猜想,SpringBoot 肯定是在某個(gè)地方掃描了當(dāng)前的 package,將帶有 RestController 注解的類作為 MVC 層的 Controller 自動(dòng)注冊進(jìn)了 Tomcat Server。
還有一個(gè)讓人不爽的地方是 SpringBoot 啟動(dòng)太慢了,一個(gè)簡單的 Hello World 啟動(dòng)居然還需要長達(dá) 5 秒,要是再復(fù)雜一些的項(xiàng)目這樣龜漫的啟動(dòng)速度那真是不好想象了。
再抱怨一下,這個(gè)簡單的 HelloWorld 雖然 pom 里只配置了一個(gè) maven 依賴,但是傳遞下去,它一共依賴了 36 個(gè) jar 包,其中以 spring 開頭的 jar 包有 15 個(gè)。說這是依賴地獄真一點(diǎn)不為過。
批評到這里就差不多了,下面就要正是進(jìn)入主題了,看看 SpringBoot 的 main 方法到底是如何跑起來的。
SpringBoot 的堆棧了解 SpringBoot 運(yùn)行的最簡單的方法就是看它的調(diào)用堆棧,下面這個(gè)啟動(dòng)調(diào)用堆棧還不是太深,我沒什么可抱怨的。
public class TomcatServer { @Override public void start() throws WebServerException { ... } }
接下來再看看運(yùn)行時(shí)堆棧,看看一個(gè) HTTP 請求的調(diào)用棧有多深。不看不知道一看嚇了一大跳!
我通過將 IDE 窗口全屏化,并將其它的控制臺窗口源碼窗口統(tǒng)統(tǒng)最小化,總算勉強(qiáng)一個(gè)屏幕裝下了整個(gè)調(diào)用堆棧。
不過轉(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 文件解包出來后再密密麻麻的塞進(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)勢在于最終的 jar 包結(jié)構(gòu)很清晰,所有的依賴一目了然。如果使用 maven shade 會(huì)將所有的 class 文件混亂堆積在一起,是無法看清其中的依賴。而最終生成的 jar 包在體積上兩也者幾乎是相等的。
在運(yùn)行機(jī)制上,使用 FatJar 技術(shù)運(yùn)行程序是需要對 jar 包進(jìn)行改造的,它還需要自定義自己的 ClassLoader 來加載 jar 包里面 lib 目錄中嵌套的 jar 包中的類。我們可以對比一下兩者的 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ù)對應(yīng)的類才是真正的業(yè)務(wù) main 方法入口。我們再看看這個(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 來另啟一個(gè)多帶帶的線程來加載 MainClass 并運(yùn)行。
又一個(gè)問題來了,當(dāng) JVM 遇到一個(gè)不認(rèn)識的類,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)先使用它來加載。如果不是內(nèi)置的就去查找這個(gè)類對應(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)系,如果緩存中找不到對應(yīng)的包名,就必須去 jar 包中挨個(gè)遍歷搜尋,這個(gè)就比較緩慢了。不過同一個(gè)包名只會(huì)搜尋一次,下一次就可以直接從緩存中得到對應(yīng)的內(nèi)嵌 jar 包路徑。
深層 jar 包的內(nèi)嵌 class 的 URL 路徑長下面這樣,使用感嘆號 ! 分割
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ì)還是很有意思的,它本身很輕量級,代碼邏輯很獨(dú)立沒有其它依賴,它也是 SpringBoot 值得欣賞的點(diǎn)之一。
HelloController 自動(dòng)注冊還剩下最后一個(gè)問題,那就是 HelloController 沒有被代碼引用,它是如何注冊到 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)注解的屬性。
那注解是又是如何傳遞的呢?
@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 注解來定義的,ComponentScan 注解會(huì)定義一個(gè)被掃描的包名稱,如果沒有顯示定義那就是當(dāng)前的包路徑。SpringBoot 在遇到 ComponentScan 注解時(shí)會(huì)掃描對應(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 對 Controller 注解進(jìn)行了特殊處理,它會(huì)將 Controller 注解的類當(dāng)成 URL 處理器注冊到 Servlet 的請求處理器中,在創(chuàng)建 Tomcat Server 時(shí),會(huì)將請求處理器傳遞進(jìn)去。HelloController 就是如此被自動(dòng)裝配進(jìn) Tomcat 的。
掃描處理注解是一個(gè)非常繁瑣骯臟的活計(jì),特別是這種用注解來注解注解(繞口)的高級使用方法,這種方法要少用慎用。SpringBoot 中有大量的注解相關(guān)代碼,企圖理解這些代碼是乏味無趣的沒有必要的,它只會(huì)把你的本來清醒的腦袋搞暈。SpringBoot 對于習(xí)慣使用的同學(xué)來說它是非常方便的,但是其內(nèi)部實(shí)現(xiàn)代碼不要輕易模仿,那絕對算不上模范 Java 代碼。
最后老錢表示自己真的很討厭 SpringBoot 這只怪獸,但是很無奈,這個(gè)世界人人都在使用它。這就好比老人們常常告誡年輕人的那句話:如果你改變不了世界,那就先適應(yīng)這個(gè)世界吧!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/72897.html
摘要:自己在前端的開發(fā)中主要使用的框架,今天的這篇文章比較基礎(chǔ),我之前在剛剛接觸項(xiàng)目的時(shí)候并沒有思考過關(guān)于項(xiàng)目是究竟怎么運(yùn)行起來的,只知道項(xiàng)目就跑起來了,究竟我在輸入這行命令之后項(xiàng)目是怎么運(yùn)行的,分別走了哪幾步,怎么樣才走到生產(chǎn)環(huán)境,什么情況下又 自己在前端的開發(fā)中主要使用vue.js的框架,今天的這篇文章比較基礎(chǔ),我之前在剛剛接觸vue項(xiàng)目的時(shí)候并沒有思考過關(guān)于項(xiàng)目是究竟怎么運(yùn)行起來的,只...
摘要:在回調(diào)隊(duì)列中,函數(shù)等待調(diào)用棧為空,因?yàn)槊總€(gè)語句都執(zhí)行一次。最后一個(gè)運(yùn)行,并且從調(diào)用棧中彈出。它將回調(diào)以先進(jìn)先出順序移動(dòng)到調(diào)用棧并執(zhí)行。 翻譯:瘋狂的技術(shù)宅原文: https://medium.freecodecamp.o... 本文首發(fā)微信公眾號:前端先鋒歡迎關(guān)注,每天都給你推送新鮮的前端技術(shù)文章 Node.js 是一個(gè) JavaScript 運(yùn)行時(shí)環(huán)境。聽起來還不錯(cuò),不過這究竟...
摘要:文章的第二部分涵蓋了內(nèi)存管理的概念,不久后將發(fā)布。的標(biāo)準(zhǔn)化工作是由國際組織負(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)境。對此請求的響應(yīng)需要時(shí)間,但兩個(gè)用戶數(shù)據(jù)請求可以獨(dú)立并同時(shí)執(zhí)行。所以這會(huì)使不太適合多線程任務(wù)。這種非阻塞消除了多線程的需要,因?yàn)榉?wù)器可以同時(shí)處理多個(gè)請求。該事件將等待毫秒,然后回調(diào)函數(shù)。系統(tǒng)事件來自庫的核心。 Node.js究竟是什么? Node.js是一個(gè)JavaScript運(yùn)行時(shí)環(huán)境。聽起來不錯(cuò),但這是什么意思?這是如何運(yùn)作的? Node運(yùn)行時(shí)環(huán)境包含執(zhí)...
摘要:究竟是什么是一個(gè)運(yùn)行時(shí)環(huán)境。對此請求的響應(yīng)需要時(shí)間,但兩個(gè)用戶數(shù)據(jù)請求可以獨(dú)立并同時(shí)執(zhí)行。所以這會(huì)使不太適合多線程任務(wù)。這種非阻塞消除了多線程的需要,因?yàn)榉?wù)器可以同時(shí)處理多個(gè)請求。該事件將等待毫秒,然后回調(diào)函數(shù)。系統(tǒng)事件來自庫的核心。 Node.js究竟是什么? Node.js是一個(gè)JavaScript運(yùn)行時(shí)環(huán)境。聽起來不錯(cuò),但這是什么意思?這是如何運(yùn)作的? Node運(yùn)行時(shí)環(huán)境包含執(zhí)...
閱讀 1632·2021-10-14 09:43
閱讀 5548·2021-09-07 10:21
閱讀 1283·2019-08-30 15:56
閱讀 2132·2019-08-30 15:53
閱讀 1239·2019-08-30 15:44
閱讀 2016·2019-08-30 15:44
閱讀 1326·2019-08-29 17:24
閱讀 759·2019-08-29 15:19