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