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 創建的時候,通過 @PostConstruct 註解的 init() 方法完成了數據加載到緩存中,最關鍵的是,由於是系統屬性,所以需要在很多地方都想使用,尤其需要在很多 bean 啓動的時候使用爲了方便就提供了 static 方法來方便調用,這樣其他的 bean 不需要依賴注入就可以直接調用,但問題是系統屬性是存在 db 裏面的,這就導致了不能把 SystemConfigService做成一個純「工具類」,它必須要被 Spring 託管起來,完成 mapper 的注入才能正常工作。因此這樣一來就比較麻煩,其他的類或者 Bean 如果想安全的使用 SystemConfigService#getSystemConfig 中的獲取配置的靜態方法,就必須等 SystemConfigService 先被 Spring 創建加載起來,完成 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 集成開發環境的時候會給你一個警告。按照 Spring 的文檔,我們應該直接去掉 getSystemConfig 的 static 修飾,讓 getSystemConfig 變成一個實例方法,讓每個需要依賴的 SystemConfigService 的 Bean 通過構造函數完成依賴注入,這樣 Spring 會保證每個 Bean 在創建之前會先把它所有的依賴創建並初始化完成。
看來我們還是要想一些其他的方法來達成我們的目的。

嘗試解決問題的一些方法

@Order 註解或者實現 org.springframework.core.Ordered

最先想到的就是 Spring 提供的 Order 相關的註解和接口,實際上測試下來不可行。Order 相關的方法一般用來控制 Spring 自身組件相關 Bean 的順序,比如 ApplicationListener,RegistrationBean 等,對於我們自己使用 @Service @Compont 註解註冊的業務相關的 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 創建的相關知識

 

首先要明白一點,Bean 創建的順序是怎麼來的,如果你對 Spring 的源碼比較熟悉,你會知道在 AbstractApplicationContext 裏面有個 refresh 方法, Bean 創建的大部分邏輯都在 refresh 方法裏面,在 refresh 末尾的 finishBeanFactoryInitialization(beanFactory) 方法調用中,會調用 beanFactory.preInstantiateSingletons(),在這裏對所有的 beanDefinitionNames 一一遍歷,進行 bean 實例化和組裝:

 這個 beanDefinitionNames 列表的順序就決定了 Bean 的創建順序,那麼這個 beanDefinitionNames 列表又是怎麼來的?答案是 ConfigurationClassPostProcessor 通過掃描你的代碼和註解生成的,將 Bean 掃描解析成 Bean 定義(BeanDefinition),同時將 Bean 定義(BeanDefinition)註冊到 BeanDefinitionRegistry 中,纔有了 beanDefinitionNames 列表。

 

ConfigurationClassPostProcessor 的介紹

 

這裏提到了 ConfigurationClassPostProcessor,實現了 BeanDefinitionRegistryPostProcessor 接口。它是一個非常非常重要的類,甚至可以說它是 Spring boot 提供的掃描你的註解並解析成 BeanDefinition 最重要的組件。我們在使用 SpringBoot 過程中用到的 @Configuration、@ComponentScan、@Import、@Bean 這些註解的功能都是通過 ConfigurationClassPostProcessor 註解實現的,這裏找了一篇文件介紹,就不多說了。https://juejin.cn/post/6844903944146124808

 

BeanDefinitionRegistryPostProcessor 相關接口的介紹

 

接下來還要介紹 Spring 中提供的一些擴展,它們在 Bean 的創建過程中起到非常重要的作用。BeanFactoryPostProcessor 它的作用:

  • 在 BeanFactory 初始化之後調用,來定製和修改 BeanFactory 的內容

  • 所有的 Bean 定義(BeanDefinition)已經保存加載到 beanFactory,但是 Bean 的實例還未創建

  • 方法的入參是 ConfigurrableListableBeanFactory,意思是你可以調整 ConfigurrableListableBeanFactory 的配置

BeanDefinitionRegistryPostProcessor 它的作用:

  • 是 BeanFactoryPostProcessor 的子接口

  • 在所有 Bean 定義(BeanDefinition)信息將要被加載,Bean 實例還未創建的時候加載

  • 優先於 BeanFactoryPostProcessor 執行,利用 BeanDefinitionRegistryPostProcessor 可以給 Spring 容器中自定義添加 Bean 

  • 方法入參是 BeanDefinitionRegistry,意思是你可以調整 BeanDefinitionRegistry 的配置

還有一個類似的 BeanPostProcessor 它的作用:

  • 在 Bean 實例化之後執行的

  • 執行順序在 BeanFactoryPostProcessor 之後

  • 方法入參是 Object bean,意思是你可以調整 bean 的配置

搞明白了以上的內容,下面我們可以直接動手寫代碼了。

最終答案

 

第一步:通過 spring.factories 擴展來註冊一個 ApplicationContextInitializer:# 註冊 ApplicationContextInitializerorg.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 都和業務沒關係,是需要跳過的
        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 自帶的優先級要高,所以這裏就不需要再實現 Ordered 接口提升優先級就可以排在 ConfigurationClassPostProcessor 前面:

 

經過測試發現,上面的方式可行的,SystemConfigService 被排在第五個 Bean 進行實例化,排在前面的四個都是 Spring 自己內部的 Bean 了,也沒有必要再提前了。

 原文鏈接

其他辦法

1.在啓動類上添加@DependsOn(&#39;systemConfigService’) 就可以了
2.繼承 BeanPostProcessor 等,使得它可以提前加載
3.config 配置可以直接注入到 MutablePropertySources 上下文裏
4.EnvironmentPostProcessor,PropertySourceLoader 配合延遲加載,應該也能實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章