面試官:Spring Boot 的啟動原理是什么?
共 12403字,需瀏覽 25分鐘
·
2024-05-24 12:20
??????點擊查看:無需魔法,國內(nèi)可直接使用官方ChatGPT-4!
雖然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ā)生。出現(xiàn)這樣的問題,說明項目初期的程序員沒有深入理解Spring的啟動原理。
接下來,我再次拋出 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 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 ");
}
}
執(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之后開啟流量。
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。Spring并沒有使用這種機制啟動,即所有的Bean先執(zhí)行 PostConstruct,再統(tǒng)一執(zhí)行afterProperfiesSet。
此外,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中注冊服務的!
希望大家有所收獲。
來源:juejin.cn/post/7308610896803659812
