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

學習方法之少廢話:吹牛、裝逼、叫大哥。
作者: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均由非常重要的意義,希望對你有幫助,謝謝你的三連。

posted @ 2020-07-06 22:24  YourBatman  閱讀(...)  評論(...編輯  收藏
TOP
色网站直播