一張長圖透徹理解 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 實例化階段就開啟了流量,導致故障發生。
接下來,我再次拋出 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的執行順序
首先我們先通過代碼實驗,驗證一下以上擴展點的執行順序。
1.聲明 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");
}
3.新建 TestSpringOrder2
public class TestSpringOrder2 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 之后開啟流量。
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。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中注冊服務的!