SpringApplication.run執行流程詳解

SpringApplication 將一個典型的 Spring 應用啟動的流程“模板化”(這裡是動詞),在沒有特殊需求的情況下,默認模板化後的執行流程就可以滿足需求了但有特殊需求也沒關係,SpringApplication 在合適的流程結點開放了一系列不同類型的擴展點,我們可以通過這些擴展點對 SpringBoot 程序的啟動和關閉過程進行擴展。最“膚淺”的擴展或者配置是 SpringApplication 通過一系列設置方法(setters)開放的定製方式,比如,我們之前的啟動類的 main 方法中只有一句:

<code>SpringApplication.run(DemoApplication.class,args);/<code>

但如果我們想通過 SpringApplication 的一系列設置方法來擴展啟動行為,則可以用如下方式進行:

<code>public class DemoApplication {
public static void main(String[] args) {
// SpringApplication.run(DemoConfiguration.class, args);
SpringApplication bootstrap = new SpringApplication(Demo - Configuration.class);
bootstrap.setBanner(new Banner() {
@Override
public void printBanner(Environment environment, Class> aClass, PrintStream printStream) {
// 比如打印一個我們喜歡的ASCII Arts字符畫
}
});
bootstrap.setBannerMode(Banner.Mode.CONSOLE);
// 其他定製設置...
bootstrap.run(args);
}
}/<code>

設置自定義 banner 最簡單的方式其實是把 ASCII Art 字符畫放到一個資源文件,然後通過 ResourceBanner 來加載:

<code>bootstrap.setBanner(new ResourceBanner(new ClassPathResource("banner.txt")));/<code>

大部分情況下,SpringApplication 已經提供了很好的默認設置,所以,我們不再對這些表層進行探究了,因為對錶層之下的東西進行探究才是我們的最終目的。

深入探索 SpringApplication 執行流程

SpringApplication 的 run 方法的實現是我們本次旅程的主要線路,該方法的主要流程大體可以歸納如下:

1)如果我們使用的是 SpringApplication 的靜態 run 方法,那麼,這個方法裡面首先需要創建一個 SpringApplication 對象實例,然後調用這個創建好的 SpringApplication 的實例 run方 法。在 SpringApplication 實例初始化的時候,它會提前做幾件事情:

根據 classpath 裡面是否存在某個特徵類(org.springframework.web.context.ConfigurableWebApplicationContext)來決定是否應該創建一個為 Web 應用使用的 ApplicationContext 類型,還是應該創建一個標準 Standalone 應用使用的 ApplicationContext 類型。

使用 SpringFactoriesLoader 在應用的 classpath 中查找並加載所有可用的ApplicationContextInitializer。

使用 SpringFactoriesLoader 在應用的 classpath 中查找並加載所有可用的 ApplicationListener。推斷並設置 main 方法的定義類。

2)SpringApplication 實例初始化完成並且完成設置後,就開始執行 run 方法的邏輯了,方法執行伊始,首先遍歷執行所有通過 SpringFactoriesLoader 可以查找到並加載的 SpringApplicationRunListener,調用它們的 started() 方法,告訴這些 SpringApplicationRunListener,“嘿,SpringBoot 應用要開始執行咯!”。

3)創建並配置當前 SpringBoot 應用將要使用的 Environment(包括配置要使用的 PropertySource 以及 Profile)。

4)遍歷調用所有 SpringApplicationRunListener 的 environmentPrepared()的方法,告訴它們:“當前 SpringBoot 應用使用的 Environment 準備好咯!”。

5)如果 SpringApplication的showBanner 屬性被設置為 true,則打印 banner(SpringBoot 1.3.x版本,這裡應該是基於 Banner.Mode 決定 banner 的打印行為)。這一步的邏輯其實可以不關心,我認為唯一的用途就是“好玩”(Just For Fun)。

6)根據用戶是否明確設置了applicationContextClass 類型以及初始化階段的推斷結果,決定該為當前 SpringBoot 應用創建什麼類型的 ApplicationContext 並創建完成,然後根據條件決定是否添加 ShutdownHook,決定是否使用自定義的 BeanNameGenerator,決定是否使用自定義的 ResourceLoader,當然,最重要的,將之前準備好的 Environment 設置給創建好的 ApplicationContext 使用。

7)ApplicationContext 創建好之後,SpringApplication 會再次藉助 Spring-FactoriesLoader,查找並加載 classpath 中所有可用的 ApplicationContext-Initializer,然後遍歷調用這些 ApplicationContextInitializer 的 initialize(applicationContext)方法來對已經創建好的 ApplicationContext 進行進一步的處理。

8)遍歷調用所有 SpringApplicationRunListener 的 contextPrepared()方法,通知它們:“SpringBoot 應用使用的 ApplicationContext 準備好啦!”

9)最核心的一步,將之前通過 @EnableAutoConfiguration 獲取的所有配置以及其他形式的 IoC 容器配置加載到已經準備完畢的 ApplicationContext。

10)遍歷調用所有 SpringApplicationRunListener 的 contextLoaded() 方法,告知所有 SpringApplicationRunListener,ApplicationContext "裝填完畢"!

11)調用 ApplicationContext 的 refresh() 方法,完成 IoC 容器可用的最後一道工序。

12)查找當前 ApplicationContext 中是否註冊有 CommandLineRunner,如果有,則遍歷執行它們。13)正常情況下,遍歷執行 SpringApplicationRunListener 的 finished() 方法,告知它們:“搞定!”。(如果整個過程出現異常,則依然調用所有 SpringApplicationRunListener 的 finished() 方法,只不過這種情況下會將異常信息一併傳入處理)。

至此,一個完整的 SpringBoot 應用啟動完畢!整個過程看起來冗長無比,但其實很多都是一些事件通知的擴展點,如果我們將這些邏輯暫時忽略,那麼,其實整個 SpringBoot 應用啟動的邏輯就可以壓縮到極其精簡的幾步,如圖 1 所示。

SpringApplication.run執行流程詳解

​圖 1 SpringBoot應用啟動步驟簡要示意圖

前後對比我們就可以發現,其實 SpringApplication 提供的這些各類擴展點近乎“喧賓奪主”,佔據了一個 Spring 應用啟動邏輯的大部分“江山”,除了初始化並準備好 ApplicationContext,剩下的大部分工作都是通過這些擴展點完成的,所以,我們接下來對各類擴展點進行逐一剖析。

SpringApplicationRunListener

SpringApplicationRunListener 是一個只有 SpringBoot 應用的 main 方法執行過程中接收不同執行時點事件通知的監聽者:

<code>public interface SpringApplicationRunListener {
void started();
void environmentPrepared(ConfigurableEnvironment environment);
void contextPrepared(ConfigurableApplicationContext context);
void contextLoaded(ConfigurableApplicationContext context);
void finished(ConfigurableApplicationContext context, Throwable exception);
}/<code>

對於我們來說,基本沒什麼常見的場景需要自己實現一個 Spring-ApplicationRunListener,即使 SpringBoot 默認也只是實現了一個 org.spring-framework.boot.context.event.EventPublishingRunListener,用於在 SpringBoot 啟動的不同時點發布不同的應用事件類型(ApplicationEvent),如果有哪些 ApplicationListener 對這些應用事件感興趣,則可以接收並處理。假設我們真的有場景需要自定義一個 SpringApplicationRunListener 實現,那麼有一點需要注意,即任何一個 SpringApplicationRunListener 實現類的構造方法(Constructor)需要有兩個構造參數,一個構造參數的類型就是我們的 org.springframework.boot.SpringApplication,另外一個就是 args 參數列表的 String[]:

<code>public class DemoSpringApplicationRunListener implements SpringApplicationRunListener {
@Override
public void started() {
// do whatever you want to do
}
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
// do whatever you want to do
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
// do whatever you want to do
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
// do whatever you want to do
}
@Override
public void finished(ConfigurableApplicationContext context, Throwable exception) {
// do whatever you want to do
}
}/<code>

之後,我們可以通過 SpringFactoriesLoader 立下的規矩,在當前 SpringBoot 應用的 classpath 下的 META-INF/spring.factories 文件中進行類似如下的配置:

<code>org.springframework.boot.SpringApplicationRunListener=\\com.keevol.springboot.demo.DemoSpringApplicationRunListener/<code>

然後 SpringApplication 就會在運行的時候調用它啦!

ApplicationListener

ApplicationListener 其實是老面孔,屬於 Spring 框架對 Java 中實現的監聽者模式的一種框架實現,這裡唯一值得著重強調的是,對於初次接觸 SpringBoot,但對 Spring 框架本身又沒有過多接觸的開發者來說,可能會將這個名字與 SpringApplicationRunListener 混淆。關於 ApplicationListener 我們就不做過多介紹了,如果感興趣,請參考 Spring 框架相關的資料和書籍。如果我們要為 SpringBoot 應用添加自定義的 ApplicationListener,有兩種方式:

  • 通過 SpringApplication.addListeners(..)或者 SpringApplication.setListeners(..)方法添加一個或者多個自定義的 ApplicationListener。
  • 藉助 SpringFactoriesLoader 機制,在 META-INF/spring.factories 文件中添加配置(以下代碼是為 SpringBoot 默認註冊的 ApplicationListener 配置)。
<code>org.springframework.context.ApplicationListener=
\\org.springframework.boot.builder.ParentContextCloserApplicationListener,
\\org.springframework.boot.cloudfoundry.VcapApplicationListener,
\\org.springframework.boot.context.FileEncodingApplicationListener,
\\org.springframework.boot.context.config.AnsiOutputApplicationListener,
\\org.springframework.boot.context.config.ConfigFileApplicationListener,
\\org.springframework.boot.context.config.DelegatingApplicationListener,
\\org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicat-ionListener,
\\org.springframework.boot.logging.ClasspathLoggingApplicationListener,
\\org.springframework.boot.logging.LoggingApplicationListener/<code>

關於 ApplicationListener,我們就說這些。

ApplicationContextInitializer

ApplicationContextInitializer 也是 Spring 框架原有的概念,這個類的主要目的就是在 ConfigurableApplicationContext 類型(或者子類型)的 ApplicationContext 做 refresh 之前,允許我們對 ConfigurableApplicationContext 的實例做進一步的設置或者處理。實現一個 ApplicationContextInitializer 很簡單,因為它只有一個方法需要實現:

<code>public class DemoApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// do whatever you want with applicationContext,
// e.g.
applicationContext.registerShutdownHook();
}
}/<code>

不過,一般情況下我們基本不會需要自定義一個 ApplicationContext-Initializer,即使 SpringBoot 框架默認也只是註冊了三個實現:

<code>org.springframework.context.ApplicationContextInitializer=
\\org.springframework.boot.context.ConfigurationWarningsApplication-ContextInitializer,
\\org.springframework.boot.context.ContextIdApplicationContextInitia-lizer,
\\org.springframework.boot.context.config.DelegatingApplicationContex-tInitializer/<code>

如果我們真的需要自定義一個 ApplicationContextInitializer,那麼只要像上面這樣,通過 SpringFactoriesLoader 機制進行配置,或者通過 SpringApplication.addInitializers(..)設置即可。

CommandLineRunner

CommandLineRunner 是很好的擴展接口,不是 Spring 框架原有的“寶貝”,它屬於 SpringBoot 應用特定的回調擴展接口。源碼如下所示:

<code>public interface CommandLineRunner {
void run(String... args) throws Exception;
}/<code>

CommandLineRunner 需要大家關注的其實就兩點:1)所有 CommandLineRunner 的執行時點在 SpringBoot 應用的 Application-Context 完全初始化開始工作之後(可以認為是 main 方法執行完成之前最後一步)。2)只要存在於當前 SpringBoot 應用的 ApplicationContext 中的任何 Command-LineRunner,都會被加載執行(不管你是手動註冊這個 CommandLineRunner 到 IoC 容器,還是自動掃描進去的)。與其他幾個擴展點接口類型相似,建議 CommandLineRunner 的實現類使用 @org.springframework.core.annotation.Order 進行標註或者實現 org.springframework.core.Ordered 接口,便於對它們的執行順序進行調整,這其實十分重要,我們不希望順序不當的 CommandLineRunner 實現類阻塞了後面其他 CommandLineRunner 的執行。


分享到:


相關文章: