[SpringBoot] SpringApplication.run 執行流程


作者:王奕然
鏈接:https://www.zhihu.com/question/21346206/answer/101789659
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

建議不要硬着頭皮看spring代碼,本身的代碼800多m,就是不上班開始看也不知道什么時候看完。如果想學學ioc,控制反轉這些建議看看jodd項目,比較簡練,但是我仍然不建議過多的看這些框架的代碼,因為這些代碼要完成任務需要很多瑣碎的類實現,比如讀取某個包下面的所有類,解析class的頭文件,反射各種信息,再加上封裝,很有可能在讀源碼的過程中掉到各種細節里出不來,所以讀這種源碼要事無巨細,理解原理即可。
基本原理其實就是通過反射解析類及其類的各種信息,包括構造器、方法及其參數,屬性。然后將其封裝成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     }

該方法流程為:

  1. 通過SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法,在 META-INF/spring.factories 文件下查找ApplicationContextInitializer類型對應的資源名稱。
  2. 實例化上面的資源信息(初始化器)。
  3. 對初始化器根據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   }
    getRunListeners(args) 方法:
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     }

大致流程為:

  1. 創建ConfigurableEnvironment對象。
  2. 配置environment變量。
  3. 發布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” 文件中加上配置。

  1. 綁定environmentSpringApplication上。
  • createApplicationContext 方法

該方法會根據webApplicationType類型,創建不同的ConfigurableApplicationContextSpring 應用上下文:

 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     }

大致流程為:

  1. context的屬性做賦值,如設置環境變量,調用初始化器來初始化context
  2. 獲取所有配置源信息,包括 Configuration Class、類名、包名及Spring XML 配置資源路徑信息。
  3. 加載 Spring 應用上下文配置源。將BeanDefinition加載到context中。
  4. 發布上下文已准備事件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()方法返回值,而屬性annotatedReaderxmlReadergroovyReader分別為注解驅動實現AnnotatedBeanDefinitionReader、XML 配置實現XmlBeanDefinitionReader和 Groovy 實現GroovyBeanDefinitionReader。其中AnnotatedBeanDefinitionReaderClassPathBeanDefinitionScanner配合,形成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 方法的執行流程已經講解完畢。下面我們來整理一下大體的步驟:

  1. 初始化 SpringApplication 實例:決定web應用類型、加載初始化器和監聽器、推斷 main 方法的定義類。
  2. 通過 SpringFactoriesLoader 加載的 SpringApplicationRunListener,調用它們的 started 方法。
  3. 創建並配置當前 Spring Boot 應用將要使用的 Environment,如 applocation.properties 文件和外部配置。
  4. 根據 Web 服務類型創建不同的 Spring 應用上下文,並將之前准備好的 Environment 設置給 Spring 應用上下文 ApplicationContext 使用。
  5. 遍歷初始化器,對 ApplicationContext 進行初始化操作。
  6. 加載所有資源,如 Configuration Class、類名、包名以及 Spring XML 配置資源路徑,將所有 BeanDefinition 加載至 ApplicationContext。
  7. 初始化上下文 refresh(),進行自動裝配,初始化 Ioc 容器等操作。
  8. 尋找當前 ApplicationContext 中是否注冊有 CommandLineRunner 或者 ApplicationRunner,如果有,則遍歷執行它們。

     

作者:habit_learning
鏈接:https://www.jianshu.com/p/c2789f1548ab
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM