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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...