一張長圖透徹理解 SpringBoot 啓動原理
雖然 Java 程序員大部分工作都是 CRUD,但是工作中常用的中間件必須和 Spring 集成,如果不知道 Spring 的原理,很難理解這些中間件和框架的原理。
一張長圖透徹解釋 Spring 啓動順序
測試對 Spring 啓動原理的理解程度
我舉個例子,測試一下,你對 Spring 啓動原理的理解程度。
-
Rpc 框架和 Spring 的集成問題。Rpc 框架何時註冊暴露服務,在哪個 Spring 擴展點註冊呢?
init-method
中行不行? -
MQ 消費組和 Spring 的集成問題。MQ 消費者何時開始消費,在哪個 Spring 擴展點” 註冊 “自己?
init-method
中行不行? -
SpringBoot 集成 Tomcat 問題。如果出現已開啓 Http 流量,Spring 還未啓動完成,怎麼辦?Tomcat 何時開啓端口,對外服務?
SpringBoot 項目常見的流量入口無外乎 Rpc、Http、MQ 三種方式。一名合格的架構師必須精通服務的入口流量何時開啓,如何正確開啓?最近我遇到的兩次線上故障都和 Spring 啓動過程相關。
故障的具體表現是:Kafka 消費組已經開始消費,已開啓流量,然而 Spring 還未啓動完成。因爲業務代碼中使用的 Spring Event 事件訂閱組件還未啓動(訂閱者還未註冊到 Spring),所以處理異常,出了線上故障。根本原因是————項目在錯誤的時機開啓 MQ 流量,然而 Spring 還未啓動完成,導致出現故障。
正確的做法是:項目在 Spring 啓動完成後開啓入口流量,然而我司的 Kafka 消費組 在 Spring init-method bean
實例化階段就開啓了流量,導致故障發生。出現這樣的問題,說明項目初期的程序員沒有深入理解 Spring 的啓動原理。
接下來,我再次拋出 11 個問題,說明這個問題————深入理解 Spring 啓動原理的重要性。
-
Spring 還未完全啓動,在
PostConstruct
中調用getBeanByAnnotation
能否獲得準確的結果? -
項目應該如何監聽 Spring 的啓動就緒事件?
-
項目如何監聽 Spring 刷新事件?
-
Spring 就緒事件和刷新事件的執行順序和區別?
-
Http 流量入口何時啓動完成?
-
項目中在
init-method
方法中註冊 Rpc 是否合理?什麼是合理的時機? -
項目中在
init-method
方法中註冊 MQ 消費組是否合理?什麼是合理的時機? -
PostConstruct
中方法依賴ApplicationContextAware
拿到ApplicationContext
,兩者的順序誰先誰後?是否會出現空指針! -
init-method
、PostConstruct
、afterPropertiesSet
三個方法的執行順序? -
有兩個 Bean 聲明瞭初始化方法。A 使用
PostConstruct
註解聲明,B 使用init-method
聲明。Spring 一定先執行 A 的PostConstruct
方法嗎? -
Spring 何時裝配 Autowire 屬性,
PostConstruct
方法中引用 Autowired 字段什麼場景會空指針?
精通 Spring 啓動原理,以上問題則迎刃而解。接下來,大家一起學習 Spring 的啓動原理,看看 Spring 的擴展點分別在何時執行。
一起數數 Spring 啓動過程的擴展點有幾個?
Spring 的擴展點極多,這裏爲了講清楚啓動原理,所以只列舉和啓動過程有關的擴展點。
-
BeanFactoryAware
可在 Bean 中獲取BeanFactory
實例 -
ApplicationContextAware
可在 Bean 中獲取ApplicationContext
實例 -
BeanNameAware
可以在 Bean 中得到它在 IOC 容器中的 Bean 的實例的名字。 -
ApplicationListener
可監聽ContextRefreshedEvent
等。 -
CommandLineRunner
整個項目啓動完畢後,自動執行 -
SmartLifecycle#start
在 Spring Bean 實例化完成後,執行 start 方法。 -
使用
@PostConstruct
註解,用於 Bean 實例初始化 -
實現
InitializingBean
接口,用於 Bean 實例初始化 -
xml 中聲明
init-method
方法,用於 Bean 實例初始化 -
Configuration
配置類 通過 @Bean 註解 註冊 Bean 到 Spring -
BeanPostProcessor
在 Bean 的初始化前後,植入擴展點! -
BeanFactoryPostProcessor
在BeanFactory
創建後植入 擴展點!
通過打印日誌學習 Spring 的執行順序
首先我們先通過 代碼實驗,驗證一下以上擴展點的執行順序。
- 聲明
TestSpringOrder
分別繼承以下接口,並且在接口方法實現中,日誌打印該接口的名稱。
public class TestSpringOrder implements
ApplicationContextAware,
BeanFactoryAware,
InitializingBean,
SmartLifecycle,
BeanNameAware,
ApplicationListener<ContextRefreshedEvent>,
CommandLineRunner,
SmartInitializingSingleton {
@Override
public void afterPropertiesSet() throws Exception {
log.error("啓動順序:afterPropertiesSet");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
log.error("啓動順序:setApplicationContext");
}
2.TestSpringOrder
使用 PostConstruct
註解初始化,聲明 init-method
方法初始化。
@PostConstruct
public void postConstruct() {
log.error("啓動順序:post-construct");
}
public void initMethod() {
log.error("啓動順序:init-method");
}
- 新建
TestSpringOrder2
繼承
public class TestSpringOrder3 implements
BeanPostProcessor,
BeanFactoryPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
log.error("啓動順序:BeanPostProcessor postProcessBeforeInitialization beanName:{}", beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.error("啓動順序:BeanPostProcessor postProcessAfterInitialization beanName:{}", beanName);
return bean;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
log.error("啓動順序:BeanFactoryPostProcessor postProcessBeanFactory ");
}
}
執行以上代碼後,可以在日誌中看到啓動順序!
實際的執行順序
2023-11-25 18:10:53,748 [main] ERROR (TestSpringOrder3:37) - 啓動順序:BeanFactoryPostProcessor postProcessBeanFactory
2023-11-25 18:10:59,299 [main] ERROR (TestSpringOrder:53) - 啓動順序:構造函數 TestSpringOrder
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:127) - 啓動順序: Autowired
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:129) - 啓動順序:setBeanName
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:111) - 啓動順序:setBeanFactory
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:121) - 啓動順序:setApplicationContext
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder3:25) - 啓動順序:BeanPostProcessor postProcessBeforeInitialization beanName:testSpringOrder
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:63) - 啓動順序:post-construct
2023-11-25 18:10:59,317 [main] ERROR (TestSpringOrder:116) - 啓動順序:afterPropertiesSet
2023-11-25 18:10:59,317 [main] ERROR (TestSpringOrder:46) - 啓動順序:init-method
2023-11-25 18:10:59,320 [main] ERROR (TestSpringOrder3:31) - 啓動順序:BeanPostProcessor postProcessAfterInitialization beanName:testSpringOrder
2023-11-25 18:17:21,563 [main] ERROR (SpringOrderConfiguartion:21) - 啓動順序: @Bean 註解方法執行
2023-11-25 18:17:21,668 [main] ERROR (TestSpringOrder:58) - 啓動順序:SmartInitializingSingleton
2023-11-25 18:17:21,675 [main] ERROR (TestSpringOrder:74) - 啓動順序:start
2023-11-25 18:17:23,508 [main] ERROR (TestSpringOrder:68) - 啓動順序:ContextRefreshedEvent
2023-11-25 18:17:23,574 [main] ERROR (TestSpringOrder:79) - 啓動順序:CommandLineRunner
我通過在以上擴展點 添加 debug 斷點,調試代碼,整理出 Spring 啓動原理的 長圖。過程省略…………
一張長圖透徹解釋 Spring 啓動順序
實例化和初始化的區別
new TestSpringOrder()
:new 創建對象實例,即爲實例化一個對象;執行該 Bean 的 init-method
等方法 爲初始化一個 Bean。注意初始化和實例化的區別。
Spring 重要擴展點的啓動順序
1.BeanFactoryPostProcessor
BeanFactory 初始化之後,所有的 Bean 定義已經被加載,但 Bean 實例還沒被創建(不包括BeanFactoryPostProcessor
類型)。Spring IoC 容器允許BeanFactoryPostProcessor
讀取配置元數據,修改 bean 的定義,Bean 的屬性值等。
2. 實例化 Bean
Spring 調用 java 反射 API 實例化 Bean。等同於 new TestSpringOrder()
;
3.Autowired 裝配依賴
Autowired 是 藉助於 AutowiredAnnotationBeanPostProcessor
解析 Bean 的依賴,裝配依賴。如果被依賴的 Bean 還未初始化,則先初始化 被依賴的 Bean。在 Bean 實例化完成後,Spring 將首先裝配 Bean 依賴的屬性。
4.BeanNameAware
setBeanName
5.BeanFactoryAware
setBeanFactory
6.ApplicationContextAware setApplicationContext
在 Bean 實例化前,會率先設置 Aware 接口,例如 BeanNameAware
BeanFactoryAware
ApplicationContextAware
等
7.BeanPostProcessor postProcessBeforeInitialization
如果我想在 bean 初始化方法前後要添加一些自己邏輯處理。可以提供 BeanPostProcessor
接口實現類,然後註冊到 Spring IoC 容器中。在此接口中,可以創建 Bean 的代理,甚至替換這個 Bean。
8.PostConstruct 執行
接下來 Spring 會依次調用 Bean 實例初始化的 三大方法。
9.InitializingBean
afterPropertiesSet
10.init-method
方法執行
11.BeanPostProcessor postProcessAfterInitialization
在 Spring 對 Bean 的初始化方法執行完成後,執行該方法
12. 其他 Bean 實例化和初始化
Spring 會循環初始化 Bean。直至所有的單例 Bean 都完成初始化
13. 所有單例 Bean 初始化完成後
14.SmartInitializingSingleton Bean 實例化後置處理
該接口的執行時機在 所有的單例 Bean 執行完成後。例如 Spring 事件訂閱機制的 EventListener
註解,所有的訂閱者 都是 在這個位置被註冊進 Spring 的。而在此之前,Spring Event 訂閱機制還未初始化完成。所以如果有 MQ、Rpc 入口流量在此之前開啓,Spring Event 就可能出問題!
所以強烈建議 Http、MQ、Rpc 入口流量在
SmartInitializingSingleton
之後開啓流量。
Http、MQ、Rpc 入口流量必須在 SmartInitializingSingleton
之後開啓流量。
15.Spring 提供的擴展點,在所有單例 Bean 的 EventListener 等組件全部啓動完成後,即 Spring 啓動完成,則執行 start 方法。在這個位置適合開啓入口流量!
Http、MQ、Rpc 入口流量適合 在 SmartLifecyle
中開啓
16. 發佈 ContextRefreshedEvent 方法
該事件會執行多次,在 Spring Refresh
執行完成後,就會發布該事件!
17. 註冊和初始化 Spring MVC
SpringBoot 應用,在父級 Spring 啓動完成後,會嘗試啓動 內嵌式 tomcat 容器。在此之前,SpringBoot 會初始化 SpringMVC 和註冊DispatcherServlet
到 Web 容器。
18.Tomcat/Jetty 容器開啓端口
SpringBoot 調用內嵌式容器,會開啓並監聽端口,此時 Http 流量就開啓了。
19. 應用啓動完成後,執行 CommandLineRunner
SpringBoot 特有的機制,待所有的完全執行完成後,會執行該接口 run 方法。值得一提的是,由於此時 Http 流量已經開啓,如果此時進行本地緩存初始化、預熱緩存等,稍微有些晚了!在這個間隔期,可能緩存還未就緒!
所以預熱緩存的時機應該發生在 入口流量開啓之前,比較合適的機會是在 Bean 初始化的階段。雖然 在 Bean 初始化時 Spring 尚未完成啓動,但是調用 Bean 預熱緩存也是可以的。但是注意:不要在 Bean 初始化時 使用 Spring Event,因爲它還未完成初始化 。
回答 關於 Spring 啓動原理的若干問題
1.init-method、PostConstruct、afterPropertiesSet 三個方法的執行順序。
回答:PostConstruct
,afterPropertiesSet
,init-method
2. 有兩個 Bean 聲明瞭初始化方法。A 使用 PostConstruct 註解聲明,B 使用 init-method 聲明。Spring 一定先執行 A 的 PostConstruct 方法嗎?
回答:Spring 會循環初始化 Bean 實例,初始化完成 1 個 Bean,再初始化下一個 Bean。Spring 並沒有使用這種機制啓動,即所有的 Bean 先執行 PostConstruct
,再統一執行afterProperfiesSet
。
此外,A、B 兩個 Bean 的初始化順序不確定,誰先誰後不確定。無法保證 A 的PostConstruct
一定先執行。除非使用 Order 註解,聲明 Bean 的初始化順序!
3.Spring 何時裝配 Autowire 屬性,PostConstruct 方法中引用 Autowired 字段是否會空指針?
Autowired 裝配依賴發生在 PostConstruct
之前,不會出現空指針!
4.PostConstruct 中方法依賴 ApplicationContextAware 拿到 ApplicationContext,兩者的順序誰先誰後?是否會出現空指針!
ApplicationContextAware
會先執行,不會出現空指針!但是當 Autowired 沒有找到對應的依賴,並且聲明瞭非強制依賴時,該字段會爲空,有潛在 空指針風險。
5. 項目應該如何監聽 Spring 的啓動就緒事件。
通過SmartLifecyle start
方法,監聽 Spring 就緒 。適合在此開啓入口流量!
6. 項目如何監聽 Spring 刷新事件。
監聽 Spring Event ContextRefreshedEvent
7.Spring 就緒事件和刷新事件的執行順序和區別。
Spring 就緒事件會先於 刷新事件。兩者都可能多次執行,要確保方法的冪等處理,避免重複註冊問題
8.Http 流量入口何時啓動完成。
SpringBoot 最後階段,啓動完成 Spring 上下文,纔開啓 Http 入口流量,此時 SmartLifecycle#start
已執行。所有單例 Bean 和 SpringEvent 等組件都已經就緒!
9. 項目中在 init-method 方法中註冊 Rpc 是否合理?什麼是合理的時機?
init 開啓 Rpc 流量非常不合理。因爲 Spring 尚未啓動完成,包括 Spring Event 尚未就緒!
10. 項目中在 init-method 方法中註冊 MQ 消費組是否合理?什麼是合理的時機?
init 開啓 MQ 流量非常不合理。因爲 Spring 尚未啓動完成,包括 Spring Event 尚未就緒!
11.Spring 還未完全啓動,在 PostConstruct 中調用 getBeanByAnnotation 能否獲得準確的結果?
雖然未啓動完成,但是 Spring 執行該getBeanByAnnotation
方法時,會率先檢查 Bean 定義,如果 Bean 定義對應的 Bean 尚未初始化,則初始化這些 Bean。所以即便是 Spring 初始化過程中調用,調用結果是準確的。
源碼級別介紹
SmartInitializingSingleton 接口的執行位置
下圖代碼說明了,Spring 在初始化全部 單例 Bean 以後,會執行 SmartInitializingSingleton
接口。
Autowired 何時裝配 Bean 的依賴
在 Bean 實例化之後,但初始化之前,AutowiredAnnotationBeanPostProcessor
會注入 Autowired 字段。
SpringBoot 何時開啓 Http 端口
下圖代碼中可以看到,SpringBoot 會首先啓動 Spring 上下文,完成後才啓動 嵌入式 Web 容器,初始化 SpringMVC,監聽端口
Spring 初始化 Bean 的關鍵代碼
下圖我加了註釋,Spring 初始化 Bean 的關鍵代碼,全在 這個方法裏,感興趣的可以自行查閱代碼 。
AbstractAutowireCapableBeanFactory#initializeBean
Spring CommandLineRunner 執行位置
Spring Boot 外部,當啓動完 Spring 上下文以後,最後才啓動 CommandLineRunner
。
總結
SpringBoot 會在 Spring 完全啓動完成後,纔開啓 Http 流量。這給了我們啓示:應該在 Spring 啓動完成後開啓入口流量。Rpc 和 MQ 流量 也應該如此,所以建議大家 在 SmartLifecype
或者 ContextRefreshedEvent
等位置 註冊服務,開啓流量。
例如 Spring Cloud Eureka 服務發現組件,就是在 SmartLifecype
中註冊服務的!
整理 10 個小時寫完本篇文章,希望大家有所收穫。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rZEYo_6CvDmFBZR4VwlB6Q