一張長圖透徹理解 SpringBoot 啟動原理
雖然Java程序員大部分工作都是CRUD,但是工作中常用的中間件必須和Spring集成,如果不知道Spring的原理,很難理解這些中間件和框架的原理。
一張長圖透徹解釋 Spring啟動順序
圖片
測試對Spring啟動原理的理解程度
我舉個例子,測試一下,你對Spring啟動原理的理解程度。
- Rpc框架和Spring的集成問題。Rpc框架何時注冊暴露服務,在哪個Spring擴展點注冊呢?init-method 中行不行?
- MQ 消費組和Spring的集成問題。MQ消費者何時開始消費,在哪個Spring擴展點”注冊“自己?init-method 中行不行?
- SpringBoot 集成Tomcat問題。如果出現(xiàn)已開啟Http流量,Spring還未啟動完成,怎么辦?Tomcat何時開啟端口,對外服務?
SpringBoot項目常見的流量入口無外乎 Rpc、Http、MQ 三種方式。一名合格的架構師必須精通服務的入口流量何時開啟,如何正確開啟?最近我遇到的兩次線上故障都和Spring啟動過程相關。
故障的具體表現(xiàn)是:Kafka消費組已經(jīng)開始消費,已開啟流量,然而Spring 還未啟動完成。因為業(yè)務代碼中使用的Spring Event事件訂閱組件還未啟動(訂閱者還未注冊到Spring),所以處理異常,出了線上故障。根本原因是————項目在錯誤的時機開啟 MQ 流量,然而Spring還未啟動完成,導致出現(xiàn)故障。
正確的做法是:項目在Spring啟動完成后開啟入口流量,然而我司的Kafka消費組在Spring init-method bean 實例化階段就開啟了流量,導致故障發(fā)生。
接下來,我再次拋出 11 個問題,說明這個問題————深入理解Spring啟動原理的重要性。
- Spring還未完全啟動,在 PostConstruct 中調(diào)用 getBeanByAnnotation 能否獲得準確的結果?
- 項目應該如何監(jiān)聽 Spring 的啟動就緒事件?
- 項目如何監(jiān)聽Spring 刷新事件?
- Spring就緒事件和刷新事件的執(zhí)行順序和區(qū)別?
- Http 流量入口何時啟動完成?
- 項目中在 init-method 方法中注冊 Rpc 是否合理?什么是合理的時機?
- 項目中在 init-method 方法中注冊 MQ 消費組是否合理?什么是合理的時機?
- PostConstruct 中方法依賴ApplicationContextAware拿到 ApplicationContext,兩者的順序誰先誰后?是否會出現(xiàn)空指針!
- init-method、PostConstruct、afterPropertiesSet 三個方法的執(zhí)行順序?
- 有兩個 Bean聲明了初始化方法。A使用 PostConstruct注解聲明,B使用 init-method 聲明。Spring一定先執(zhí)行 A 的PostConstruct 方法嗎?
- Spring 何時裝配Autowire屬性,PostConstruct 方法中引用 Autowired 字段什么場景會空指針?
精通Spring 啟動原理,以上問題則迎刃而解。接下來,大家一起學習Spring的啟動原理,看看Spring的擴展點分別在何時執(zhí)行。
一起數(shù)數(shù) Spring啟動過程的擴展點有幾個?
Spring的擴展點極多,這里為了講清楚啟動原理,所以只列舉和啟動過程有關的擴展點。
- BeanFactoryAware 可在Bean 中獲取 BeanFactory 實例
- ApplicationContextAware 可在Bean 中獲取 ApplicationContext 實例
- BeanNameAware 可以在Bean中得到它在IOC容器中的Bean的實例的名字。
- ApplicationListener 可監(jiān)聽 ContextRefreshedEvent等。
- CommandLineRunner 整個項目啟動完畢后,自動執(zhí)行
- SmartLifecycle#start 在Spring Bean實例化完成后,執(zhí)行start 方法。
- 使用@PostConstruct注解,用于Bean實例初始化
- 實現(xiàn)InitializingBean接口,用于Bean實例初始化
- xml 中聲明 init-method 方法,用于Bean實例初始化
- Configuration 配置類 通過@Bean注解 注冊Bean到Spring
- BeanPostProcessor 在Bean的初始化前后,植入擴展點!
- BeanFactoryPostProcessor 在BeanFactory創(chuàng)建后植入 擴展點!
通過打印日志學習Spring的執(zhí)行順序
首先我們先通過代碼實驗,驗證一下以上擴展點的執(zhí)行順序。
1.聲明 TestSpringOrder 分別繼承以下接口,并且在接口方法實現(xiàn)中,日志打印該接口的名稱。
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 ");
}
}
執(zhí)行以上代碼后,可以在日志中看到啟動順序!
實際的執(zhí)行順序
2023-11-25 18:10:53,748 [main] ERROR (TestSpringOrder3:37) - 啟動順序:BeanFactoryPostProcessor postProcessBeanFactory
2023-11-25 18:10:59,299 [main] ERROR (TestSpringOrder:53) - 啟動順序:構造函數(shù) 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 注解方法執(zhí)行
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 斷點,調(diào)試代碼,整理出 Spring啟動原理的 長圖。過程省略…………
一張長圖透徹解釋 Spring啟動順序
圖片
實例化和初始化的區(qū)別
new TestSpringOrder():new 創(chuàng)建對象實例,即為實例化一個對象;執(zhí)行該Bean的 init-method 等方法 為初始化一個Bean。注意初始化和實例化的區(qū)別。
Spring 重要擴展點的啟動順序
1.BeanFactoryPostProcessor
BeanFactory初始化之后,所有的Bean定義已經(jīng)被加載,但Bean實例還沒被創(chuàng)建(不包括BeanFactoryPostProcessor類型)。Spring IoC容器允許BeanFactoryPostProcessor讀取配置元數(shù)據(jù),修改bean的定義,Bean的屬性值等。
2.實例化Bean
Spring 調(diào)用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初始化方法前后要添加一些自己邏輯處理??梢蕴峁?nbsp;BeanPostProcessor接口實現(xiàn)類,然后注冊到Spring IoC容器中。在此接口中,可以創(chuàng)建Bean的代理,甚至替換這個Bean。
8.PostConstruct 執(zhí)行
接下來 Spring會依次調(diào)用 Bean實例初始化的 三大方法。
9.InitializingBean
afterPropertiesSet。
10.init-method
方法執(zhí)行。
11.BeanPostProcessor postProcessAfterInitialization
在 Spring 對Bean的初始化方法執(zhí)行完成后,執(zhí)行該方法。
12.其他Bean 實例化和初始化
Spring 會循環(huán)初始化Bean。直至所有的單例Bean都完成初始化。
13.所有單例Bean 初始化完成后
14.SmartInitializingSingleton Bean實例化后置處理
該接口的執(zhí)行時機在所有的單例Bean執(zhí)行完成后。例如Spring 事件訂閱機制的 EventListener注解,所有的訂閱者都是在這個位置被注冊進 Spring的。而在此之前,Spring Event訂閱機制還未初始化完成。所以如果有 MQ、Rpc 入口流量在此之前開啟,Spring Event就可能出問題!
所以Http、MQ、Rpc 入口流量必須在 SmartInitializingSingleton 之后開啟流量。
15.Spring 提供的擴展點,在所有單例Bean的 EventListener等組件全部啟動完成后,即Spring啟動完成,則執(zhí)行 start 方法。在這個位置適合開啟入口流量!
Http、MQ、Rpc 入口流量適合 在 SmartLifecyle 中開啟
16.發(fā)布 ContextRefreshedEvent 方法
該事件會執(zhí)行多次,在 Spring Refresh 執(zhí)行完成后,就會發(fā)布該事件!
17.注冊和初始化 Spring MVC
SpringBoot 應用,在父級 Spring啟動完成后,會嘗試啟動 內(nèi)嵌式 tomcat容器。在此之前,SpringBoot會初始化 SpringMVC 和注冊DispatcherServlet到Web容器。
18.Tomcat/Jetty 容器開啟端口
SpringBoot 調(diào)用內(nèi)嵌式容器,會開啟并監(jiān)聽端口,此時Http流量就開啟了。
19.應用啟動完成后,執(zhí)行 CommandLineRunner
SpringBoot 特有的機制,待所有的完全執(zhí)行完成后,會執(zhí)行該接口 run方法。值得一提的是,由于此時Http流量已經(jīng)開啟,如果此時進行本地緩存初始化、預熱緩存等,稍微有些晚了!在這個間隔期,可能緩存還未就緒!
所以預熱緩存的時機應該發(fā)生在 入口流量開啟之前,比較合適的機會是在 Bean初始化的階段。雖然 在Bean初始化時 Spring尚未完成啟動,但是調(diào)用 Bean預熱緩存也是可以的。但是注意:不要在 Bean初始化時 使用 Spring Event,因為它還未完成初始化 。
回答 關于 Spring 啟動原理的若干問題
1.init-method、PostConstruct、afterPropertiesSet 三個方法的執(zhí)行順序。
回答:PostConstruct,afterPropertiesSet,init-method
2.有兩個 Bean聲明了初始化方法。A使用 PostConstruct注解聲明,B使用 init-method 聲明。Spring一定先執(zhí)行 A 的PostConstruct 方法嗎?
回答:Spring 會循環(huán)初始化Bean實例,初始化完成1個Bean,再初始化下一個Bean。A、B兩個Bean的初始化順序不確定,誰先誰后不確定。無法保證 A 的PostConstruct 一定先執(zhí)行。除非使用 Order注解,聲明Bean的初始化順序!
3.Spring 何時裝配Autowire屬性,PostConstruct方法中引用 Autowired 字段是否會空指針?
Autowired裝配依賴發(fā)生在 PostConstruct之前,不會出現(xiàn)空指針!
4.PostConstruct 中方法依賴ApplicationContextAware拿到 ApplicationContext,兩者的順序誰先誰后?是否會出現(xiàn)空指針!
ApplicationContextAware 會先執(zhí)行,不會出現(xiàn)空指針!但是當Autowired沒有找到對應的依賴,并且聲明了非強制依賴時,該字段會為空,有潛在空指針風險。
5.項目應該如何監(jiān)聽 Spring 的啟動就緒事件。
通過SmartLifecyle start方法,監(jiān)聽Spring就緒 。適合在此開啟入口流量!
6.項目如何監(jiān)聽Spring 刷新事件。
監(jiān)聽 Spring Event ContextRefreshedEvent
7.Spring就緒事件和刷新事件的執(zhí)行順序和區(qū)別。
Spring就緒事件會先于 刷新事件。兩者都可能多次執(zhí)行,要確保方法的冪等處理,避免重復注冊問題
8.Http 流量入口何時啟動完成。
SpringBoot 最后階段,啟動完成Spring 上下文,才開啟Http入口流量,此時 SmartLifecycle#start 已執(zhí)行。所有單例Bean和SpringEvent等組件都已經(jīng)就緒!
9.項目中在 init-method 方法中注冊 Rpc是否合理?什么是合理的時機?
init 開啟Rpc流量非常不合理。因為Spring尚未啟動完成,包括 Spring Event尚未就緒!
10.項目中在 init-method 方法中注冊 MQ消費組是否合理?什么是合理的時機?
init 開啟 MQ 流量非常不合理。因為Spring尚未啟動完成,包括 Spring Event尚未就緒!
11.Spring還未完全啟動,在 PostConstruct 中調(diào)用 getBeanByAnnotation能否獲得準確的結果?
雖然未啟動完成,但是Spring執(zhí)行該getBeanByAnnotation方法時,會率先檢查 Bean定義,如果Bean定義對應的 Bean尚未初始化,則初始化這些Bean。所以即便是Spring初始化過程中調(diào)用,調(diào)用結果是準確的。
源碼級別介紹
SmartInitializingSingleton 接口的執(zhí)行位置
下圖代碼說明了,Spring在初始化全部 單例Bean以后,會執(zhí)行 SmartInitializingSingleton 接口。
圖片
Autowired 何時裝配Bean的依賴
在Bean實例化之后,但初始化之前,AutowiredAnnotationBeanPostProcessor 會注入Autowired字段。
圖片
SpringBoot 何時開啟Http端口
下圖代碼中可以看到,SpringBoot會首先啟動 Spring上下文,完成后才啟動 嵌入式Web容器,初始化SpringMVC,監(jiān)聽端口。
圖片
Spring 初始化Bean的關鍵代碼
下圖我加了注釋,Spring初始化Bean的關鍵代碼,全在 這個方法里,感興趣的可以自行查閱代碼 。
AbstractAutowireCapableBeanFactory#initializeBean。
Spring CommandLineRunner 執(zhí)行位置
Spring Boot外部,當啟動完Spring上下文以后,最后才啟動 CommandLineRunner。
總結
SpringBoot 會在Spring完全啟動完成后,才開啟Http流量。這給了我們啟示:應該在Spring啟動完成后開啟入口流量。Rpc和 MQ流量 也應該如此,所以建議大家 在 SmartLifecype 或者 ContextRefreshedEvent 等位置 注冊服務,開啟流量。
例如 Spring Cloud Eureka 服務發(fā)現(xiàn)組件,就是在 SmartLifecype中注冊服務的!