目錄
一、啟動入口
二、SpringApplication的構造過程
2.1 設置應用類型
2.2 設置初始化器(Initializer)
2.2.1 獲取BootstrapRegistryInitializer對象
2.2.2 獲取ApplicationContextInitializer對象
2.3 設置監聽器(Listener)
2.4 總結
三、SpringApplication的執行:執行run(String... args)方法
3.1 run方法第一步:獲取并啟動監聽器
3.1.1 如何獲取運行監聽器?
3.1.2 如何啟動運行監聽器?
3.1.3 總結
3.2 run方法第二步:環境搭建,解析配置文件
3.2.1 配置文件解析流程圖
3.2.2 Spring Boot的配置優先級
3.4 run方法第四步:創建Spring IoC容器
3.5 run方法第五步:Spring IoC容器前置處理
3.5.1 調用初始化器
3.5.1.1 利用ApplicationContextInitializer初始化Spring容器對象
3.5.2 觸發SpringApplicationRunListener的contextPrepared()
3.5.3 調用DefaultBootstrapContext對象的close()
3.5.4 判斷是否開啟懶加載
3.5.5 加載啟動類,注入容器
3.5.6 觸發SpringApplicationRunListener的contextLoaded()
3.6 run方法第六步:刷新容器【關鍵】
3.7 run方法第七步:spring容器后置處理 afterRefresh()
3.8 run方法第八步:發出結束執行的事件
3.9 run方法第九步:執行Runners
3.10 總結
四、Spring Boot啟動過程流程圖
我們在使用Spring Boot啟動項目的時候,可能只需加一個注解,然后啟動main,整個項目就運行了起來,但事實真的是所見即所得嗎,還是Spring Boot在背后默默做了很多?本文會通過源碼解析的方式深入理解Spring Boot啟動全過程。
首先要明確一個事情,不要妄想把Spring Boot啟動過程步驟全記下來,那是不可能的,神仙也記不住,我們重點是要理解啟動的步驟,可以做到看到源碼就知道這是怎么實現的,遇到某些問題知道能夠在哪個步驟定位和解決就可以了。
一、啟動入口
大家不要抗拒源碼解析,這個非常優秀的代碼,我們如果能夠學會對自己代碼編寫水平大有裨益。
相信很多人嘗試讀過Spring Boot?的源碼,但是始終沒有找到合適的方法。那是因為你對Spring Boot的各個組件、機制不是很了解,研究起來就像大海撈針。
至于從哪入手不是很簡單的問題嗎,當然主啟動類了,即是標注著@SpringBootApplication?注解并且有著main()方法的類,如下一段代碼:
@SpringBootApplication
public class SpringDemoApplication {// 啟動main方法public static void main(String[] args) {// 這里傳入的是配置類,我們習慣的方法就是把啟動類和配置類寫成一個,但是其實這里也可以將傳入的配置類設置為其他類,和啟動類分開,并不是強制要求要寫在一起的SpringApplication.run(SpringDemoApplication.class, args);}
}
一個是@SpringBootApplication,這個注解和自動配置有關,參考另一篇文章Spring Boot 自動配置原理分析。
另一個關鍵點是SpringApplication.run()方法,這是一個靜態方法,我們詳細看下代碼:
/*** 靜態方法*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[]{primarySource}, args);
}
- primarySource - 要載入的主要源,即指定源,這里為傳入的Application.class Class<?> :泛型決定了任何類都可以傳入
- args - 應用程序參數(通常從main方法傳遞)
- 返回:正在運行的ApplicationContext
SpringApplication?中的靜態run()方法并不是一步完成的,最終執行的源碼如下:
/*** 調用此方法啟動會使用默認設置和用戶提供的參數args*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {// 實例化SpringApplication,然后調用runreturn new SpringApplication(primarySources).run(args);
}
很顯然分為兩個步驟,分別是創建SpringApplication?和執行run()方法,下面將分為這兩個部分介紹。
二、SpringApplication的構造過程
可以看到代碼new SpringApplication(),new了一個這個對象,然后調用run,我們先看看SpringApplication構造函數:
public SpringApplication(Class<?>... primarySources) {this(null, primarySources);
}public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");// 把SpringDemoApplication作為primarySources屬性存儲起來this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 從classpath中推斷是否為web應用,也就是設置應用類型是Standard還是Webthis.webApplicationType = WebApplicationType.deduceFromClasspath();// 獲取啟動加載器this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));// 設置初始化器(Initializer),在啟動的過程中會調用這些功能setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 設置一系列監聽器(Listener),在啟動過程中會觸發setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 獲取main方法所在的類,沒什么具體的作用,邏輯是根據當前線程的調用棧來判斷main()方法在哪個類,哪個類就是Main類this.mainApplicationClass = deduceMainApplicationClass();
}
基本就是做如下幾件事情:
- 配置primarySources(這個就是傳入的配置類,這個配置類就是@SpringBootApplication注解修飾的類,會將一些類自動配置到Spring Boot中)
- 配置環境是否為web環境
- 創建初始化構造器setInitializers
- 創建應用監聽器
- 配置應用主方法所在類(就是main方法所在類,即啟動類),準備啟動run()方法
如下圖中標注的注釋,創建SpringApplication過程重要的其實就分為②?、③?、④這三個階段,下面將會一一介紹每個階段做了什么事。
2.1 設置應用類型
這個過程非常重要,直接決定了項目的類型,應用類型分為三種,都在WebApplicationType這個枚舉類中,如下:
- NONE?:顧名思義,什么都沒有,正常流程走,不額外的啟動web容器?,即普通的Java程序。
- SERVLET?:基于servlet?的web程序,需要啟動內嵌的servlet?web容器,比如Tomcat。
- REACTIVE?:基于reactive?的web程序,需要啟動內嵌reactiveweb容器,作者不是很了解,不便多說。
判斷的依據很簡單,就是看是否加載了對應的類,比如加載了DispatcherServlet?等則會判斷是Servlet的web程序。源碼如下:
static WebApplicationType deduceFromClasspath() {/*** 檢查是否為基于reactive?的web程序:* 這個判斷邏輯用來檢查類路徑中是否存在WebFlux的指示類(`WEBFLUX_INDICATOR_CLASS`),* 同時確保Spring MVC的指示類(`WEBMVC_INDICATOR_CLASS`)和Jersey的指示類(`JERSEY_INDICATOR_CLASS`)不存在。* 如果滿足這些條件,說明當前應用是一個反應式應用,方法返回`WebApplicationType.REACTIVE`。*/if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {return WebApplicationType.REACTIVE;}/*** 檢查是否為非Web項目:* 這部分邏輯通過遍歷一個包含Servlet指示類名稱的數組(`SERVLET_INDICATOR_CLASSES`)。* 如果類路徑中缺少任何一個指示類,那么推斷結果為非Web應用,即`WebApplicationType.NONE`。*/for (String className : SERVLET_INDICATOR_CLASSES) {if (!ClassUtils.isPresent(className, null)) {return WebApplicationType.NONE;}}/*** 檢查是否為基于servlet?的web程序:* 如果上面遍歷完成后沒有返回`WebApplicationType.NONE`,* 則說明類路徑中存在所有指示類,可以認為當前應用是基于Servlet的Web應用,* 因此方法返回`WebApplicationType.SERVLET`。*/return WebApplicationType.SERVLET;
}
這段代碼的核心作用是根據類路徑中的類來動態推斷Spring Boot應用的類型。這是通過檢查特定的類是否存在來實現的:
- 如果項目依賴中存在org.springframework.web.reactive.DispatcherHandler,并且不存在org.springframework.web.servlet.DispatcherServlet,那么應用類型為WebApplicationType.REACTIVE
- 如果項目依賴中不存在org.springframework.web.reactive.DispatcherHandler,也不存在org.springframework.web.servlet.DispatcherServlet,那么應用類型為WebApplicationType.NONE
- 否則,應用類型為WebApplicationType.SERVLET
這種機制使得Spring Boot可以靈活地適配不同類型的Web應用開發,無論是傳統的Servlet應用、Spring MVC應用,還是反應式Web應用,Spring Boot都能提供合適的配置和環境。
這里我引入了spring-boot-starter-web?,肯定是Servlet的web程序。
2.2 設置初始化器(Initializer)
這里我們將一下設置的兩個初始化器:BootstrapRegistryInitializer對象和ApplicationContextInitializer對象。
2.2.1 獲取BootstrapRegistryInitializer對象
由上面的源碼我們可以看出,是先獲取BootstrapRegistryInitializer初始化器,再獲取ApplicationContextInitializer初始化器。
執行流程如下:
- 從"META-INF/spring.factories"中讀取key為BootstrapRegistryInitializer類型的擴展點,并實例化出對應擴展點對象
- BootstrapRegistryInitializer的作用是可以初始化BootstrapRegistry
- 源碼中的DefaultBootstrapContext對象就是一個BootstrapRegistry,可以用來注冊一些對象,這些對象可以用在從Spring Boot啟動到Spring容器初始化完成的過程中
- 我的理解:沒有Spring容器之前就利用BootstrapRegistry來共享一些對象,有了Spring容器之后就利用Spring容器來共享一些對象。
2.2.2 獲取ApplicationContextInitializer對象
初始化器ApplicationContextInitializer?是個好東西,顧名思義,ApplicationContextInitializer是用來初始化Spring容器ApplicationContext對象的,用于IoC?容器刷新之前初始化一些組件,比如可以利用ApplicationContextInitializer來向Spring容器中添加ApplicationListener、初始化ServletContextApplicationContextInitializer對象。
那么如何獲取初始化器呢?跟著上圖中的代碼進入,在SpringApplication中的如下圖中的方法:
相對重要的就是第一步獲取初始化器的名稱了,這個肯定是全類名?了,詳細源碼肯定在loadFactoryNames()?方法中了,跟著源碼進入,最終調用的是#SpringFactoriesLoader.loadSpringFactories()方法。
loadSpringFactories()?方法就不再詳細解釋了,其實就是從類路徑META-INF/spring.factories?中加載ApplicationContextInitializer的值。從"META-INF/spring.factories"中讀取key為ApplicationContextInitializer類型的擴展點,并實例化出對應擴展點對象。
在spring-boot-autoconfigure?的spring.factories文件中的值如下圖:
上圖中的只是一部分初始化器,因為spring.factories文件不止一個,幾乎每一個集成到Spring Boot的依賴都會提供spring.factories來幫助完成自動配置。
下圖中是我的demo中注入的初始化器,現實項目中并不止這些:
這也告訴我們自定義一個ApplicationContextInitializer?只需要實現接口,然后在spring.factories文件中設置即可。
2.3 設置監聽器(Listener)
監聽器(ApplicationListener?)這個概念在Spring?中就已經存在,主要用于監聽特定的事件(ApplicationEvent),比如IoC容器刷新、容器關閉等。
Spring Boot?擴展了ApplicationEvent?構建了SpringApplicationEvent?這個抽象類,主要用于Spring Boot啟動過程中觸發的事件,比如程序啟動中、程序啟動完成等。如下圖:
- 啟動事件,ApplicationStartingEvent。一旦一個 SpringApplication 開始,事件就會盡早發布 - 在 or ApplicationContext 可用之前Environment,但在注冊之后ApplicationListener。事件的來源是本身SpringApplication,但要注意不要在這個早期階段過多地使用其內部狀態,因為它可能會在生命周期的后期被修改
- 失敗事件,ApplicationFailedEvent
- 準備事件,ApplicationPreparedEvent 事件發布為 當一個SpringApplication正在啟動并且ApplicationContext已完全準備好但未refresh。將加載 Bean 定義,并在此階段可以使用
- ApplicationEnvironmentPreparedEvent
- ContextClosedEvent
應用程序事件監聽器跟監聽事件是綁定的,如:
- ConfigServerBootstrapApplicationListener只跟ApplicationEnvironmentPreparedEvent事件綁定
- LiquibaseServiceLocatorApplicationListener只跟ApplicationStartedEvent事件綁定
- LoggingApplicationListener跟所有事件綁定
監聽器如何獲取?從源碼中知道其實和初始化器(ApplicationContextInitializer?)執行的是同一個方法,最終調用的都是#SpringFactoriesLoader.loadSpringFactories()方法,同樣是從META-INF/spring.factories文件中獲取監聽器類。從"META-INF/spring.factories"中讀取key為ApplicationListener類型的擴展點,并實例化出對應擴展點對象。
在spring-boot-autoconfigure?的spring.factories文件中的值如下圖:
spring.factories文件不止一個,同樣監聽器也不止以上這些,Spring Boot引入的很多依賴都會提供監聽器。
作者demo中注入的一些監聽器如下圖:
2.4 總結
SpringApplication?的構建都是為了run()方法啟動做鋪墊,構造方法中總共就有幾行代碼,最重要的部分就是設置應用類型、設置初始化器、設置監聽器。
「注意」?:初始化器和這里的監聽器都要放置在spring.factories?文件中才能在這一步驟加載,否則不會生效,因為此時IoC容器?還未創建,即使將其注入到IoC容器中也是不會生效的。所以如果想讓初始化器和監聽器在完成IoC容器創建前就起作用,必須要用spring.factories?文件才行。
作者簡單的畫了張執行流程圖,僅供參考,如下:
new SpringApplication()基本上就是做以上這些必要的屬性初始化和賦值,接下來我們看下關鍵方法run()。
三、SpringApplication的執行:執行run(String... args)方法
上面分析了SpringApplication的構建過程,一切都做好了鋪墊,現在到了啟動的過程了。
這里根據源碼將啟動過程分為了?「9步」?,下面將會一一介紹。
/*** 運行spring應用程序,創建并刷新一個新的 {@link ApplicationContext}.** @param args the application arguments (usually passed from a Java main method)* @return a running {@link ApplicationContext}*/
public ConfigurableApplicationContext run(String... args) {// 計時工具StopWatch stopWatch = new StopWatch();// 開始計時stopWatch.start();// 創建啟動上下文對象DefaultBootstrapContext bootstrapContext = createBootstrapContext();ConfigurableApplicationContext context = null;configureHeadlessProperty();// 第一步:獲取并啟動監聽器,這是從spring.factories中獲取監聽器// 獲取監聽器SpringApplicationRunListeners listeners = getRunListeners(args);// 啟動監聽器listeners.starting(bootstrapContext, this.mainApplicationClass);try {// 獲取參數對象,將啟動時傳入的arg參數封裝成ApplicationArguments對象ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 第二步:準備環境ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);configureIgnoreBeanInfo(environment);// 第三步:打印banner,就是啟動的時候在console的spring boot圖案Banner printedBanner = printBanner(environment);// 第四步:創建spring容器context = createApplicationContext();context.setApplicationStartup(this.applicationStartup);// 第五步:spring容器前置處理prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);// 第六步:刷新容器refreshContext(context);// 第七步:spring容器后置處理afterRefresh(context, applicationArguments);stopWatch.stop(); // 結束計時器并打印,這就是我們啟動后console的顯示的時間if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 第八步:發出啟動結束事件listeners.started(context);// 第九步:執行runner的run方法callRunners(context, applicationArguments);} catch (Throwable ex) {// 異常處理,如果run過程發生異常handleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {listeners.running(context);} catch (Throwable ex) {// 異常處理,如果run過程發生異常handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}// 返回最終構建的容器對象return context;
}
run()方法流程圖:
基本流程如下:
- 啟動一個計時器,啟動完成后會打印耗時
- 獲取并啟動監聽器 SpringApplicationRunListeners
- 配置環境 ConfigurableEnvironment
- Banner配置,就是控制臺的那個spirng
- 應用上下文模塊(前置處理、刷新、后置處理) ConfigurableApplicationContext
- 發出啟動結束事件并結束計時
這里的每一個方法都是做了很多事情,接下來我們一步步深入看下。
3.1 run方法第一步:獲取并啟動監聽器
SpringApplicationRunListener?這個監聽器和ApplicationListener不同,它是用來監聽應用程序啟動過程的,接口的各個方法含義如下:
public interface SpringApplicationRunListener {/*** 當調用run方法后會立即調用,可以用于非常早期的初始化*/default void starting(ConfigurableBootstrapContext bootstrapContext) {starting();}/*** 環境準備好之后調用*/default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment) {environmentPrepared(environment);}/*** 在加載資源之前,ApplicationContex準備好之后調用*/default void contextPrepared(ConfigurableApplicationContext context) {}/*** 在加載應用程序上下文ApplicationContext但在其刷新之前調用*/default void contextLoaded(ConfigurableApplicationContext context) {}/*** ApplicationContext上下文已經刷新且應用程序已啟動且所有{@link CommandLineRunner commandLineRunner}* 和{@link ApplicationRunner ApplicationRunners}未調用之前調用*/default void started(ConfigurableApplicationContext context) {}/*** 當應用程序上下文ApplicationContext被刷新并且所有{@link CommandLineRunner commandLineRunner}* 和{@link ApplicationRunner ApplicationRunners}都已被調用時,在run方法結束之前立即調用。*/default void running(ConfigurableApplicationContext context) {}/*** 在啟動過程發生失敗時調用*/default void failed(ConfigurableApplicationContext context, Throwable exception) {}
}
這里的啟動監聽就是我們需要監聽Spring Boot的啟動流程,實現SpringApplicationRunListener類即可監聽。
3.1.1 如何獲取運行監聽器?
在SpringApplication#run()方法中,源碼如下:
//從spring.factories中獲取監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);/*** 獲取運行監聽的監聽者們,在對應的階段會發送對應的事件到監聽者* @param args* @return*/
private SpringApplicationRunListeners getRunListeners(String[] args) {Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};return new SpringApplicationRunListeners(logger,getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),this.applicationStartup);
}
跟進getRunListeners()?方法,其實還是調用了loadFactoryNames()?方法從spring.factories文件中獲取值,如下:
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
最終注入的是EventPublishingRunListener這個實現類,創建實例過程肯定是通過反射了,因此我們看看它的構造方法,如下圖:
這個運行監聽器內部有一個事件廣播器(SimpleApplicationEventMulticaster?),主要用來廣播特定的事件(SpringApplicationEvent?)來觸發特定的監聽器ApplicationListener。
EventPublishingRunListener?中的每個方法用來觸發SpringApplicationEvent中的不同子類。
3.1.2 如何啟動運行監聽器?
在SpringApplication#run()方法中,源碼如下:
// 執行starting()方法
listeners.starting(bootstrapContext, this.mainApplicationClass);
執行SpringApplicationRunListeners?的starting()?方法,跟進去其實很簡單,遍歷執行上面獲取的運行監聽器,這里只有一個EventPublishingRunListener?。因此執行的是它的starting()方法,源碼如下圖:
上述源碼中邏輯很簡單,其實只是執行了multicastEvent()?方法,廣播了ApplicationStartingEvent?事件。至于multicastEvent()?內部方法感興趣的可以看看,其實就是遍歷ApplicationListener?的實現類,找到監聽ApplicationStartingEvent?這個事件的監聽器,執行onApplicationEvent()方法。
3.1.3 總結
這一步其實就是廣播了ApplicationStartingEvent?事件來觸發監聽這個事件的ApplicationListener監聽器。
默認情況下SpringBoot提供了一個EventPublishingRunListener,它實現了SpringApplicationRunListener接口,默認情況下會利用EventPublishingRunListener發布一個ApplicationContextInitializedEvent事件,程序員可以通過定義ApplicationListener來消費這個事件。
因此如果自定義了ApplicationListener?并且監聽了ApplicationStartingEvent(應用程序開始啟動)事件,則這個監聽器將會被觸發,通過這個事件就可以知道應用程序什么時候開始啟動了。
- ApplicationListener是用來監聽spring發布的事件
- SpringApplicationRunListener在spring boot啟動的過程中,會監聽回調一些方法,它里面的每個方法都是表示做完了某個步驟或者該做某個步驟時要執行的方法。所以如果我們想在spring boot啟動過程中間想要做一些事情,我們就可以通過這個監聽器。可以自定義,spring boot也默認提供了一個實現類:
這個默認實現的SpringApplicationRunListener實現的各個階段的方法就是發布的一個事件,發布告知某某步驟開始執行了,某某步驟執行完成了,這樣我們同樣可以利用ApplicationListener來監聽這些事件,來實現在spring boot啟動過程中間想要做一些事情的效果,也就類似于用自定義的SpringApplicationRunListener來完成了。
所以如果我們想要在spring boot啟動過程中間想要做一些事情,就用上面兩種方法。
3.2 run方法第二步:環境搭建,解析配置文件
這一步主要用于加載系統配置以及用戶的自定義配置(application.properties?),創建Environment對象。源碼如下,在run()方法中:
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
?Environment對象表示環境變量,該對象內部主要包含了:
- 當前操作系統的環境變量
- JVM的一些配置信息
- -D方式所配置的JVM環境變量
下面我們再去看環境搭建源碼,prepareEnvironment?方法內部廣播了ApplicationEnvironmentPreparedEvent事件,源碼如下:
/*** 創建并配置Spring Boot應用將要使用的Environment** @param listeners* @param bootstrapContext* @param applicationArguments* @return*/
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {// 根據不同的web類型創建不同實現的Environment對象,比如webApplication為servlet,則就會返回StandardServletEnvironment實例ConfigurableEnvironment environment = getOrCreateEnvironment();// 配置環境,加載系統屬性配置,這一步會涉及到添加存儲配置鍵值對的對象configureEnvironment(environment, applicationArguments.getSourceArgs());ConfigurationPropertySources.attach(environment);// 廣播環境已準備完成事件,觸發監聽器,就是通過觸發的監聽器加載了用戶自定義配置(application.properties等配置文件)/*** 觸發SpringApplicationRunListener的environmentPrepared()* 默認情況下會利用EventPublishingRunListener發布一個ApplicationEnvironmentPreparedEvent事件,* 程序員可以通過定義ApplicationListener來消費這個事件,* 比如默認情況下會有一個EnvironmentPostProcessorApplicationListener來消費這個事件,* 而這個ApplicationListener接收到這個事件之后,就會解析application.properties、application.yml文件,并將解析出來的鍵值對添加到Environment對象中去。* */listeners.environmentPrepared(bootstrapContext, environment);DefaultPropertiesPropertySource.moveToEnd(environment);// 根據命令行參數中spring.profiles.active屬性配置Environment對象中的activeProfile(比如dev、prod、test)configureAdditionalProfiles(environment);// 為當前應用綁定環境,將環境中spring.main屬性綁定到SpringApplication對象中bindToSpringApplication(environment);// 如果用戶使用spring.main.web-application-type屬性手動設置了webApplicationTypeif (!this.isCustomEnvironment) {// 將環境對象轉換成用戶設置的webApplicationType相關類型,它們是繼承同一個父類,直接強轉environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());}ConfigurationPropertySources.attach(environment);return environment;
}
這里主要有如下過程:
- 創建配置環境 ConfigurableEnvironment
- 加載屬性文件資源
- 配置監聽
環境構建這一步加載了系統環境配置、用戶自定義配置并且廣播了ApplicationEnvironmentPreparedEvent事件,觸發監聽器。
3.2.1 配置文件解析流程圖
就是ConfigDataEnvironmentPostProcessor負責解析的properties和yaml配置文件。
3.2.2 Spring Boot的配置優先級
在加載配置數據的過程中,涉及到了對配置信息按照優先級進行存儲。
這四個對象是有優先級的。這四個其實就相當于Map,加載配置文件的鍵值對就是存入的這四個map中,查詢key對應的value時會根據這從上到下的優先級一個個查詢,只要從一個map中查到了,就不會再去查后面的map了。每一個Map存儲不同類型的配置數據,有的是存儲JVM的(倒數第二個),有的是存儲操作系統的(倒數第一個)。前兩個暫時先不管了。
由此可見,配置是存在優先級的,優先級高的配置方式會覆蓋掉優先級低的配置方式。
2.6以及以后的版本,properties配置文件的優先級比yaml文件要高,但是之前的版本yaml文件優先級比properties配置文件高。
不同位置的配置對應的優先級如下,從上到下的順序:
file指的是應用項目的根目錄,不是classpath這個裝class文件的目錄了,也就是IDEA平時我們寫代碼的根目錄。
下面的file表示的是項目根目錄。
配置文件這里寫在后面的優先級更高。
官方文檔中有配置優先級的更詳細說明:
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
優先級由低到高:
- Default properties (specified by setting SpringApplication.setDefaultProperties).
- @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
- Config data (such as application.properties files).
- A RandomValuePropertySource that has properties only in random.*.
- OS environment variables.
- Java System properties (System.getProperties()).
- JNDI attributes from java:comp/env. 不管它
- ServletContext init parameters.
- ServletConfig init parameters.
- Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
- Command line arguments.
- properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
- @TestPropertySource annotations on your tests.
- Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.
3.3 run方法第三步:打印banner
/*** 打印banner** @param environment* @return*/
private Banner printBanner(ConfigurableEnvironment environment) {// banner模式,可以是console、log、offif (this.bannerMode == Banner.Mode.OFF) {return null;}ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader: new DefaultResourceLoader(null);SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);if (this.bannerMode == Mode.LOG) {return bannerPrinter.print(environment, this.mainApplicationClass, logger);}return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
基本就是依據不同情況打印banner而已,比較簡單。
3.4 run方法第四步:創建Spring IoC容器
創建IoC容器,獲取到ConfigurableApplicationContext上下文對象(IoC容器)。
源碼在run()方法中,如下:
context = createApplicationContext();
跟進代碼,真正執行的是ApplicationContextFactory方法,會利用ApplicationContextFactory.DEFAULT,根據應用類型創建對應的Spring容器。源碼如下:
ApplicationContextFactory DEFAULT = (webApplicationType) -> {try {switch (webApplicationType) {case SERVLET:return new AnnotationConfigServletWebServerApplicationContext();case REACTIVE:return new AnnotationConfigReactiveWebServerApplicationContext();default:return new AnnotationConfigApplicationContext();}}catch (Exception ex) {throw new IllegalStateException("Unable create a default ApplicationContext instance, "+ "you may need a custom ApplicationContextFactory", ex);}
};
- 應用類型為SERVLET,則對應AnnotationConfigServletWebServerApplicationContext
- 應用類型為REACTIVE,則對應AnnotationConfigReactiveWebServerApplicationContext
- 應用類型為普通類型,則對應AnnotationConfigApplicationContext
根據webApplicationType?決定創建的類型,很顯然,我這里的是servlet?,因此創建的是AnnotationConfigServletWebServerApplicationContext。
這一步僅僅是創建了IOC容器,未有其他操作。
3.5 run方法第五步:Spring IoC容器前置處理
這一步真是精華了,在刷新容器之前做準備,其中有一個非常關鍵的操作:將啟動類注入容器,為后續的自動配置奠定基礎。源碼如下:
prepareContext(context, environment, listeners, applicationArguments,printedBanner);
prepareContext()源碼解析如下,內容還是挺多的:
/*** Spring容器準備*/
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments, Banner printedBanner) {// 設置上下文(容器)環境,包括自定義配置和系統環境配置context.setEnvironment(environment);// 執行后置處理,來處理上下文(容器)postProcessApplicationContext(context);// 執行所有初始化器ApplicationContextInitializer對象的initialize方法(這些對象是通過讀取spring.factories加載)applyInitializers(context);// 廣播容器準備完成事件,觸發監聽器listeners.contextPrepared(context);bootstrapContext.close(context);if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// 注冊springApplicationArguments和springBootBanner到bean工廠中ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 注冊啟動參數Bean,這里將容器的參數封裝成了該Bean,注冊到容器中beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {// 設置Banner,將Banner注冊到bean工廠中beanFactory.registerSingleton("springBootBanner", printedBanner);}if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}// 判斷是否開啟了懶加載if (this.lazyInitialization) {context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());}// Load the sources// 獲取啟動類指定的參數,可以是多個Set<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");// 加載啟動類,將啟動類注冊到容器中load(context, sources.toArray(new Object[0]));// 廣播容器已加載完成事件,觸發監聽器listeners.contextLoaded(context);
}
從源碼中可以看出步驟很多,下面將會詳細介紹幾個重點的內容。
3.5.1 調用初始化器
調用執行在SpringApplication?構建過程中設置的初始化器(就是從spring.factories取值的那些)。執行的流程很簡單,遍歷執行,源碼如下圖:
將自定義的ApplicationContextInitializer?放在META-INF/spring.factories中,在此時也是會被調用。
3.5.1.1 利用ApplicationContextInitializer初始化Spring容器對象
默認情況下SpringBoot提供了多個ApplicationContextInitializer,其中比較重要的有ConditionEvaluationReportLoggingListener,別看到它的名字叫XXXListener,但是它確實是實現了ApplicationContextInitializer接口的。在它的initialize()方法中會:
- 將Spring容器賦值給它的applicationContext屬性
- 并且往Spring容器中添加一個ConditionEvaluationReportListener(ConditionEvaluationReportLoggingListener的內部類),它是一個ApplicationListener
- 并生成一個ConditionEvaluationReport對象賦值給它的report屬性
ConditionEvaluationReportListener會負責接收ContextRefreshedEvent事件,也就是Spring容器一旦啟動完畢就會觸發ContextRefreshedEvent,ConditionEvaluationReportListener就會打印自動配置類的條件評估報告。
3.5.2 觸發SpringApplicationRunListener的contextPrepared()
默認情況下會利用EventPublishingRunListener發布一個ApplicationContextInitializedEvent事件,默認情況下暫時沒有ApplicationListener消費這個事件。
3.5.3 調用DefaultBootstrapContext對象的close()
沒什么特殊的,忽略。
3.5.4 判斷是否開啟懶加載
下面這個設置可以讓spring boot中所有的bean都變成懶加載。不設置這個參數的話,默認都是非懶加載的。
以上配置在源碼中的體現。
LazyInitializationBeanFactoryPostProcessor其實就是把容器中所有的bean定義取出來,然后把它們的懶加載屬性全部設置為true,這樣這些bean就都是懶加載了。
3.5.5 加載啟動類,注入容器
這一步就是將啟動類作為配置類注冊到Spring容器中(load()方法)。
將SpringApplication.run(MyApplication.class);中傳入進來的類,比如MyApplication.class,作為Spring容器的配置類加載到IoC容器中,作為后續自動配置的入口。
在SpringApplication?構建過程中將主啟動類放置在了primarySources?這個集合中,此時的getAllSources()即是從其中取值,如下圖:
這里取出的就是主啟動類,當然你的項目中可能不止一個,接下來就是將其加載到IoC容器中了,源碼如下:
load(context, sources.toArray(new Object[0]));
跟著代碼進去,其實主要邏輯都在BeanDefinitionLoader.load()方法,如下圖:
將主啟動類加載到beanDefinitionMap,后續該啟動類將作為開啟自動配置化的入口,后續章節詳細介紹。
3.5.6 觸發SpringApplicationRunListener的contextLoaded()
默認情況下在容器加載完成后,會利用EventPublishingRunListener發布一個ApplicationPreparedEvent事件,通知容器已加載完成。
3.6 run方法第六步:刷新容器【關鍵】
刷新容器完全是Spring?的功能了,比如初始化資源,初始化上下文廣播器等,這個在之前Spring章節已經講過了,就不再詳細介紹,有興趣可以看看前面的Spring源碼筆記。
/*** 刷新應用程序上下文** @param context*/
private void refreshContext(ConfigurableApplicationContext context) {// 注冊一個關閉鉤子,在jvm停止時會觸發,然后退出時執行一定的退出邏輯if (this.registerShutdownHook) {try {// 添加:Runtime.getRuntime().addShutdownHook()// 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)context.registerShutdownHook();} catch (AccessControlException ex) {// Not allowed in some environments.}}// ApplicationContext真正開始初始化容器和創建bean的階段refresh((ApplicationContext) context);
}protected void refresh(ApplicationContext applicationContext) {Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);refresh((ConfigurableApplicationContext) applicationContext);
}protected void refresh(ConfigurableApplicationContext applicationContext) {//調用創建的容器applicationContext中的refresh()方法applicationContext.refresh();
}
調用應用上下文對象的refresh()方法,接下來我們到ConfigurableApplicationContext類中去看下這個方法:
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {void refresh() throws BeansException, IllegalStateException;
}
這是一個接口,且這個類是在Spring框架中,并非Spring Boot中的類,它的實現類共有三個:
AbstractApplicationContext是一個抽象類,其余兩個類都繼承了它,我們來看看這個抽象類refresh()的代碼:
@Override
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");// 第一步:刷新上下文環境prepareRefresh();// 第二步:獲取上下文內部BeanFactory。初始化BeanFactory,解析XML,相當于之前的XmlBeanFactory的操作。ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 第三步:對BeanFactory做預備工作/*** 為上下文準備BeanFactory,即對BeanFactory的各種功能進行填充,如常用的注解@Autowired @Qualifier等* 添加ApplicationContextAwareProcessor處理器* 在依賴注入忽略實現*Aware的接口,如EnvironmentAware、ApplicationEventPublisherAware等* 注冊依賴,如一個bean的屬性中含有ApplicationEventPublisher(beanFactory),則會將beanFactory的實例注入進去*/prepareBeanFactory(beanFactory);try {// 第四步:允許在上下文子類中對bean工廠進行post-processing,即即子類處理自定義的BeanFactoryPostProcesspostProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");// 第五步:調用上下文中注冊為bean的工廠 BeanFactoryPostProcessor/*** 激活各種BeanFactory處理器,包括BeanDefinitionRegistryBeanFactoryPostProcessor和普通的BeanFactoryPostProcessor* 執行對應的postProcessBeanDefinitionRegistry方法 和 postProcessBeanFactory方法*/invokeBeanFactoryPostProcessors(beanFactory);// 第六步:注冊攔截bean創建的攔截器/*** 注冊攔截Bean創建的Bean處理器,即注冊BeanPostProcessor,不是BeanFactoryPostProcessor,注意兩者的區別* 注意,這里僅僅是注冊,并不會執行對應的方法,將在bean的實例化時執行對應的方法*/registerBeanPostProcessors(beanFactory);beanPostProcess.end();// 第七步:初始化上下文中的資源文件,如初始化MessageSource(國際化相關)等initMessageSource();// 第八步:初始化容器事件廣播器(用來發布事件),并放入applicatioEventMulticaster,如ApplicationEventPublisherinitApplicationEventMulticaster();// 第九步:初始化一些特殊的bean,比如在這一步就會啟動Tomcat(如果設置Tomcat作為Web服務器的話)onRefresh();// 第十步:將所有監聽器(在所有bean中查找listener bean)注冊到前兩步創建的事件廣播器中registerListeners();// 第十一步:結束bean的初始化工作(主要將所有單例BeanDefinition實例化)/*** 設置轉換器* 注冊一個默認的屬性值解析器* 凍結所有的bean定義,說明注冊的bean定義將不能被修改或進一步的處理* 初始化剩余的非惰性的bean,即初始化非延遲加載的bean*/finishBeanFactoryInitialization(beanFactory);// 第十二步:afterRefresh。上下文(容器)刷新完畢,發布相應事件/*** 通過spring的事件發布機制發布ContextRefreshedEvent事件,以保證對應的監聽器做進一步的處理* 即對那種在spring啟動后需要處理的一些類,這些類實現了ApplicationListener<ContextRefreshedEvent>,* 這里就是要觸發這些類的執行(執行onApplicationEvent方法)* 另外,spring的內置Event有ContextClosedEvent、ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、RequestHandleEvent* 完成初始化,通知生命周期處理器lifeCycleProcessor刷新過程,同時發出ContextRefreshEvent通知其他人*/finishRefresh();} catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;} finally {// Reset common introspection caches in Spring's core, since we// might not ever need metadata for singleton beans anymore...resetCommonCaches();contextRefresh.end();}}
}
這個源碼是不是看著很熟悉,這個就是我們在學習Spring初始化容器源碼時的代碼。詳見:Spring容器的初始化過程(IoC容器的啟動過程)
這里有非常多的步驟,上下文對象主要的bean也是在這里進行處理的,具體的說明可以看注釋。
就是在refresh()的onRefresh()方法中,啟動了Tomcat:
啟動Tomcat(啟動WebServer)
因為我們這里是web應用,所以實現類是ServletWebServerApplicationContext,我們看下這個類refresh()的代碼:
@Override
public final void refresh() throws BeansException, IllegalStateException {try {super.refresh();}catch (RuntimeException ex) {WebServer webServer = this.webServer;if (webServer != null) {webServer.stop();}throw ex;}
}
主要還是調用父類方法,沒有什么特殊的。
用Spring容器的refresh()方法,結合 3.4 run方法第四步:創建Spring IoC容器 和 3.5.4 加載啟動類,注入容器 這兩個步驟,相當于執行了這樣一個流程:
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(MyApplication.class)
applicationContext.refresh()
3.7 run方法第七步:spring容器后置處理 afterRefresh()
一個擴展方法,源碼如下:
afterRefresh(context, applicationArguments);
該方法默認為空,如果有自定義需求可以重寫,比如打印一些啟動結束日志等。
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
3.8 run方法第八步:發出結束執行的事件
觸發SpringApplicationRunListener的started(),發布ApplicationStartedEvent事件和AvailabilityChangeEvent事件,AvailabilityChangeEvent事件表示狀態變更狀態,變更后的狀態為LivenessState.CORRECT。
LivenessState枚舉有兩個值:
- CORRECT:表示當前應用正常運行中
- BROKEN:表示當前應用還在運行,但是內部出現問題,暫時還沒發現哪里用到了
這一步同樣是EventPublishingRunListener?這個監聽器,廣播ApplicationStartedEvent事件。
但是這里廣播事件和前幾次不同,并不是廣播給SpringApplication?中的監聽器(在構建過程中從spring.factories?文件獲取的監聽器),現在IoC容器已經創建好了,可以廣播給IoC容器中的監聽器了。因此在IoC容器?中注入的監聽器(使用@Component?等方式注入的)當前也能夠生效。前面幾個事件只有在spring.factories文件中設置的監聽器才會生效。
跟著代碼進入,可以看到started()方法源碼如下:
這里并沒有用事件廣播器SimpleApplicationEventMulticaster?廣播事件,而是使用ConfigurableApplicationContext?直接在IoC容器中發布事件。
3.9 run方法第九步:執行Runners
Spring Boot? 提供了兩種Runner?讓我們定制一些額外的操作,分別是CommandLineRunner?和ApplicationRunner,關于這兩個的區別,后面文章詳細介紹。
調用的源碼如下:
callRunners(context, applicationArguments);
?跟進代碼,其實真正調執行的是如下方法:
- 獲取Spring容器中的ApplicationRunner類型的Bean
- 獲取Spring容器中的CommandLineRunner類型的Bean
- 執行它們的run()
邏輯很簡單,從IoC容器中獲取,遍歷調用它們的run()方法。
3.10 總結
Spring Boot 啟動流程相對簡單些,打印Banner那步并不是很重要,所以重要的就是以下八個步驟,希望能夠幫助讀者理解,流程圖如下:
run方法啟動后
主要做如下幾件事情:
- 發出啟動結束事件
- 執行實現ApplicationRunner或CommandLineRunner的run方法
- 發布應用程序已啟動(ApplicationStartedEvent)事件
run方法異常處理
如果run方法的處理過程中發生異常,則對exitCode進行相應處理
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, SpringApplicationRunListeners listeners) {try {try {handleExitCode(context, exception);if (listeners != null) {listeners.failed(context, exception);}} finally {reportFailure(getExceptionReporters(context), exception);if (context != null) {context.close();}}} catch (Exception ex) {logger.warn("Unable to close ApplicationContext", ex);}ReflectionUtils.rethrowRuntimeException(exception);
}
至此,所有Spring Boot的啟動流程已經完成,你的項目也順利的跑起來了。我們需要重點理解run()方法執行的九個步驟以及事件、初始化器、監聽器等組件的執行時間點。
四、Spring Boot啟動過程流程圖
更全面的一個流程圖:
?相關文章:【Spring】Java SPI機制及Spring Boot使用實例_springboot spi-CSDN博客
? ? ? ? ? ? ? ? ??【Spring】面試官,請別再問Spring Bean的生命周期了!_give any instantiationawarebeanpostprocessors the -CSDN博客