解析Spring Boot 如何讓你的 bean 在其他 bean 之前完成加載
問題
今天有個(gè)小伙伴給我出了一個(gè)難題:在 SpringBoot 中如何讓自己的某個(gè)指定的 Bean 在其他 Bean 前完成被 Spring 加載?我聽到這個(gè)問題的第一反應(yīng)是,為什么會(huì)有這樣奇怪的需求?
Talk is cheap,show me the code,這里列出了那個(gè)想做最先加載的“天選 Bean” 的代碼,我們來(lái)分析一下:
/** * 系統(tǒng)屬性服務(wù) **/ @Service public class SystemConfigService { // 訪問 db 的 mapper private final SystemConfigMapper systemConfigMapper; // 存放一些系統(tǒng)配置的緩存 map private static Map<String, String>> SYS_CONF_CACHE = new HashMap<>() // 使用構(gòu)造方法完成依賴注入 public SystemConfigServiceImpl(SystemConfigMapper systemConfigMapper) { this.systemConfigMapper = systemConfigMapper; } // Bean 的初始化方法,撈取數(shù)據(jù)庫(kù)中的數(shù)據(jù),放入緩存的 map 中 @PostConstruct public void init() { // systemConfigMapper 訪問 DB,撈取數(shù)據(jù)放入緩存的 map 中 // SYS_CONF_CACHE.put(key, value); // ... } // 對(duì)外提供獲得系統(tǒng)配置的 static 工具方法 public static String getSystemConfig(String key) { return SYS_CONF_CACHE.get(key); } // 省略了從 DB 更新緩存的代碼 // ... }
看過(guò)了上面的代碼后,很容易就理解了為什么會(huì)標(biāo)題中的需求了。
SystemConfigService
是一個(gè)提供了查詢系統(tǒng)屬性的服務(wù),系統(tǒng)屬性存放在 DB 中并且讀多寫少,在 Bean 創(chuàng)建的時(shí)候,通過(guò) @PostConstruct 注解的 init() 方法完成了數(shù)據(jù)加載到緩存中,最關(guān)鍵的是,由于是系統(tǒng)屬性,所以需要在很多地方都想使用,尤其需要在很多 bean 啟動(dòng)的時(shí)候使用,為了方便就提供了 static 方法來(lái)方便調(diào)用,這樣其他的 bean 不需要依賴注入就可以直接調(diào)用,但問題是系統(tǒng)屬性是存在 db 里面的,這就導(dǎo)致了不能把 SystemConfigService做成一個(gè)純「工具類」,它必須要被 Spring 托管起來(lái),完成 mapper 的注入才能正常工作。因此這樣一來(lái)就比較麻煩,其他的類或者 Bean 如果想安全的使用 SystemConfigService#getSystemConfig
中的獲取配置的靜態(tài)方法,就必須等 SystemConfigService
先被 Spring 創(chuàng)建加載起來(lái),完成 init() 方法后才可以。
所以才有了最開頭提到的問題,如何讓這個(gè) Bean 在其他的 Bean 之前加載。
SpringBoot 官方文檔推薦做法
這里引用了一段 Spring Framework 官方文檔的原文:
Constructor-based or setter-based DI?
Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Autowired annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.
可以看到 Spring 對(duì)于依賴注入更推薦(is preferable)使用構(gòu)造函數(shù)來(lái)注入必須的依賴,用 setter 方法來(lái)注入可選的依賴。至于我們平時(shí)工作中更多采用的 @Autowired 注解 + 屬性的注入方式是不推薦的,這也是為什么你用 Idea 集成開發(fā)環(huán)境的時(shí)候會(huì)給你一個(gè)警告。
按照 Spring 的文檔,我們應(yīng)該直接去掉 getSystemConfig 的 static 修飾,讓 getSystemConfig 變成一個(gè)實(shí)例方法,讓每個(gè)需要依賴的 SystemConfigService
的 Bean 通過(guò)構(gòu)造函數(shù)完成依賴注入,這樣 Spring 會(huì)保證每個(gè) Bean 在創(chuàng)建之前會(huì)先把它所有的依賴創(chuàng)建并初始化完成。
看來(lái)我們還是要想一些其他的方法來(lái)達(dá)成我們的目的。
嘗試解決問題的一些方法
@Order 注解或者實(shí)現(xiàn) org.springframework.core.Ordered
最先想到的就是 Spring 提供的 Order 相關(guān)的注解和接口,實(shí)際上測(cè)試下來(lái)不可行。Order 相關(guān)的方法一般用來(lái)控制 Spring 自身組件相關(guān) Bean 的順序,比如 ApplicationListener,RegistrationBean 等,對(duì)于我們自己使用 @Service @Compont 注解注冊(cè)的業(yè)務(wù)相關(guān)的 bean 沒有排序的效果。
@AutoConfigureOrder/@AutoConfigureAfter/@AutoConfigureBefore 注解
測(cè)試下來(lái)這些注解也是不可行,它們和 Ordered 一樣都是針對(duì) Spring 自身組件 Bean 的順序。
@DependsOn 注解
接下來(lái)是嘗試加上 @DependsOn 注解:
@Service @DependsOn({"systemConfigService"}) public class BizService { public BizService() { String xxValue = SystemConfigService.getSystemConfig("xxKey"); // 可行 } }
這樣測(cè)試下來(lái)是可以是可以的,就是操作起來(lái)也太麻煩了,需要讓每個(gè)每個(gè)依賴 SystemConfigService的 Bean 都改代碼加上注解,那有沒有一種默認(rèn)就讓 SystemConfigService 提前的方法?
上面提到的方法都不好用,那我們只能利用 spring 給我們提供的擴(kuò)展點(diǎn)來(lái)做文章了。
Spring 中 Bean 創(chuàng)建的相關(guān)知識(shí)
首先要明白一點(diǎn),Bean 創(chuàng)建的順序是怎么來(lái)的,如果你對(duì) Spring 的源碼比較熟悉,你會(huì)知道在 AbstractApplicationContext
里面有個(gè) refresh 方法, Bean 創(chuàng)建的大部分邏輯都在 refresh 方法里面,在 refresh 末尾的 finishBeanFactoryInitialization(beanFactory)
方法調(diào)用中,會(huì)調(diào)用 beanFactory.preInstantiateSingletons()
,在這里對(duì)所有的 beanDefinitionNames
一一遍歷,進(jìn)行 bean 實(shí)例化和組裝:
這個(gè) beanDefinitionNames 列表的順序就決定了 Bean 的創(chuàng)建順序,那么這個(gè) beanDefinitionNames 列表又是怎么來(lái)的?答案是 ConfigurationClassPostProcessor 通過(guò)掃描你的代碼和注解生成的,將 Bean 掃描解析成 Bean 定義(BeanDefinition),同時(shí)將 Bean 定義(BeanDefinition)注冊(cè)到 BeanDefinitionRegistry 中,才有了 beanDefinitionNames 列表。
ConfigurationClassPostProcessor 的介紹
在 BeanFactory 初始化之后調(diào)用,來(lái)定制和修改 BeanFactory 的內(nèi)容
所有的 Bean 定義(BeanDefinition)已經(jīng)保存加載到 beanFactory,但是 Bean 的實(shí)例還未創(chuàng)建
方法的入?yún)⑹?ConfigurrableListableBeanFactory,意思是你可以調(diào)整 ConfigurrableListableBeanFactory 的配置
BeanDefinitionRegistryPostProcessor 相關(guān)接口的介紹
接下來(lái)還要介紹 Spring 中提供的一些擴(kuò)展,它們?cè)?Bean 的創(chuàng)建過(guò)程中起到非常重要的作用。
BeanFactoryPostProcessor 它的作用:
- 在 BeanFactory 初始化之后調(diào)用,來(lái)定制和修改 BeanFactory 的內(nèi)容
- 所有的 Bean 定義(BeanDefinition)已經(jīng)保存加載到 beanFactory,但是 Bean 的實(shí)例還未創(chuàng)建
- 方法的入?yún)⑹?ConfigurrableListableBeanFactory,意思是你可以調(diào)整 ConfigurrableListableBeanFactory 的配置
BeanDefinitionRegistryPostProcessor 它的作用:
- 是 BeanFactoryPostProcessor 的子接口
- 在所有 Bean 定義(BeanDefinition)信息將要被加載,Bean 實(shí)例還未創(chuàng)建的時(shí)候加載
- 優(yōu)先于 BeanFactoryPostProcessor 執(zhí)行,利用 BeanDefinitionRegistryPostProcessor 可以給 Spring 容器中自定義添加 Bean
- 方法入?yún)⑹?BeanDefinitionRegistry,意思是你可以調(diào)整 BeanDefinitionRegistry 的配置
還有一個(gè)類似的 BeanPostProcessor 它的作用:
- 在 Bean 實(shí)例化之后執(zhí)行的
- 執(zhí)行順序在 BeanFactoryPostProcessor 之后
- 方法入?yún)⑹?Object bean,意思是你可以調(diào)整 bean 的配置
搞明白了以上的內(nèi)容,下面我們可以直接動(dòng)手寫代碼了。
最終答案
第一步:通過(guò) spring.factories 擴(kuò)展來(lái)注冊(cè)一個(gè) ApplicationContextInitializer:
# 注冊(cè) ApplicationContextInitializer org.springframework.context.ApplicationContextInitializer=com.antbank.demo.bootstrap.MyApplicationContextInitializer
注冊(cè) ApplicationContextInitializer
的目的其實(shí)是為了接下來(lái)注冊(cè) BeanDefinitionRegistryPostProcessor
到 Spring 中,我沒有找到直接使用 spring.factories
來(lái)注冊(cè) BeanDefinitionRegistryPostProcessor
的方式,猜測(cè)是不支持的:
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { // 注意,如果你同時(shí)還使用了 spring cloud,這里需要做個(gè)判斷,要不要在 spring cloud applicationContext 中做這個(gè)事 // 通常 spring cloud 中的 bean 都和業(yè)務(wù)沒關(guān)系,是需要跳過(guò)的 applicationContext.addBeanFactoryPostProcessor(new MyBeanDefinitionRegistryPostProcessor()); } }
除了使用 spring 提供的 SPI 來(lái)注冊(cè) ApplicationContextInitializer
,你也可以用 SpringApplication.addInitializers
的方式直接在 main 方法中直接注冊(cè)一個(gè) ApplicationContextInitializer
結(jié)果都是可以的:
@SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication application = new SpringApplication(SpringBootDemoApplication.class); // 通過(guò) SpringApplication 注冊(cè) ApplicationContextInitializer application.addInitializers(new MyApplicationContextInitializer()); application.run(args); } }
當(dāng)然了,通過(guò) Spring 的事件機(jī)制也可以做到注冊(cè) BeanDefinitionRegistryPostProcessor,選擇實(shí)現(xiàn)合適的 ApplicationListener 事件,可以通過(guò) ApplicationContextEvent 獲得 ApplicationContext,即可注冊(cè) BeanDefinitionRegistryPostProcessor,這里就不多展開了。
這里需要注意一點(diǎn),為什么需要用 ApplicationContextInitializer 來(lái)注冊(cè) BeanDefinitionRegistryPostProcessor,能不能用 @Component 或者其他的注解的方式注冊(cè)?
答案是不能的。@Component 注解的方式注冊(cè)能注冊(cè)上的前提是能被 ConfigurationClassPostProcessor 掃描到,也就是說(shuō)用 @Component 注解的方式來(lái)注冊(cè),注冊(cè)出來(lái)的 Bean 一定不可能排在 ConfigurationClassPostProcessor 前面,而我們的目的就是在所有的 Bean 掃描前注冊(cè)你需要的 Bean,這樣才能排在其他所有 Bean 前面,所以這里的場(chǎng)景下是不能用注解注冊(cè)的,這點(diǎn)需要額外注意。
第二步:實(shí)現(xiàn) BeanDefinitionRegistryPostProcessor,注冊(cè)目標(biāo) bean:
用 MyBeanDefinitionRegistryPostProcessor 在 ConfigurationClassPostProcessor 掃描前注冊(cè)你需要的目標(biāo) bean 的 BeanDefinition 即可。
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 手動(dòng)注冊(cè)一個(gè) BeanDefinition registry.registerBeanDefinition("systemConfigService", new RootBeanDefinition(SystemConfigService.class)); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
當(dāng)然你也可以使用一個(gè)類同時(shí)實(shí)現(xiàn) ApplicationContextInitializer 和BeanDefinitionRegistryPostProcessor
通過(guò) applicationContext#addBeanFactoryPostProcessor 注冊(cè)的 BeanDefinitionRegistryPostProcessor,比 Spring 自帶的優(yōu)先級(jí)要高,所以這里就不需要再實(shí)現(xiàn) Ordered 接口提升優(yōu)先級(jí)就可以排在 ConfigurationClassPostProcessor 前面:
經(jīng)過(guò)測(cè)試發(fā)現(xiàn),上面的方式可行的,SystemConfigService 被排在第五個(gè) Bean 進(jìn)行實(shí)例化,排在前面的四個(gè)都是 Spring 自己內(nèi)部的 Bean 了,也沒有必要再提前了。
本文提供的方式并不是唯一的,如果你有更好的方法,歡迎在評(píng)論區(qū)留言交流。
到此這篇關(guān)于Spring Boot 如何讓你的 bean 在其他 bean 之前完成加載 的文章就介紹到這了,更多相關(guān)Spring Boot bean 加載 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot 動(dòng)態(tài)配置郵箱發(fā)件人過(guò)程解析
這篇文章主要介紹了SpringBoot 動(dòng)態(tài)配置郵箱發(fā)件人過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08Eclipse下基于Java的OpenCV開發(fā)環(huán)境配置教程
這篇文章主要為大家詳細(xì)介紹了Eclipse下基于Java的OpenCV開發(fā)環(huán)境配置教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07一文帶你掌握SpringBoot中常見定時(shí)任務(wù)的實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了Spring?Boot中定時(shí)任務(wù)的基本用法、高級(jí)特性以及最佳實(shí)踐,幫助開發(fā)人員更好地理解和應(yīng)用定時(shí)任務(wù),提高系統(tǒng)的穩(wěn)定性和可靠性,需要的可以參考下2024-03-03MyBatis-Plus枚舉和自定義主鍵ID的實(shí)現(xiàn)步驟
這篇文章主要給大家介紹了關(guān)于MyBatis-Plus枚舉和自定義主鍵ID的相關(guān)資料,文中通過(guò)實(shí)例代碼以及圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-02-02maven子模塊相互依賴打包時(shí)報(bào)錯(cuò)找不到類的解決方案
本文主要介紹了maven子模塊相互依賴打包時(shí)報(bào)錯(cuò)找不到類的解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06SpringBoot與rabbitmq的結(jié)合的示例
這篇文章主要介紹了SpringBoot與rabbitmq的結(jié)合的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03SpringBoot下實(shí)現(xiàn)session保持方式
這篇文章主要介紹了SpringBoot下實(shí)現(xiàn)session保持方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03