解析Spring Boot 如何讓你的 bean 在其他 bean 之前完成加載
問題
今天有個小伙伴給我出了一個難題:在 SpringBoot 中如何讓自己的某個指定的 Bean 在其他 Bean 前完成被 Spring 加載?我聽到這個問題的第一反應是,為什么會有這樣奇怪的需求?
Talk is cheap,show me the code,這里列出了那個想做最先加載的“天選 Bean” 的代碼,我們來分析一下:
/** * 系統屬性服務 **/ @Service public class SystemConfigService { // 訪問 db 的 mapper private final SystemConfigMapper systemConfigMapper; // 存放一些系統配置的緩存 map private static Map<String, String>> SYS_CONF_CACHE = new HashMap<>() // 使用構造方法完成依賴注入 public SystemConfigServiceImpl(SystemConfigMapper systemConfigMapper) { this.systemConfigMapper = systemConfigMapper; } // Bean 的初始化方法,撈取數據庫中的數據,放入緩存的 map 中 @PostConstruct public void init() { // systemConfigMapper 訪問 DB,撈取數據放入緩存的 map 中 // SYS_CONF_CACHE.put(key, value); // ... } // 對外提供獲得系統配置的 static 工具方法 public static String getSystemConfig(String key) { return SYS_CONF_CACHE.get(key); } // 省略了從 DB 更新緩存的代碼 // ... }
看過了上面的代碼后,很容易就理解了為什么會標題中的需求了。
SystemConfigService
是一個提供了查詢系統屬性的服務,系統屬性存放在 DB 中并且讀多寫少,在 Bean 創(chuàng)建的時候,通過 @PostConstruct 注解的 init() 方法完成了數據加載到緩存中,最關鍵的是,由于是系統屬性,所以需要在很多地方都想使用,尤其需要在很多 bean 啟動的時候使用,為了方便就提供了 static 方法來方便調用,這樣其他的 bean 不需要依賴注入就可以直接調用,但問題是系統屬性是存在 db 里面的,這就導致了不能把 SystemConfigService做成一個純「工具類」,它必須要被 Spring 托管起來,完成 mapper 的注入才能正常工作。因此這樣一來就比較麻煩,其他的類或者 Bean 如果想安全的使用 SystemConfigService#getSystemConfig
中的獲取配置的靜態(tài)方法,就必須等 SystemConfigService
先被 Spring 創(chuàng)建加載起來,完成 init() 方法后才可以。
所以才有了最開頭提到的問題,如何讓這個 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 對于依賴注入更推薦(is preferable)使用構造函數來注入必須的依賴,用 setter 方法來注入可選的依賴。至于我們平時工作中更多采用的 @Autowired 注解 + 屬性的注入方式是不推薦的,這也是為什么你用 Idea 集成開發(fā)環(huán)境的時候會給你一個警告。
按照 Spring 的文檔,我們應該直接去掉 getSystemConfig 的 static 修飾,讓 getSystemConfig 變成一個實例方法,讓每個需要依賴的 SystemConfigService
的 Bean 通過構造函數完成依賴注入,這樣 Spring 會保證每個 Bean 在創(chuàng)建之前會先把它所有的依賴創(chuàng)建并初始化完成。
看來我們還是要想一些其他的方法來達成我們的目的。
嘗試解決問題的一些方法
@Order 注解或者實現 org.springframework.core.Ordered
最先想到的就是 Spring 提供的 Order 相關的注解和接口,實際上測試下來不可行。Order 相關的方法一般用來控制 Spring 自身組件相關 Bean 的順序,比如 ApplicationListener,RegistrationBean 等,對于我們自己使用 @Service @Compont 注解注冊的業(yè)務相關的 bean 沒有排序的效果。
@AutoConfigureOrder/@AutoConfigureAfter/@AutoConfigureBefore 注解
測試下來這些注解也是不可行,它們和 Ordered 一樣都是針對 Spring 自身組件 Bean 的順序。
@DependsOn 注解
接下來是嘗試加上 @DependsOn 注解:
@Service @DependsOn({"systemConfigService"}) public class BizService { public BizService() { String xxValue = SystemConfigService.getSystemConfig("xxKey"); // 可行 } }
這樣測試下來是可以是可以的,就是操作起來也太麻煩了,需要讓每個每個依賴 SystemConfigService的 Bean 都改代碼加上注解,那有沒有一種默認就讓 SystemConfigService 提前的方法?
上面提到的方法都不好用,那我們只能利用 spring 給我們提供的擴展點來做文章了。
Spring 中 Bean 創(chuàng)建的相關知識
首先要明白一點,Bean 創(chuàng)建的順序是怎么來的,如果你對 Spring 的源碼比較熟悉,你會知道在 AbstractApplicationContext
里面有個 refresh 方法, Bean 創(chuàng)建的大部分邏輯都在 refresh 方法里面,在 refresh 末尾的 finishBeanFactoryInitialization(beanFactory)
方法調用中,會調用 beanFactory.preInstantiateSingletons()
,在這里對所有的 beanDefinitionNames
一一遍歷,進行 bean 實例化和組裝:
這個 beanDefinitionNames 列表的順序就決定了 Bean 的創(chuàng)建順序,那么這個 beanDefinitionNames 列表又是怎么來的?答案是 ConfigurationClassPostProcessor 通過掃描你的代碼和注解生成的,將 Bean 掃描解析成 Bean 定義(BeanDefinition),同時將 Bean 定義(BeanDefinition)注冊到 BeanDefinitionRegistry 中,才有了 beanDefinitionNames 列表。
ConfigurationClassPostProcessor 的介紹
在 BeanFactory 初始化之后調用,來定制和修改 BeanFactory 的內容
所有的 Bean 定義(BeanDefinition)已經保存加載到 beanFactory,但是 Bean 的實例還未創(chuàng)建
方法的入參是 ConfigurrableListableBeanFactory,意思是你可以調整 ConfigurrableListableBeanFactory 的配置
BeanDefinitionRegistryPostProcessor 相關接口的介紹
接下來還要介紹 Spring 中提供的一些擴展,它們在 Bean 的創(chuàng)建過程中起到非常重要的作用。
BeanFactoryPostProcessor 它的作用:
- 在 BeanFactory 初始化之后調用,來定制和修改 BeanFactory 的內容
- 所有的 Bean 定義(BeanDefinition)已經保存加載到 beanFactory,但是 Bean 的實例還未創(chuàng)建
- 方法的入參是 ConfigurrableListableBeanFactory,意思是你可以調整 ConfigurrableListableBeanFactory 的配置
BeanDefinitionRegistryPostProcessor 它的作用:
- 是 BeanFactoryPostProcessor 的子接口
- 在所有 Bean 定義(BeanDefinition)信息將要被加載,Bean 實例還未創(chuàng)建的時候加載
- 優(yōu)先于 BeanFactoryPostProcessor 執(zhí)行,利用 BeanDefinitionRegistryPostProcessor 可以給 Spring 容器中自定義添加 Bean
- 方法入參是 BeanDefinitionRegistry,意思是你可以調整 BeanDefinitionRegistry 的配置
還有一個類似的 BeanPostProcessor 它的作用:
- 在 Bean 實例化之后執(zhí)行的
- 執(zhí)行順序在 BeanFactoryPostProcessor 之后
- 方法入參是 Object bean,意思是你可以調整 bean 的配置
搞明白了以上的內容,下面我們可以直接動手寫代碼了。
最終答案
第一步:通過 spring.factories 擴展來注冊一個 ApplicationContextInitializer:
# 注冊 ApplicationContextInitializer org.springframework.context.ApplicationContextInitializer=com.antbank.demo.bootstrap.MyApplicationContextInitializer
注冊 ApplicationContextInitializer
的目的其實是為了接下來注冊 BeanDefinitionRegistryPostProcessor
到 Spring 中,我沒有找到直接使用 spring.factories
來注冊 BeanDefinitionRegistryPostProcessor
的方式,猜測是不支持的:
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { // 注意,如果你同時還使用了 spring cloud,這里需要做個判斷,要不要在 spring cloud applicationContext 中做這個事 // 通常 spring cloud 中的 bean 都和業(yè)務沒關系,是需要跳過的 applicationContext.addBeanFactoryPostProcessor(new MyBeanDefinitionRegistryPostProcessor()); } }
除了使用 spring 提供的 SPI 來注冊 ApplicationContextInitializer
,你也可以用 SpringApplication.addInitializers
的方式直接在 main 方法中直接注冊一個 ApplicationContextInitializer
結果都是可以的:
@SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication application = new SpringApplication(SpringBootDemoApplication.class); // 通過 SpringApplication 注冊 ApplicationContextInitializer application.addInitializers(new MyApplicationContextInitializer()); application.run(args); } }
當然了,通過 Spring 的事件機制也可以做到注冊 BeanDefinitionRegistryPostProcessor,選擇實現合適的 ApplicationListener 事件,可以通過 ApplicationContextEvent 獲得 ApplicationContext,即可注冊 BeanDefinitionRegistryPostProcessor,這里就不多展開了。
這里需要注意一點,為什么需要用 ApplicationContextInitializer 來注冊 BeanDefinitionRegistryPostProcessor,能不能用 @Component 或者其他的注解的方式注冊?
答案是不能的。@Component 注解的方式注冊能注冊上的前提是能被 ConfigurationClassPostProcessor 掃描到,也就是說用 @Component 注解的方式來注冊,注冊出來的 Bean 一定不可能排在 ConfigurationClassPostProcessor 前面,而我們的目的就是在所有的 Bean 掃描前注冊你需要的 Bean,這樣才能排在其他所有 Bean 前面,所以這里的場景下是不能用注解注冊的,這點需要額外注意。
第二步:實現 BeanDefinitionRegistryPostProcessor,注冊目標 bean:
用 MyBeanDefinitionRegistryPostProcessor 在 ConfigurationClassPostProcessor 掃描前注冊你需要的目標 bean 的 BeanDefinition 即可。
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 手動注冊一個 BeanDefinition registry.registerBeanDefinition("systemConfigService", new RootBeanDefinition(SystemConfigService.class)); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
當然你也可以使用一個類同時實現 ApplicationContextInitializer 和BeanDefinitionRegistryPostProcessor
通過 applicationContext#addBeanFactoryPostProcessor 注冊的 BeanDefinitionRegistryPostProcessor,比 Spring 自帶的優(yōu)先級要高,所以這里就不需要再實現 Ordered 接口提升優(yōu)先級就可以排在 ConfigurationClassPostProcessor 前面:
經過測試發(fā)現,上面的方式可行的,SystemConfigService 被排在第五個 Bean 進行實例化,排在前面的四個都是 Spring 自己內部的 Bean 了,也沒有必要再提前了。
本文提供的方式并不是唯一的,如果你有更好的方法,歡迎在評論區(qū)留言交流。
到此這篇關于Spring Boot 如何讓你的 bean 在其他 bean 之前完成加載 的文章就介紹到這了,更多相關Spring Boot bean 加載 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot 動態(tài)配置郵箱發(fā)件人過程解析
這篇文章主要介紹了SpringBoot 動態(tài)配置郵箱發(fā)件人過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-08-08Eclipse下基于Java的OpenCV開發(fā)環(huán)境配置教程
這篇文章主要為大家詳細介紹了Eclipse下基于Java的OpenCV開發(fā)環(huán)境配置教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07