Spring Boot |如何讓你的 bean 在其他 bean 之前完成載入

来源:https://www.cnblogs.com/88223100/archive/2023/05/20/Spring-Boot-How-to-make-your-bean-complete-loading-before-other-beans.html
-Advertisement-
Play Games

本文圍繞 Spring Boot 中如何讓你的 bean 在其他 bean 之前完成載入展開討論。 問題 今天有個小伙伴給我出了一個難題:在 SpringBoot 中如何讓自己的某個指定的 Bean 在其他 Bean 前完成被 Spring 載入?我聽到這個問題的第一反應是,為什麼會有這樣奇怪的需求 ...


本文圍繞 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:
# 註冊 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 都和業務沒關係,是需要跳過的
        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 了,也沒有必要再提前了。
本文提供的方式並不是唯一的,如果你有更好的方法,歡迎在評論區留言交流。
作者|劉順(蕭易)

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Spring-Boot-How-to-make-your-bean-complete-loading-before-other-beans.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、導出靜態數據 1、安裝 vue-json-excel npm i vue-json-excel 註意,此插件對node有版本要求,安裝失敗檢查一下報錯是否由於node版本造成! 2、引入並註冊組件(以全局為例) import Vue ...
  • **本文為千鋒資深前端教學老師帶來的【JavaScript全解析】系列,文章內含豐富的代碼案例及配圖,從0到1講解JavaScript相關知識點,致力於教會每一個人學會JS!** **文末有本文重點總結,可以收藏慢慢看\~ 更多技術類內容,主頁關註一波!** # ES6函數中參數的預設值 給函數的形 ...
  • 一.業務描述 最近在負責公司一個語音的微服務模塊優化,這個模塊主要的業務是:1.天貓精靈、小度、若琪、小京魚、小愛同學、思必馳這些第三方音響對我們的用戶進行oauth2/JWT授權; 2.這些第三方音響服務調用我們的設備發現介面對公司的設備信息在第三方平臺進行一個存儲;3.第三方平臺對用戶發出的語音 ...
  • 關於JWT,可以說是分散式系統下的一個利器,我在我的很多項目實踐中,認證系統的第一選擇都是JWT。它的優勢會讓你欲罷不能,就像你領優惠券一樣。 ...
  • ## 創建阻塞的 EchoClient 客戶程式一般不需要同時建立與伺服器的多個連接,因此用一個線程,按照阻塞模式運行就能滿足需求 ```java public class EchoClient { private SocketChannel socketChannel = null; public ...
  • ## 1. 安裝Django 在命令行中輸入以下命令安裝Django ```shell pip install django ``` ## 2. 創建Django項目 在命令行中輸入以下命令創建一個名為myblog的Django項目 ```shell django-admin startprojec ...
  • ## 文章首發 [【重學C++】01| C++ 如何進行記憶體資源管理?](https://mp.weixin.qq.com/s/ZhRhN07wjypnkWXcu_Lz3g) ## 前言 大家好,我是只講技術乾貨的會玩code,今天是【重學C++】的第一講,我們來學習下C++的記憶體管理。 與java ...
  • 前言: 最近生產環境系統發現一個疑難雜症,看了很久的問題但是始終無法定位到問題並處理,然後查閱了相關資料也是定位不到問題,不過資料查閱卻給了個新的思路,以此為跳板最終解決了問題。 一、問題描述 功能介紹: “主計劃拆分子計劃”是APS系統很常見的功能,功能大概意思是用戶可選多個主計劃一次性進行“展開 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...