鏈接:https://www.zhihu.com/question/21346206/answer/101789659
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
基本原理其實就是通過反射解析類及其類的各種信息,包括構造器、方法及其參數,屬性。然后將其封裝成bean定義信息類、constructor信息類、method信息類、property信息類,最終放在一個map里,也就是所謂的container,池等等,其實就是個map。。汗。。。。當你寫好配置文件,啟動項目后,框架會先按照你的配置文件找到那個要scan的包,然后解析包里面的所有類,找到所有含有@bean,@service等注解的類,利用反射解析它們,包括解析構造器,方法,屬性等等,然后封裝成各種信息類放到一個map里。每當你需要一個bean的時候,框架就會從container找是不是有這個類的定義啊?如果找到則通過構造器new出來(這就是控制反轉,不用你new,框架幫你new),再在這個類找是不是有要注入的屬性或者方法,比如標有@autowired的屬性,如果有則還是到container找對應的解析類,new出對象,並通過之前解析出來的信息類找到setter方法,然后用該方法注入對象(這就是依賴注入)。如果其中有一個類container里沒找到,則拋出異常,比如常見的spring無法找到該類定義,無法wire的異常。還有就是嵌套bean則用了一下遞歸,container會放到servletcontext里面,每次reQuest從servletcontext找這個container即可,不用多次解析類定義。如果bean的scope是singleton,則會重用這個bean不再重新創建,將這個bean放到一個map里,每次用都先從這個map里面找。如果scope是session,則該bean會放到session里面。僅此而已,沒必要花更多精力。建議還是多看看底層的知識。
不得不說 SpringBoot 太復雜了,我本來只想研究一下 SpringBoot 最簡單的 HelloWorld 程序是如何從 main 方法一步一步跑起來的,但是這卻是一個相當深的坑。你可以試着沿着調用棧代碼一層一層的深入進去,如果你不打斷點,你根本不知道接下來程序會往哪里流動。這個不同於我研究過去的 Go 語言、Python 語言框架,它們通常都非常直接了當,設計上清晰易懂,代碼寫起來簡單,里面的實現同樣也很簡單。但是 SpringBoot 不是,它的外表輕巧簡單,但是它的里面就像一只巨大的怪獸,這只怪獸有千百只腳把自己纏繞在一起,把愛研究源碼的讀者繞的暈頭轉向。但是這 Java 編程的世界 SpringBoot 就是老大哥,你卻不得不服。即使你的心中有千萬頭草泥馬在奔跑,但是它就是天下第一。如果你是一個學院派的程序員,看到這種現象你會懷疑人生,你不得不接受一個規則 —— 受市場最歡迎的未必就是設計的最好的,里面夾雜着太多其它的非理性因素。
經過了一番痛苦的折磨,我還是把 SpringBoot 的運行原理摸清楚了,這里分享給大家。
Hello World
首先我們看看 SpringBoot 簡單的 Hello World 代碼,就兩個文件 HelloControll.java 和 Application.java,運行 Application.java 就可以跑起來一個簡單的 RESTFul Web 服務器了。
1 // HelloController.java 2 package hello; 3 4 import org.springframework.web.bind.annotation.RestController; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 7 @RestController 8 public class HelloController { 9 10 @RequestMapping("/") 11 public String index() { 12 return "Greetings from Spring Boot!"; 13 } 14 15 } 16 17 // Application.java 18 package hello; 19 20 import org.springframework.boot.SpringApplication; 21 import org.springframework.boot.autoconfigure.SpringBootApplication; 22 23 @SpringBootApplication 24 public class Application { 25 26 public static void main(String[] args) { 27 SpringApplication.run(Application.class, args); 28 } 29 30 }
當我打開瀏覽器看到服務器正常地將輸出呈現在瀏覽器的時候,我不禁大呼 —— 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 運行的最簡單的方法就是看它的調用堆棧,下面這個啟動調用堆棧還不是太深,我沒什么可抱怨的。
public class TomcatServer { @Override public void start() throws WebServerException { ... } }
接下來再看看運行時堆棧,看看一個 HTTP 請求的調用棧有多深。不看不知道一看嚇了一大跳!
我通過將 IDE 窗口全屏化,並將其它的控制台窗口源碼窗口統統最小化,總算勉強一個屏幕裝下了整個調用堆棧。
不過轉念一想,這也不怪 SpringBoot,絕大多數都是 Tomcat 的調用堆棧,跟 SpringBoot 相關的只有不到 10 層。
探索 ClassLoader
SpringBoot 還有一個特色的地方在於打包時它使用了 FatJar 技術將所有的依賴 jar 包一起放進了最終的 jar 包中的 BOOT-INF/lib 目錄中,當前項目的 class 被統一放到了 BOOT-INF/classes 目錄中。
1 <build> 2 <plugins> 3 <plugin> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-maven-plugin</artifactId> 6 </plugin> 7 </plugins> 8 </build>
這不同於我們平時經常使用的 maven shade 插件,將所有的依賴 jar 包中的 class 文件解包出來后再密密麻麻的塞進統一的 jar 包中。下面我們將 springboot 打包的 jar 包解壓出來看看它的目錄結構。
├── 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
這種打包方式的優勢在於最終的 jar 包結構很清晰,所有的依賴一目了然。如果使用 maven shade 會將所有的 class 文件混亂堆積在一起,是無法看清其中的依賴。而最終生成的 jar 包在體積上兩也者幾乎是相等的。
在運行機制上,使用 FatJar 技術運行程序是需要對 jar 包進行改造的,它還需要自定義自己的 ClassLoader 來加載 jar 包里面 lib 目錄中嵌套的 jar 包中的類。我們可以對比一下兩者的 MANIFEST 文件就可以看出明顯差異
1 // Generated by Maven Shade Plugin 2 Manifest-Version: 1.0 3 Implementation-Title: gs-spring-boot 4 Implementation-Version: 0.1.0 5 Built-By: qianwp 6 Implementation-Vendor-Id: org.springframework 7 Created-By: Apache Maven 3.5.4 8 Build-Jdk: 1.8.0_191 9 Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo 10 ot-starter-parent/gs-spring-boot 11 Main-Class: hello.Application 12 13 // Generated by SpringBootLoader Plugin 14 Manifest-Version: 1.0 15 Implementation-Title: gs-spring-boot 16 Implementation-Version: 0.1.0 17 Built-By: qianwp 18 Implementation-Vendor-Id: org.springframework 19 Spring-Boot-Version: 2.0.5.RELEASE 20 Main-Class: org.springframework.boot.loader.JarLauncher 21 Start-Class: hello.Application 22 Spring-Boot-Classes: BOOT-INF/classes/ 23 Spring-Boot-Lib: BOOT-INF/lib/ 24 Created-By: Apache Maven 3.5.4 25 Build-Jdk: 1.8.0_191 26 Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo 27 ot-starter-parent/gs-spring-boot
SpringBoot 將 jar 包中的 Main-Class 進行了替換,換成了 JarLauncher。還增加了一個 Start-Class 參數,這個參數對應的類才是真正的業務 main 方法入口。我們再看看這個 JarLaucher 具體干了什么
1 public class JarLauncher{ 2 ... 3 static void main(String[] args) { 4 new JarLauncher().launch(args); 5 } 6 7 protected void launch(String[] args) { 8 try { 9 JarFile.registerUrlProtocolHandler(); 10 ClassLoader cl = createClassLoader(getClassPathArchives()); 11 launch(args, getMainClass(), cl); 12 } 13 catch (Exception ex) { 14 ex.printStackTrace(); 15 System.exit(1); 16 } 17 } 18 19 protected void launch(String[] args, String mcls, ClassLoader cl) { 20 Runnable runner = createMainMethodRunner(mcls, args, cl); 21 Thread runnerThread = new Thread(runner); 22 runnerThread.setContextClassLoader(classLoader); 23 runnerThread.setName(Thread.currentThread().getName()); 24 runnerThread.start(); 25 } 26 27 } 28 29 class MainMethodRunner { 30 @Override 31 public void run() { 32 try { 33 Thread th = Thread.currentThread(); 34 ClassLoader cl = th.getContextClassLoader(); 35 Class<?> mc = cl.loadClass(this.mainClassName); 36 Method mm = mc.getDeclaredMethod("main", String[].class); 37 if (mm == null) { 38 throw new IllegalStateException(this.mainClassName 39 + " does not have a main method"); 40 } 41 mm.invoke(null, new Object[] { this.args }); 42 } catch (Exception ex) { 43 ex.printStackTrace(); 44 System.exit(1); 45 } 46 } 47 }
從源碼中可以看出 JarLaucher 創建了一個特殊的 ClassLoader,然后由這個 ClassLoader 來另啟一個單獨的線程來加載 MainClass 並運行。
又一個問題來了,當 JVM 遇到一個不認識的類,BOOT-INF/lib 目錄里又有那么多 jar 包,它是如何知道去哪個 jar 包里加載呢?我們繼續看這個特別的 ClassLoader 的源碼
1 class LaunchedURLClassLoader extends URLClassLoader { 2 ... 3 private Class<?> doLoadClass(String name) { 4 if (this.rootClassLoader != null) { 5 return this.rootClassLoader.loadClass(name); 6 } 7 8 findPackage(name); 9 Class<?> cls = findClass(name); 10 return cls; 11 } 12 13 }
這里的 rootClassLoader 就是雙親委派模型里的 ExtensionClassLoader ,JVM 內置的類會優先使用它來加載。如果不是內置的就去查找這個類對應的 Package。
1 private void findPackage(final String name) { 2 int lastDot = name.lastIndexOf('.'); 3 if (lastDot != -1) { 4 String packageName = name.substring(0, lastDot); 5 if (getPackage(packageName) == null) { 6 try { 7 definePackage(name, packageName); 8 } catch (Exception ex) { 9 // Swallow and continue 10 } 11 } 12 } 13 } 14 15 private final HashMap<String, Package> packages = new HashMap<>(); 16 17 protected Package getPackage(String name) { 18 Package pkg; 19 synchronized (packages) { 20 pkg = packages.get(name); 21 } 22 if (pkg == null) { 23 if (parent != null) { 24 pkg = parent.getPackage(name); 25 } else { 26 pkg = Package.getSystemPackage(name); 27 } 28 if (pkg != null) { 29 synchronized (packages) { 30 Package pkg2 = packages.get(name); 31 if (pkg2 == null) { 32 packages.put(name, pkg); 33 } else { 34 pkg = pkg2; 35 } 36 } 37 } 38 } 39 return pkg; 40 } 41 42 private void definePackage(String name, String packageName) { 43 String path = name.replace('.', '/').concat(".class"); 44 for (URL url : getURLs()) { 45 try { 46 if (url.getContent() instanceof JarFile) { 47 JarFile jf= (JarFile) url.getContent(); 48 if (jf.getJarEntryData(path) != null && jf.getManifest() != null) { 49 definePackage(packageName, jf.getManifest(), url); 50 return null; 51 } 52 } 53 } catch (IOException ex) { 54 // Ignore 55 } 56 } 57 return null; 58 }
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 的注解就好比代碼注釋,本身只有屬性,沒有邏輯,注解相應的功能由散落在其它地方的代碼來完成,需要分析被注解的類結構才可以得到相應注解的屬性。
那注解是又是如何傳遞的呢?
@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,這個注解又是由ComponentScan 注解來定義的,ComponentScan 注解會定義一個被掃描的包名稱,如果沒有顯示定義那就是當前的包路徑。SpringBoot 在遇到 ComponentScan 注解時會掃描對應包路徑下面的所有 Class,根據這些 Class 上標注的其它注解繼續進行后續處理。當它掃到 HelloController 類時發現它標注了 RestController 注解。
@RestController public class HelloController { ... } @Controller public @interface RestController { }
而 RestController 注解又標注了 Controller 注解。SpringBoot 對 Controller 注解進行了特殊處理,它會將 Controller 注解的類當成 URL 處理器注冊到 Servlet 的請求處理器中,在創建 Tomcat Server 時,會將請求處理器傳遞進去。HelloController 就是如此被自動裝配進 Tomcat 的。
掃描處理注解是一個非常繁瑣骯臟的活計,特別是這種用注解來注解注解(繞口)的高級使用方法,這種方法要少用慎用。SpringBoot 中有大量的注解相關代碼,企圖理解這些代碼是乏味無趣的沒有必要的,它只會把你的本來清醒的腦袋搞暈。SpringBoot 對於習慣使用的同學來說它是非常方便的,但是其內部實現代碼不要輕易模仿,那絕對算不上模范 Java 代碼。
用過 SpringBoot 的同學都知道,其程序的啟動類是在一個main
方法中調用SpringApplication.run
方法執行的,如:
@SpringBootApplication public class SpringApplicationBootstrap { public static void main(String[] args) { SpringApplication.run(SpringApplicationBootstrap.class, args); } }
那么,這里面到底做了什么呢?本篇文章將深入源碼,帶你一起探究底層實現。
SpringApplication 初始化階段
進入到SpringApplication.run
方法,其首先會創建一個SpringApplication
對象,我們看其構造函數:
1 public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { 2 this.resourceLoader = resourceLoader; 3 Assert.notNull(primarySources, "PrimarySources must not be null"); 4 // primarySources 為 run 方法傳入的引導類 5 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); 6 // 推斷web應用類 7 this.webApplicationType = deduceWebApplicationType(); 8 // 初始化 initializers 屬性 9 setInitializers((Collection) getSpringFactoriesInstances( 10 ApplicationContextInitializer.class)); 11 // 初始化監聽器 12 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 13 // 推斷應用引導類 14 this.mainApplicationClass = deduceMainApplicationClass(); 15 }
我們先看推斷web應用類的方法deduceWebApplicationType()
:
1 private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", 2 "org.springframework.web.context.ConfigurableWebApplicationContext" }; 3 4 private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework." 5 + "web.reactive.DispatcherHandler"; 6 7 private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework." 8 + "web.servlet.DispatcherServlet"; 9 10 private WebApplicationType deduceWebApplicationType() { 11 if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) 12 && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) { 13 return WebApplicationType.REACTIVE; 14 } 15 for (String className : WEB_ENVIRONMENT_CLASSES) { 16 if (!ClassUtils.isPresent(className, null)) { 17 return WebApplicationType.NONE; 18 } 19 } 20 return WebApplicationType.SERVLET; 21 }
根據 classpath 下是否存在某個特征類來決定是否應該創建一個為 Web 應用使用的ApplicationContext
類型。具體判斷為:
如果僅存在 Reactive 的包,則為
WebApplicationType.REACTIVE
類型;
如果 Servlet 和 Reactive的包都不存在,則為WebApplicationType.NONE
類型;
其他情況都為WebApplicationType.SERVLET
類型。
接下來我們看初始化initializers
屬性的過程,其通過getSpringFactoriesInstances(ApplicationContextInitializer.class)
方法獲取初始化器:
1 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, 2 Class<?>[] parameterTypes, Object... args) { 3 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 4 // Use names and ensure unique to protect against duplicates 5 Set<String> names = new LinkedHashSet<>( 6 SpringFactoriesLoader.loadFactoryNames(type, classLoader)); 7 List<T> instances = createSpringFactoriesInstances(type, parameterTypes, 8 classLoader, args, names); 9 AnnotationAwareOrderComparator.sort(instances); 10 return instances; 11 }
該方法流程為:
- 通過
SpringFactoriesLoader.loadFactoryNames(type, classLoader)
方法,在 META-INF/spring.factories 文件下查找ApplicationContextInitializer
類型對應的資源名稱。 - 實例化上面的資源信息(初始化器)。
- 對初始化器根據
Ordered
接口或者@Order
注解進行排序。
同理,初始化listeners
監聽器也是類似的,這里不再累贅。
SpringApplication 初始化階段的最后一步是推斷引導類deduceMainApplicationClass()
:
1 private Class<?> deduceMainApplicationClass() { 2 try { 3 StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); 4 for (StackTraceElement stackTraceElement : stackTrace) { 5 if ("main".equals(stackTraceElement.getMethodName())) { 6 return Class.forName(stackTraceElement.getClassName()); 7 } 8 } 9 } 10 catch (ClassNotFoundException ex) { 11 // Swallow and continue 12 } 13 return null; 14 }
其將調用棧中main
方法所在的類作為引導類。
SpringApplication 運行階段
SpringApplication 運行階段屬於核心過程,完全圍繞 run(String...) 方法展開。該過程結合初始化階段完成的狀態,進一步完善運行時所需要准備的資源,隨后啟動 Spring 應用上下文。在此期間伴隨着 Spring Boot 和 Spring 事件的觸發,形成完整的 SpringApplication 生命周期。因此,下面將圍繞以下三個子議題進行討論。
- SpringApplication 准備階段
- ApplicationContext 啟動階段
- ApplicationContext 啟動后階段
1. SpringApplication 准備階段
本階段屬於 ApplicationContext 啟動階段的前一階段,設計的范圍從 run(String...)
方法調用開始,到refreshContext(ConfigurableApplicationContext)
調用前:
1 public ConfigurableApplicationContext run(String... args) { 2 //記錄程序運行時間 3 StopWatch stopWatch = new StopWatch(); 4 stopWatch.start(); 5 //Spring 應用的上下文 6 ConfigurableApplicationContext context = null; 7 Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); 8 configureHeadlessProperty(); 9 // 獲取 SpringApplicationRunListeners 10 SpringApplicationRunListeners listeners = getRunListeners(args); 11 listeners.starting(); 12 try { 13 // 創建 ApplicationArguments 對象 14 ApplicationArguments applicationArguments = new DefaultApplicationArguments( 15 args); 16 // 加載屬性配置 17 ConfigurableEnvironment environment = prepareEnvironment(listeners, 18 applicationArguments); 19 // 處理需要忽略的Bean 20 configureIgnoreBeanInfo(environment); 21 // 打印 banner 22 Banner printedBanner = printBanner(environment); 23 // 創建 Spring 應用上下文 24 context = createApplicationContext(); 25 // 實例化 SpringBootExceptionReporter,用來報告關於啟動過程中的錯誤 26 exceptionReporters = getSpringFactoriesInstances( 27 SpringBootExceptionReporter.class, 28 new Class[] { ConfigurableApplicationContext.class }, context); 29 // 應用上下文的准備階段 30 prepareContext(context, environment, listeners, applicationArguments, 31 printedBanner); 32 // 刷新應用上下文(自動裝配,初始化 IOC 容器) 33 refreshContext(context); 34 ... 35 } 36 }
1 private SpringApplicationRunListeners getRunListeners(String[] args) { 2 Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class }; 3 return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances( 4 SpringApplicationRunListener.class, types, this, args)); 5 }
該方法會通過getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)
方法,獲取 META-INF/spring.factories 文件下SpringApplicationRunListener
對應的資源,並且實例化這些資源:
1 # Run Listeners 2 org.springframework.boot.SpringApplicationRunListener=\ 3 org.springframework.boot.context.event.EventPublishingRunListener
我們發現,其有且僅有一個實現類EventPublishingRunListener
:
1 public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { 2 3 private final SpringApplication application; 4 5 private final String[] args; 6 7 private final SimpleApplicationEventMulticaster initialMulticaster; 8 9 public EventPublishingRunListener(SpringApplication application, String[] args) { 10 this.application = application; 11 this.args = args; 12 this.initialMulticaster = new SimpleApplicationEventMulticaster(); 13 for (ApplicationListener<?> listener : application.getListeners()) { 14 this.initialMulticaster.addApplicationListener(listener); 15 } 16 } 17 18 @Override 19 public int getOrder() { 20 return 0; 21 } 22 23 @Override 24 public void starting() { 25 this.initialMulticaster.multicastEvent( 26 new ApplicationStartingEvent(this.application, this.args)); 27 } 28 29 @Override 30 public void environmentPrepared(ConfigurableEnvironment environment) { 31 this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent( 32 this.application, this.args, environment)); 33 } 34 35 @Override 36 public void contextPrepared(ConfigurableApplicationContext context) { 37 38 } 39 40 @Override 41 public void contextLoaded(ConfigurableApplicationContext context) { 42 for (ApplicationListener<?> listener : this.application.getListeners()) { 43 if (listener instanceof ApplicationContextAware) { 44 ((ApplicationContextAware) listener).setApplicationContext(context); 45 } 46 context.addApplicationListener(listener); 47 } 48 this.initialMulticaster.multicastEvent( 49 new ApplicationPreparedEvent(this.application, this.args, context)); 50 } 51 52 @Override 53 public void started(ConfigurableApplicationContext context) { 54 context.publishEvent( 55 new ApplicationStartedEvent(this.application, this.args, context)); 56 } 57 58 @Override 59 public void running(ConfigurableApplicationContext context) { 60 context.publishEvent( 61 new ApplicationReadyEvent(this.application, this.args, context)); 62 } 63 64 @Override 65 public void failed(ConfigurableApplicationContext context, Throwable exception) { 66 ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, 67 this.args, context, exception); 68 if (context != null && context.isActive()) { 69 // Listeners have been registered to the application context so we should 70 // use it at this point if we can 71 context.publishEvent(event); 72 } 73 else { 74 // An inactive context may not have a multicaster so we use our multicaster to 75 // call all of the context's listeners instead 76 if (context instanceof AbstractApplicationContext) { 77 for (ApplicationListener<?> listener : ((AbstractApplicationContext) context) 78 .getApplicationListeners()) { 79 this.initialMulticaster.addApplicationListener(listener); 80 } 81 } 82 this.initialMulticaster.setErrorHandler(new LoggingErrorHandler()); 83 this.initialMulticaster.multicastEvent(event); 84 } 85 } 86 ... 87 88 }
在實例化EventPublishingRunListener
的過程中,會給它最重要的屬性initialMulticaster
賦值,其類型是SimpleApplicationEventMulticaster
。接着遍歷 SpringApplication 初始化階段的listeners
監聽器集合,將監聽器存入其關聯的ListenerRetriever#applicationListeners
屬性中。
了解 Spring 事件監聽機制的同學應該對SimpleApplicationEventMulticaster
不陌生,它是ApplicationEvent
事件的發布者。Spring Boot 的事件監聽機制也是如出一轍,具體可參考我的另一篇文章 深入理解 Spring 的事件發布監聽機制。
於是接下來調用listeners.starting()
方法就會通過其內部的initialMulticaster
屬性發布ApplicationStartingEvent
事件。
- prepareEnvironment(listeners,applicationArguments) 方法:
加載屬性配置。執行完成后,所有的environment
的屬性都會加載進來,包括 application.properties 和外部的屬性配置。
1 private ConfigurableEnvironment prepareEnvironment( 2 SpringApplicationRunListeners listeners, 3 ApplicationArguments applicationArguments) { 4 // 創建 ConfigurableEnvironment 對象 5 ConfigurableEnvironment environment = getOrCreateEnvironment(); 6 // 配置 ConfigurableEnvironment 7 configureEnvironment(environment, applicationArguments.getSourceArgs()); 8 // 發布 ApplicationEnvironmentPreparedEvent 事件 9 listeners.environmentPrepared(environment); 10 // 將 ConfigurableEnvironment 綁定到 SpringApplication 中 11 bindToSpringApplication(environment); 12 if (this.webApplicationType == WebApplicationType.NONE) { 13 environment = new EnvironmentConverter(getClassLoader()) 14 .convertToStandardEnvironmentIfNecessary(environment); 15 } 16 ConfigurationPropertySources.attach(environment); 17 return environment; 18 }
大致流程為:
- 創建
ConfigurableEnvironment
對象。 - 配置
environment
變量。 - 發布
ApplicationEnvironmentPreparedEvent
事件。
其對應的監聽器為ConfigFileApplicationListener
,當接收到上面的事件時,加載並實例化 “META-INF/spring.factories” 文件中EnvironmentPostProcessor
類型的實現類,遍歷並執行其postProcessEnvironment
方法。值得注意的是,ConfigFileApplicationListener
自身也是EnvironmentPostProcessor
的實現類,於是也會執行其postProcessEnvironment
方法:
1 public void postProcessEnvironment(ConfigurableEnvironment environment, 2 SpringApplication application) { 3 // 將配置文件信息添加到 environment 中 4 addPropertySources(environment, application.getResourceLoader()); 5 configureIgnoreBeanInfo(environment); 6 // 將 environment 綁定到 Spring 應用上下文中 7 bindToSpringApplication(environment, application); 8 }
該方法的目的是,加載配置文件信息至enviroment
,並將enviroment
綁定到 Spring 應用上下文中。
當我們需要在配置文件加載完成之后做一些事情的話,我們就可以自定義一個EnvironmentPostProcessor
的實現類,操作邏輯寫在postProcessEnvironment
方法中,當然,別忘了在你的 “META-INF/spring.factories” 文件中加上配置。
- 綁定
environment
到SpringApplication
上。
- createApplicationContext 方法
該方法會根據webApplicationType
類型,創建不同的ConfigurableApplicationContext
Spring 應用上下文:
1 protected ConfigurableApplicationContext createApplicationContext() { 2 Class<?> contextClass = this.applicationContextClass; 3 if (contextClass == null) { 4 try { 5 switch (this.webApplicationType) { 6 case SERVLET: 7 contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS); 8 break; 9 case REACTIVE: 10 contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); 11 break; 12 default: 13 contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); 14 } 15 } 16 catch (ClassNotFoundException ex) { 17 throw new IllegalStateException( 18 "Unable create a default ApplicationContext, " 19 + "please specify an ApplicationContextClass", 20 ex); 21 } 22 } 23 return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); 24 }
我們以 SERVLET 類型為例,它會創建AnnotationConfigServletWebServerApplicationContext
應用上下文實例。
- 獲取 Spring 異常報告器
getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context)
方法,獲取 META-INF/spring.factories 文件下類型為SpringBootExceptionReporter
的資源實例:
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers
其實現類有且僅有一個,即FailureAnalyzers
。
- prepareContext 方法
Spring 應用上下文啟動前的准備工作:
1 private void prepareContext(ConfigurableApplicationContext context, 2 ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, 3 ApplicationArguments applicationArguments, Banner printedBanner) { 4 //設置 context 的 environment 屬性 5 context.setEnvironment(environment); 6 // Spring 應用上下文的后置處理 7 postProcessApplicationContext(context); 8 // 運用 Spring 應用上下文初始化器 9 applyInitializers(context); 10 listeners.contextPrepared(context); 11 if (this.logStartupInfo) { 12 logStartupInfo(context.getParent() == null); 13 logStartupProfileInfo(context); 14 } 15 16 // Add boot specific singleton beans 17 context.getBeanFactory().registerSingleton("springApplicationArguments", 18 applicationArguments); 19 if (printedBanner != null) { 20 context.getBeanFactory().registerSingleton("springBootBanner", printedBanner); 21 } 22 23 // Load the sources 24 Set<Object> sources = getAllSources(); 25 Assert.notEmpty(sources, "Sources must not be empty"); 26 // 加載 BeanDefinition 27 load(context, sources.toArray(new Object[0])); 28 listeners.contextLoaded(context); 29 }
大致流程為:
- 給
context
的屬性做賦值,如設置環境變量,調用初始化器來初始化context
。 - 獲取所有配置源信息,包括 Configuration Class、類名、包名及Spring XML 配置資源路徑信息。
- 加載 Spring 應用上下文配置源。將
BeanDefinition
加載到context
中。 - 發布上下文已准備事件
ApplicationPreparedEvent
。
這里,我們要着重看第三步load(context, sources.toArray(new Object[0]))
:
1 protected void load(ApplicationContext context, Object[] sources) { 2 if (logger.isDebugEnabled()) { 3 logger.debug( 4 "Loading source " + StringUtils.arrayToCommaDelimitedString(sources)); 5 } 6 BeanDefinitionLoader loader = createBeanDefinitionLoader( 7 getBeanDefinitionRegistry(context), sources); 8 if (this.beanNameGenerator != null) { 9 loader.setBeanNameGenerator(this.beanNameGenerator); 10 } 11 if (this.resourceLoader != null) { 12 loader.setResourceLoader(this.resourceLoader); 13 } 14 if (this.environment != null) { 15 loader.setEnvironment(this.environment); 16 } 17 loader.load(); 18 }
該方法將 Spring 應用上下文轉載的任務交給了BeanDefinitionLoader
:
1 class BeanDefinitionLoader { 2 3 private final Object[] sources; 4 5 private final AnnotatedBeanDefinitionReader annotatedReader; 6 7 private final XmlBeanDefinitionReader xmlReader; 8 9 private BeanDefinitionReader groovyReader; 10 11 private final ClassPathBeanDefinitionScanner scanner; 12 13 private ResourceLoader resourceLoader; 14 ... 15 }
BeanDefinitionLoader
組合了多個屬性,第一個屬性為SpringApplication#getAllSources()
方法返回值,而屬性annotatedReader
、xmlReader
、groovyReader
分別為注解驅動實現AnnotatedBeanDefinitionReader
、XML 配置實現XmlBeanDefinitionReader
和 Groovy 實現GroovyBeanDefinitionReader
。其中AnnotatedBeanDefinitionReader
與ClassPathBeanDefinitionScanner
配合,形成AnnotationConfigApplicationContext
掃描和注冊配置類的基礎,隨后這些配置類被解析為 Bean 定義BeanDefinition
:
1 public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry { 2 3 private final AnnotatedBeanDefinitionReader reader; 4 5 private final ClassPathBeanDefinitionScanner scanner; 6 7 8 /** 9 * Create a new AnnotationConfigApplicationContext that needs to be populated 10 * through {@link #register} calls and then manually {@linkplain #refresh refreshed}. 11 */ 12 public AnnotationConfigApplicationContext() { 13 this.reader = new AnnotatedBeanDefinitionReader(this); 14 this.scanner = new ClassPathBeanDefinitionScanner(this); 15 } 16 17 ... 18 19 public void register(Class<?>... annotatedClasses) { 20 Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified"); 21 this.reader.register(annotatedClasses); 22 } 23 24 public void scan(String... basePackages) { 25 Assert.notEmpty(basePackages, "At least one base package must be specified"); 26 this.scanner.scan(basePackages); 27 } 28 ... 29 }
不難看出,Spring Boot 中的BeanDefinitionLoader
是以上BeanDefinition
讀取的綜合實現。當其load()
方法調用時,這些BeanDefinitionReader
類型的屬性各司其職,為 Spring 應用上下文從不同的配置源裝載 Spring Bean 定義(BeanDefinition)。
裝載完BeanDefinition
到 Spring 應用上下文之后,就調用listeners.contextLoaded(context)
方法,發布 Spring 應用上下文已准備ApplicationPreparedEvent
事件,以結束 SpringApplication 准備階段。
2. ApplicationContext 啟動階段
本階段的執行由refreshContext(ConfigurableApplicationContext)
完成,其核心方法是AbstractApplicationContext#refresh
:
1 public void refresh() throws BeansException, IllegalStateException { 2 synchronized (this.startupShutdownMonitor) { 3 // Prepare this context for refreshing. 4 prepareRefresh(); 5 6 // Tell the subclass to refresh the internal bean factory. 7 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); 8 9 // Prepare the bean factory for use in this context. 10 prepareBeanFactory(beanFactory); 11 12 try { 13 // Allows post-processing of the bean factory in context subclasses. 14 postProcessBeanFactory(beanFactory); 15 16 // Invoke factory processors registered as beans in the context. 17 invokeBeanFactoryPostProcessors(beanFactory); 18 19 // Register bean processors that intercept bean creation. 20 registerBeanPostProcessors(beanFactory); 21 22 // Initialize message source for this context. 23 initMessageSource(); 24 25 // Initialize event multicaster for this context. 26 initApplicationEventMulticaster(); 27 28 // Initialize other special beans in specific context subclasses. 29 onRefresh(); 30 31 // Check for listener beans and register them. 32 registerListeners(); 33 34 // Instantiate all remaining (non-lazy-init) singletons. 35 finishBeanFactoryInitialization(beanFactory); 36 37 // Last step: publish corresponding event. 38 finishRefresh(); 39 } 40 41 catch (BeansException ex) { 42 if (logger.isWarnEnabled()) { 43 logger.warn("Exception encountered during context initialization - " + 44 "cancelling refresh attempt: " + ex); 45 } 46 47 // Destroy already created singletons to avoid dangling resources. 48 destroyBeans(); 49 50 // Reset 'active' flag. 51 cancelRefresh(ex); 52 53 // Propagate exception to caller. 54 throw ex; 55 } 56 57 finally { 58 // Reset common introspection caches in Spring's core, since we 59 // might not ever need metadata for singleton beans anymore... 60 resetCommonCaches(); 61 } 62 } 63 }
隨着該方法的執行,Spring Boot 核心特性也隨之啟動,如組件自動裝配、嵌入式容器啟動。了解過 Spring Boot 自動裝配機制的同學應該知道(如不清楚,請參考我的另一篇文章 Spring Boot 自動裝配),在根據應用類型創建不同的 Spring 應用上下文的方法createApplicationContext()
中,會實例化AnnotatedBeanDefinitionReader
對象,該對象的構造方法中,會將ConfigurationClassPostProcessor
封裝成 Spring Bean 定義(BeanDefinition),並將其注入到 Ioc 容器DefaultListableBeanFactory
(其實現了BeanDefinitionRegistry
,具有注入BeanDefinition
的功能)中。於是,在該階段的invokeBeanFactoryPostProcessors(beanFactory)
方法中,就會取出ConfigurationClassPostProcessor
對象,隨后調用其postProcessBeanFactory(beanFactory)
方法進行裝配工作。
3. ApplicationContext 啟動后階段
實際上,SpringApplication#afterRefresh
方法並未給 Spring 應用上下文啟動后階段提供實現,而是將其交給開發人員自行擴展:
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) { }
所有,直接跳過該步,接下來調用listeners.started(context)
方法,發布 Spring 應用上下文已啟動ApplicationStartedEvent
事件。
該階段最后,調用callRunners(context, applicationArguments)
方法,來調用實現了CommandLineRunner
或者ApplicationRunner
接口的類的 run 方法,得以滿足需要在 Spring 應用上下文完全准備完畢后,執行一些操作的場景。
1 private void callRunners(ApplicationContext context, ApplicationArguments args) { 2 List<Object> runners = new ArrayList<>(); 3 runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); 4 runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); 5 AnnotationAwareOrderComparator.sort(runners); 6 for (Object runner : new LinkedHashSet<>(runners)) { 7 if (runner instanceof ApplicationRunner) { 8 callRunner((ApplicationRunner) runner, args); 9 } 10 if (runner instanceof CommandLineRunner) { 11 callRunner((CommandLineRunner) runner, args); 12 } 13 } 14 }
總結
至此,SpringApplciation.run 方法的執行流程已經講解完畢。下面我們來整理一下大體的步驟:
- 初始化 SpringApplication 實例:決定web應用類型、加載初始化器和監聽器、推斷 main 方法的定義類。
- 通過 SpringFactoriesLoader 加載的 SpringApplicationRunListener,調用它們的 started 方法。
- 創建並配置當前 Spring Boot 應用將要使用的 Environment,如 applocation.properties 文件和外部配置。
- 根據 Web 服務類型創建不同的 Spring 應用上下文,並將之前准備好的 Environment 設置給 Spring 應用上下文 ApplicationContext 使用。
- 遍歷初始化器,對 ApplicationContext 進行初始化操作。
- 加載所有資源,如 Configuration Class、類名、包名以及 Spring XML 配置資源路徑,將所有 BeanDefinition 加載至 ApplicationContext。
- 初始化上下文 refresh(),進行自動裝配,初始化 Ioc 容器等操作。
-
尋找當前 ApplicationContext 中是否注冊有 CommandLineRunner 或者 ApplicationRunner,如果有,則遍歷執行它們。
作者:habit_learning
鏈接:https://www.jianshu.com/p/c2789f1548ab
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。