不懂SpringApplication生命週期事件?那就等於不會Spring Boot嘛

YourBatman發表於2020-07-06

學習方法之少廢話:吹牛、裝逼、叫大哥。
作者:A哥(YourBatman)
公眾號:BAT的烏托邦(ID:BAT-utopia)
文末是否有彩蛋:有

前言

各位小夥伴大家好,我是A哥。本文屬總結性文章,對總覽Spring Boot生命週期很是重要,建議點在看、轉發“造福”更多小夥伴。

我最近不是在寫Spring Cloud深度剖析的相關專欄麼,最近有收到小夥伴發過來一些問題,通過這段時間收集到的反饋,總結了一下有一個問題非常集中:那便是對Spring Boot應用SpringApplication的生命週期、事件的理解。有句話我不是經常掛嘴邊說的麼,你對Spring Framework有多瞭解決定了你對Spring Boot有多瞭解,你對Spring Boot的瞭解深度又會制約你去了解Spring Cloud,一環扣一環。因此此問題反饋比較集中是在清理之中的~

為何在Spring Boot中生命週期事件機制如此重要?緣由很簡單:Spring Cloud父容器是由該生命週期事件機制來驅動的,而它僅僅是一個典型代表。Spring Cloud構建在Spring Boot之上,它在此基礎上構建並新增了一些“Cloud”功能。應用程式事件ApplicationEvent以及監聽ApplicationListener是Spring Framework提供的擴充套件點,Spring Boot對此擴充套件點利用得非常充分和深入,並且還衍生出了非常多“子”事件型別,甚至自成體系。從ApplicationEvent衍生出來的子事件型別非常多,例如JobExecutionEvent、RSocketServerInitializedEvent、AuditApplicationEvent...

本文並不會對每個子事件分別介紹(也並無必要),而是集中火力主攻Spring Boot最為重要的一套事件機制:SpringApplication生命週期的事件體系
學習去...


正文

本文將以SpringApplication的啟動流程/生命週期各時期發出的Event事件為主線,結合每個生命週期內完成的大事記介紹,真正實現一文讓你總覽Spring Boot的全貌,這對你深入理解Spring Boot,以及整合進Spring Cloud都將非常重要

為表誠意,本文一開始便把SpringApplication生命週期事件流程圖附上,然後再精細化講解各個事件的詳情。

話外音:趕時間的小夥伴可以拿圖走人?,但不建議白嫖喲


生命週期事件流程圖


版本說明:

由於不同版本、類路徑下存在不同包時結果會存在差異,不指明版本的文章都是不夠負責任的。因此對導包/版本情況作出如下說明:

  • Spring Boot:2.2.2.RELEASE。有且僅匯入spring-boot-starter-webspring-boot-starter-actuator
  • Spring Cloud:Hoxton.SR1。有且僅匯入spring-cloud-context(注意:並非spring-cloud-starter,並不含有spring-cloud-commons哦)

總的來說:本例導包是非常非常“乾淨”的,這樣在流程上才更有說服力嘛~


SpringApplicationEvent

它是和SpringApplication生命週期有關的所有事件的父類,@since 1.0.0。

public abstract class SpringApplicationEvent extends ApplicationEvent {
	private final String[] args;
	
	public SpringApplicationEvent(SpringApplication application, String[] args) {
		super(application);
		this.args = args;
	}
	public SpringApplication getSpringApplication() {
		return (SpringApplication) getSource();
	}
	public final String[] getArgs() {
		return this.args;
	}
}

它是抽象類,擴充套件自Spring Framwork的ApplicationEvent,確保了事件和應用實體SpringApplication產生關聯(當然還有String[] args)。它有如下實現子類(7個):


每個事件都代表著SpringApplication不同生命週期所處的位置,下面分別進行講解。




ApplicationStartingEvent:開始啟動中

@since 1.5.0,並非1.0.0就有的哦。不過現在幾乎沒有人用1.5以下的版本了,所以可當它是標準事件。


完成的大事記

  • SpringApplication例項已例項化:new SpringApplication(primarySources)
    • 它在例項化階段完成了如下幾件“大”事:

      • 推斷出應用型別webApplicationType、main方法所在類
      • 給欄位initializers賦值:拿到SPI方式配置的ApplicationContextInitializer上下文初始化器
      • 給欄位listeners賦值:拿到SPI方式配置的ApplicationListener應用監聽器
    • 注意:在此階段(早期階段)不要過多地使用它的內部狀態,因為它可能在生命週期的後期被修改(話外音:使用時需謹慎)

  • 此時,SpringApplicationRunListener已例項化:它通過SPI方式指定org.springframework.boot.SpringApplicationRunListener=org.springframework.boot.context.event.EventPublishingRunListener
    • 若你有自己的執行時應用監聽器,使用相同方式配置上即可,均會生效
  • 由於EventPublishingRunListener已經例項化了,因此在後續的事件傳送中,均能夠觸發對應的監聽器的執行
  • 傳送ApplicationStartingEvent事件,觸發對應的監聽器的執行

監聽此事件的監聽器們

預設情況下,有4個監聽器監聽ApplicationStartingEvent事件:

  1. LoggingApplicationListener:@since 2.0.0。對日誌系統抽象LoggingSystem執行例項化以及初始化之前的操作,預設使用的是基於Logback的LogbackLoggingSystem
  2. BackgroundPreinitializer:啟動一個後臺進行對一些類進行預熱。如ValidationInitializer、JacksonInitializer...,因為這些元件有第一次懲罰的特點(並且首次初始化均還比較耗時),所以使用後臺執行緒先預熱效果更佳
  3. DelegatingApplicationListener:它監聽的是ApplicationEvent,而實際上只會ApplicationEnvironmentPreparedEvent到達時生效,所以此處忽略
  4. LiquibaseServiceLocatorApplicationListener:略

總結:此事件節點結束時,SpringApplication完成了一些例項化相關的動作:本例項例項化、本例項屬性賦值、日誌系統例項化等。


ApplicationEnvironmentPreparedEvent:環境已準備好

@since 1.0.0。該事件節點是最為重要的一個節點之一,因為對於Spring應用來說,環境抽象Enviroment簡直太重要了,它是最為基礎的後設資料,決定著程式的構建和走向,所以構建的時機是比較早的。


完成的大事記

  • 封裝命令列引數(main方法的args)到ApplicationArguments裡面
  • 建立出一個環境抽象例項ConfigurableEnvironment的實現類,並且填入值:Profiles配置和Properties屬性,預設內容如下(注意,這只是初始狀態,後面還會改變、新增屬性源,實際見最後的截圖):
  • 傳送ApplicationEnvironmentPreparedEvent事件,觸發對應的監聽器的執行
    • 對環境抽象Enviroment的填值,均是由監聽此事件的監聽器去完成,見下面的監聽器詳解
  • bindToSpringApplication(environment):把環境屬性中spring.main.xxx = xxx繫結到當前的SpringApplication例項屬性上,如常用的spring.main.allow-bean-definition-overriding=true會被繫結到當前SpringApplication例項的對應屬性上

監聽此事件的監聽器們

預設情況下,有9個監聽器監聽ApplicationEnvironmentPreparedEvent事件:

  1. BootstrapApplicationListener:來自SC。優先順序最高,用於啟動/建立Spring Cloud的應用上下文。需要注意的是:到此時SB的上下文ApplicationContext還並沒有建立哦。這個流程“巢狀”特別像Bean初始化流程:初始化Bean A時,遇到了Bean B,就需要先去完成Bean B的初始化,再回頭來繼續完成Bean A的步驟。
    • 說明:在建立SC的應用的時候,使用的也是SpringApplication#run()完成的(非web),因此也會走下一整套SpringApplication的生命週期邏輯,所以請你務必區分。
      • 特別是這種case會讓“絕大多數”初始化器、監聽器等執行多次,若你有那種只需要執行一次的需求(比如只想讓SB容器生命週期內執行,SC生命週期不執行),請務必自行處理,否則會被執行多次而帶來不可預知的結果
    • SC應用上下文讀取的外部化配置檔名預設是:bootstrap,使用的也是ConfigFileApplicationListener完成的載入/解析
  2. LoggingSystemShutdownListener:來自SC。對LogbackLoggingSystem先清理,再重新初始化一次,效果同上個事件,相當於重新來了一次,畢竟現在有Enviroment環境裡嘛
  3. ConfigFileApplicationListener:@since 1.0.0。它也許是最重要的一個監聽器。做了如下事情:
    • 載入SPI配置的所有的EnvironmentPostProcessor例項,並且排好序。需要注意的是:ConfigFileApplicationListener也是個EnvironmentPostProcessor,會參與排序哦
    • 排好序後,分別一個個的執行EnvironmentPostProcessor(@since 1.3.0,並非一開始就有),介紹如下:
      • SystemEnvironmentPropertySourceEnvironmentPostProcessor:@since 2.0.0。把SystemEnvironmentPropertySource替換為其子類OriginAwareSystemEnvironmentPropertySource(屬性值帶有Origin來源),僅此而已
      • SpringApplicationJsonEnvironmentPostProcessor:@since 1.3.0。把環境中spring.application.json=xxx值解析成為一個MapPropertySource屬性源,然後放進環境裡面去(屬性源的位置是做了處理的,一般不用太關心)
        • 可以看到,SB是直接支援JSON串配置的哦。Json解析參見:JsonParser
      • CloudFoundryVcapEnvironmentPostProcessor:@since 1.3.0。略
      • ConfigFileApplicationListener:@since 1.0.0(它比EnvironmentPostProcessor先出現的哦)。載入application.properties/yaml等外部化配置,解析好後放進環境裡(這應該是最為重要的)。
        • 外部化配置預設的優先順序為:"classpath:/,classpath:/config/,file:./,file:./config/"。當前工程下的config目錄裡的application.properties優先順序最高,當前工程類路徑下的application.properties優先順序最低
        • 值得強調的是:bootstrap.xxx也是由它負責載入的,處理規則一樣
      • DebugAgentEnvironmentPostProcessor:@since 2.2.0。處理和reactor測試相關,略
  4. AnsiOutputApplicationListener:@since 1.2.0。讓你的終端(可以是控制檯、可以是日誌檔案)支援Ansi彩色輸出,使其更具可讀性。當然前提是你的終端支援ANSI顯示。參考類:AnsiOutput。你可通過spring.output.ansi.enabled = xxx配置,可選值是:DETECT/ALWAYS/NEVER,一般不動即可。另外,針對控制檯可以單獨配置:spring.output.ansi.console-available = true/false
  5. LoggingApplicationListener:@since 2.0.0。根據Enviroment環境完成initialize()初始化動作:日誌等級、日誌格式模版等
    • 值得注意的是:它這步相當於在ApplicationStartingEvent事件基礎上進一步完成了初始化(上一步只是例項化)
  6. ClasspathLoggingApplicationListener:@since 2.0.0。用於把classpath路徑以log.debug()輸出,略
    1. 值得注意的是:classpath類路徑是有N多個的Arrays.toString(((URLClassLoader) classLoader).getURLs()),也就是說每個.jar裡都屬於classpath的範疇
    2. 當然嘍,你需要注意Spring在處理類路徑時:classpath和classpath*的區別~,這屬於基礎知識
  7. BackgroundPreinitializer:本事件達到時無動作
  8. DelegatingApplicationListener:執行通過外部化配置context.listener.classes = xxx,xxx的監聽器們,然後把該事件廣播給他們,關心此事件的監聽器執行
    • 這麼做的好處:可以通過屬性檔案外部化配置監聽器,而不一定必須寫在spring.factories裡,更具彈性
    • 外部化配置的執行優先順序,還是相對較低的,到這裡才給與執行嘛
  9. FileEncodingApplicationListener:檢測當前系統環境的file.encoding和spring.mandatory-file-encoding設定的值是否一樣,如果不一樣則丟擲異常如果不配置spring.mandatory-file-encoding則不檢查

總結:此事件節點結束時,Spring Boot的環境抽象Enviroment已經準備完畢,但此時其上下文ApplicationContext沒有建立,但是Spring Cloud的應用上下文(引導上下文)已經全部初始化完畢哦,所以SC管理的外部化配置也應該都進入到了SB裡面。如下圖所示(這是基本上算是Enviroment的最終態了):


小提示:SC配置的優先順序是高於SB管理的外部化配置的。例如針對spring.application.name這個屬性,若bootstrap裡已配置了值,再在application.yaml裡配置其實就無效了,因此生產上建議不要寫兩處。


ApplicationContextInitializedEvent:上下文已例項化

@since 2.1.0,非常新的一個事件。當SpringApplication的上下文ApplicationContext準備好,對單例Bean們例項化之,傳送此事件。所以此事件又可稱為:contextPrepared事件。


完成的大事記

  • printBanner(environment):列印Banner圖,預設列印的是Spring Boot字樣

    • spring.main.banner-mode = xxx來控制Banner的輸出,可選值為CONSOLE/LOG/OFF,一般預設就好
    • 預設在類路徑下放置一個banner.txt檔案,可實現自定義Banner。關於更多自定義方式,如使用圖片、gif等,本處不做過多介紹
      • 小建議:別花裡胡哨搞個佛祖在那。讓它能自動列印輸出當前應用名,這樣才是最為實用,最高階的(但需要你定製化開發,並且支援可配置,最好對使用者無感,屬於一個common元件)
  • 根據是否是web環境、是否是REACTIVE等,用空構造器建立出一個ConfigurableApplicationContext上下文例項(因為使用的是空構造器,所以不會立馬“啟動”上下文)

    • SERVLET -> AnnotationConfigServletWebServerApplicationContext
    • REACTIVE -> AnnotationConfigReactiveWebServerApplicationContext
    • 非web環境 -> AnnotationConfigApplicationContext(SC應用的容器就是使用的它)
  • 既然上下文例項已經有了,那麼就開始對它進行一些引數的設定:

    • 首先最重要的便是把已經準備好的環境Enviroment環境設定給它
    • 設定些beanNameGenerator、resourceLoader、ConversionService等元件
    • 例項化所有的ApplicationContextInitializer上下文初始化器,並且排序好後挨個執行它(這個很重要),預設有如下截圖這些初始化器此時要執行:

      下面對這些初始化器分別做出簡單介紹:
      1. BootstrapApplicationListener.AncestorInitializer:來自SC。用於把SC容器設定為SB容器的父容器。當然實際操作委託給了此方法:new ParentContextApplicationContextInitializer(this.parent).initialize(context)去完成
      2. BootstrapApplicationListener.DelegatingEnvironmentDecryptApplicationInitializer:來自SC。代理了下面會提到的EnvironmentDecryptApplicationInitializer,也就是說在此處就會先執行,用於提前解密Enviroment環境裡面的屬性,如相關URL等
      3. PropertySourceBootstrapConfiguration:來自SC。重要,和配置中心相關,若想自定義配置中心必須瞭解它。主要作用是PropertySourceLocator屬性源定位器,我會把它放在配置中心章節詳解
      4. EnvironmentDecryptApplicationInitializer:來自SC。屬性源頭通過上面載入回來了,通過它來實現解密
        • 值得注意的是:它被執行了兩次哦~
      5. DelegatingApplicationContextInitializer:和上面的DelegatingApplicationListener功能類似,支援外部化配置context.initializer.classes = xxx,xxx
      6. SharedMetadataReaderFactoryContextInitializer:略
      7. ContextIdApplicationContextInitializer:@since 1.0.0。設定應用ID -> applicationContext.setId()。預設取值為spring.application.name,再為application,再為自動生成
      8. ConfigurationWarningsApplicationContextInitializer:@since 1.2.0。對錯誤的配置進行警告(不會終止程式),以warn()日誌輸出在控制檯。預設內建的只有對包名的檢查:若你掃包含有"org.springframework"/"org"這種包名就警告
        • 若你想自定義檢查規則,請實現Check介面,然後...
      9. RSocketPortInfoApplicationContextInitializer:@since 2.2.0。暫略
      10. ServerPortInfoApplicationContextInitializer:@since 2.0.0。將自己作為一個監聽器註冊到上下文ConfigurableApplicationContext裡,專門用於監聽WebServerInitializedEvent事件(非SpringApplication的生命週期事件)
        • 該事件有兩個實現類:ServletWebServerInitializedEventReactiveWebServerInitializedEvent。傳送此事件的時機是WebServer已啟動完成,所以已經有了監聽的埠號
        • 該監聽器做的事有兩個:
          • "local." + getName(context.getServerNamespace()) + ".port"作為key(預設值是local.server.port),value是埠值。這樣可以通過@Value來獲取到本機埠了(但貌似埠寫0的時候,SB在顯示上有個小bug)
          • 作為一個屬性源MapPropertySource放進環境裡,屬性源名稱為:server.ports(因為一個server是可以監聽多個埠的,所以這裡用複數)
      • ConditionEvaluationReportLoggingListener:將ConditionEvaluationReport報告(自動配置中哪些匹配了,哪些沒匹配上)寫入日誌,當然只有LogLevel#DEBUG時才會輸出(注意:這不是日誌級別哦,應該叫報告級別)。如你配置debug=true就開啟了此自動配置類報告
        • 槽點:它明明是個初始化器,為毛命名為Listener?
  • 傳送ApplicationContextInitializedEvent事件,觸發對應的監聽器的執行


監聽此事件的監聽器們

預設情況下,有2個監聽器監聽ApplicationContextInitializedEvent事件:

  1. BackgroundPreinitializer:本事件達到時無動作
  2. DelegatingApplicationListener:本事件達到時無動作

總結:此事件節點結束時,完成了應用上下文ApplicationContext的準備工作,並且執行所有註冊的上下文初始化器ApplicationContextInitializer。但是此時,單例Bean是仍舊還沒有初始化,並且WebServer也還沒有啟動


ApplicationPreparedEvent:上下文已準備好

@since 1.0.0。截止到上個事件ApplicationContextInitializedEvent,應用上下文ApplicationContext充其量叫例項化好了,但是還剩下很重要的事沒做,這便是本週期的內容。


完成的大事記

  • 把applicationArguments、printedBanner等都作為一個Bean放進Bean工廠裡(因此你就可以@Autowired注入的哦)
    • 比如:有了Banner這個Bean,你可以在你任何想要輸出的地方輸出一個Banner,而不僅僅是啟動時只會輸出一次了
  • lazyInitialization = true延遲初始化,那就向Bean工廠放一個:new LazyInitializationBeanFactoryPostProcessor()
    • 該處理器@since 2.2.0。該處理器的作用是:對所有的Bean(通過LazyInitializationExcludeFilter介面指定的排除在外)全部.setLazyInit(true);延遲初始化
  • 根據primarySources和allSources,交給BeanDefinitionLoader(SB提供的實現)實現載入Bean的定義資訊,它支援4種載入方式(4種源):
    • AnnotatedBeanDefinitionReader -> 基於註解
    • XmlBeanDefinitionReader -> 基於xml配置
    • GroovyBeanDefinitionReader -> Groovy檔案
    • ClassPathBeanDefinitionScanner -> classpath中載入
    • (不同的源使用了不同的load載入方式)
  • 傳送ApplicationPreparedEvent事件,觸發對應的監聽器的執行

監聽此事件的監聽器們

預設情況下,有6個監聽器監聽ApplicationContextInitializedEvent事件:

  1. CloudFoundryVcapEnvironmentPostProcessor:略
  2. ConfigFileApplicationListener:向上下文註冊一個new PropertySourceOrderingPostProcessor(context)。它的作用是:Bean工廠結束後對環境裡的屬性源進行重排序 -> 把名字叫defaultProperties的屬性源放在最末位
    • 該屬性源是通過SpringApplication#setDefaultProperties API方式放進來的,一般不會使用到,留個印象即可
  3. LoggingApplicationListener:因為這時已經有Bean工廠了嘛,所以它做的事是:向工廠內放入Bean
    • "springBootLoggingSystem" -> loggingSystem
    • "springBootLogFile" -> logFile
    • "springBootLoggerGroups" -> loggerGroups
  4. BackgroundPreinitializer:本事件達到時無動作
  5. RestartListener:SC提供。把當前最新的上下文快取起來而已,目前並未發現有實質性作用,可忽略
  6. DelegatingApplicationListener:本事件達到時無動作

總結:此事件節點結束時,應用上下文ApplicationContext初始化完成,該賦值的賦值了,Bean定義資訊也已全部載入完成。但是,單例Bean還沒有被例項化,web容器依舊還沒啟動。


ApplicationStartedEvent:應用成功啟動

@since 2.0.0。截止到此,應用已經準備就緒,並且通過監聽器、初始化器等完成了非常多的工作了,但仍舊剩下被認為最為重要的初始化單例Bean動作還沒做、web容器(如Tomcat)還沒啟動,這便是這個週期所要做的事。


完成的大事記

  • 啟動Spring容器:AbstractApplicationContext#refresh(),這個步驟會做很多事,比如會例項化單例Bean
    • 該步驟屬於Spring Framework的核心內容範疇,做了很多事,請參考Spring核心技術內容章節
    • 在Spring容器refresh()啟動完成後,WebServer也隨之完成啟動,成功監聽到對應埠(們)
  • 輸出啟動成功的日誌:Started Application in xxx seconds (JVM running for xxx)
  • 傳送ApplicationStartedEvent事件,觸發對應的監聽器的執行
  • callRunners():依次執行容器內配置的ApplicationRunner/CommandLineRunner的Bean實現類,支援sort排序
    • ApplicationRunner:@since 1.3.0,入參是ApplicationArguments,先執行(推薦使用)
    • CommandLineRunner:@since 1.0.0,入參是String... args,後執行(不推薦使用)

監聽此事件的監聽器們

預設情況下,有3個監聽器監聽ApplicationStartedEvent事件:

  1. 前兩個不用再解釋了吧:本事件達到時無動作
  2. TomcatMetricsBinder:@since 2.1.0。和監控相關:將你的tomcat指標資訊TomcatMetrics繫結到MeterRegistry,從而就能收集到相關指標了

總結:此事件節點結束時,SpringApplication的生命週期到這一步,正常的啟動流程就全部完成了。也就說Spring Boot應用可以正常對對外提供服務了。


ApplicationReadyEvent:應用已準備好

@since 1.3.0。該事件所處的生命週期可認為基本同ApplicationStartedEvent,僅是在其後執行而已,兩者中間並無其它特別的動作,但是監聽此事件的監聽器們還是蠻重要的


完成的大事記

同上。


監聽此事件的監聽器們

預設情況下,有4個監聽器監聽ApplicationStartedEvent事件:

  1. SpringApplicationAdminMXBeanRegistrar:當此事件到達時,告訴Admin Spring應用已經ready,可以使用啦。
  2. 中間這兩個不用再解釋了吧:本事件達到時無動作
  3. RefreshEventListener:當此事件到達時,告訴Spring應用已經ready了,接下來便可以執行ContextRefresher.refresh()

總結:此事件節點結束時,應用已經完完全全的準備好了,並且也已經完成了相關元件的周知工作。



異常情況

SpringApplication是有可能在啟動的時候失敗(如埠號已被佔用),當然任何一步驟遇到異常時交給SpringApplication#handleRunFailure()方法來處理,這時候也會有對應的事件發出。


ApplicationFailedEvent:應用啟動失敗

SpringApplication在啟動時丟擲異常:可能是埠繫結、也可能是你自定義的監聽器你寫了個bug等,就會“可能”傳送此事件。


完成的大事記
  • 得到異常的退出碼ExitCode,然後傳送ExitCodeEvent事件(非生命週期事件)
  • 傳送ApplicationFailedEvent事件,觸發對應的監聽器的執行

監聽此事件的監聽器們

預設情況下,有6個監聽器監聽ApplicationStartedEvent事件:

  1. LoggingApplicationListener:執行loggingSystem.cleanUp()清理資源
  2. ClasspathLoggingApplicationListener:輸出一句debug日誌:Application failed to start with classpath: ...
  3. 中間這兩個不用再解釋了吧:本事件達到時無動作
  4. ConditionEvaluationReportLoggingListener:自動配置輸出報告,輸出錯誤日誌唄:特別方便你檢視和錯誤定位
    • 不得不誇:SB對錯誤定位這塊才真叫智慧,比Spring Framework好用太多了
  5. BootstrapApplicationListener.CloseContextOnFailureApplicationListener:執行context.close()

總結:此事件節點結束時,會做一些釋放資源的操作。一般情況下:我們並不需要監聽到此事件


總結

關於SpringApplication的生命週期體系的介紹就到這了,相信通過此“萬字長文”你能體會到A哥的用心。翻了翻市面上的相關文章,本文Almost可以保證是總結得最到位的,讓你通過一文便可從大的方面基本掌握Spring Boot,這不管是你使用SB,還是後續自行擴充套件、精雕細琢SB,以及去深入瞭解Spring Cloud均由非常重要的意義,希望對你有幫助,謝謝你的三連。

相關文章