SpringBoot如何讓業務Bean優先於其他Bean載入

来源:https://www.cnblogs.com/obullxl/archive/2023/09/09/NTopic2023090901.html
-Advertisement-
Play Games

SpringBoot項目的業務工具類(如:參數工具類ParamUtils,僅包含static方法,依賴DAO訪問DB載入數據),在SpringBoot啟動過程中會被其他業務Bean初始化依賴。由於參數工具類和業務Bean均被Spring框架托管,如何在其他Bean初始化之前,就優雅安全的初始化Par... ...


本博客原文地址:https://ntopic.cn/p/2023090901/

源代碼先行:

背景介紹

今天走讀一個應用程式代碼,發現一個有趣的現象:有多個不同的業務Bean中均依賴了一個參數工具類ParamUtils(即:@Autowired ParamUtils paramUtis),ParamUtils依賴了ParamDAO Bean用於從DB中獲取參數;為了便於ParamUtils使用,工具類全部都是static靜態方法,也就是說,業務Bean僅僅增加Autowired依賴,在實際調用時還是直接使用的ParamUtils類靜態方法。那個Autowired註入ParamUtils的依賴看起來是無用代碼,但是其實還不能去掉。

代碼業務這麼寫的目的其實很好理解:因為ParamUtils依賴了DAO Bean,增加依賴是保障ParamUtils的類靜態方法在調用時已經被SpringBoot初始化了。那麼,有沒有更優雅的辦法,能讓業務代碼更優雅更安全的使用ParamUtils工具類呢?

思路分析

ParamUtils業務Bean,比其他的業務Bean提前初始化,基本思路如下:

第一思路:採用優先順序Ordered註解(類:org.springframework.core.Ordered),但是不可行,因為該註解主要是用於控制Spring自身Bean的初始化順序,如Listener/Filter等。

第二思路:採用Bean依賴DependsOn註解(類:org.springframework.context.annotation.DependsOn),該方法可行,它和Autowired註解一致,也是表明Bean之間依賴,但是沒有從本質上解決問題。

第三思路:手工註冊Bean讓Spring優先初始化,查看SpringApplication類代碼,發現裡面有個addInitializers(ApplicationContextInitializer<?>... initializers)方法,可以讓業務在ApplicationContext初始化時initialize(C applicationContext)基於Context做一些事情。那麼可不可以在這個地方,能手工註冊業務Bean呢?

代碼實現和驗證

代碼分為3部分:ParamDAO業務Bean訪問DB,ParamUtils參數工具類依賴ParamDAO,RestController測試類使用參數工具類。

為了閱讀方便,以下展示的代碼均只有主體部分,完整的代碼註釋和代碼內容,請下載本工程倉庫。

ParamDAO業務Bean

為了測試簡便,本工程不依賴MySQL資料庫,我們還是採用SQLite,源文件就在代碼根目錄下,clone本倉庫後即可執行運行:

SQLite數據表準備

首先新建一張參數表(nt_param),並且插入一些數據。為了儘快驗證我們的思路,其他的數據新增、修改和刪除等就不做特別的驗證了。

--
-- 參數表
--
CREATE TABLE nt_param
(
    id          bigint unsigned NOT NULL auto_increment,
    category    varchar(64) NOT NULL,
    module      varchar(64) NOT NULL,
    name        varchar(64) NOT NULL,
    content     varchar(4096) DEFAULT '',
    create_time timestamp,
    modify_time timestamp,
    PRIMARY KEY (id),
    UNIQUE (category, module, name)
);

--
-- 插入數據
--
INSERT INTO nt_param (category, module, name, content, create_time, modify_time)
VALUES ('CONFIG', 'USER', 'minAge', '18', strftime('%Y-%m-%d %H:%M:%f', 'now'), strftime('%Y-%m-%d %H:%M:%f', 'now')),
       ('CONFIG', 'USER', 'maxAge', '60', strftime('%Y-%m-%d %H:%M:%f', 'now'), strftime('%Y-%m-%d %H:%M:%f', 'now'));

ParamDAO數據查詢

NTParamDAO為普通的Spring Bean(ID為:ntParamDAO

@Repository("ntParamDAO")
public interface NTParamDAO {

    @Select("SELECT * FROM nt_param WHERE category=#{category,jdbcType=VARCHAR} AND module=#{module,jdbcType=VARCHAR}")
    List<NTParamDO> selectByModule(@Param("category") String category, @Param("module") String module);

}

ParamUtils工具類定義和使用

ParamUtils工具類定義:非Spring Bean

ParamUtils是靜態工具類,依賴了ParamDAO Spring Bean,並且ParamUtils並不是Spring Bean:

// @Component("ntParamUtils") SpringBoot優先初始化本類,因此無需增加註解
public class NTParamUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogConstants.DAS);

    /**
     * 系統參數DAO
     */
    private static NTParamDAO NT_PARAM_DAO;

    /**
     * 依賴註入
     */
    public NTParamUtils(@Qualifier("ntParamDAO") NTParamDAO ntParamDAO) {
        Assert.notNull(ntParamDAO, "NTParamDAO註入為NULL.");
        NT_PARAM_DAO = ntParamDAO;

        // 列印日誌
        LOGGER.info("{}:初始化完成.", this.getClass().getName());
    }

    public static List<NTParamDO> findList(String category, String module) {
        Assert.hasText(category, "分類參數為空");
        Assert.hasText(module, "模塊參數為空");
        return NT_PARAM_DAO.selectByModule(category, module);
    }

}

ParamUtils工具類使用:普通Spring Bean

NTUserServiceImpl是一個普通的Spring Bean,它沒有顯示依賴ParamUtils,而是直接使用它:

@Component("ntUserService")
public final class NTUserServiceImpl implements NTUserService {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogConstants.BIZ);

    @Autowired
    public NTUserServiceImpl() {
        // 列印日誌
        LOGGER.info("{}:初始化完成.", this.getClass().getName());
    }

    /**
     * 獲取用戶模塊參數
     */
    @Override
    public List<NTParamDO> findUserParamList() {
        return NTParamUtils.findList("CONFIG", "USER");
    }
}

SpringBoot優先初始化設置

兩個關鍵點:

  1. ApplicationContextInitializer類:提供Context初始化入口,業務邏輯可以通過此次註入。
  2. BeanDefinitionRegistryPostProcessor類:Spring Bean收集完成後,但還沒有初始化之前入口,我們的關鍵就在這裡定義ParamUtils Bean,並且Bean定義為RootBeanDefinition保障提前初始化。

Context自定義初始化:手工註冊ParamUtils Bean

public class NTApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, BeanDefinitionRegistryPostProcessor {
    
    /**
     * Context初始化,給業務邏輯初始化提供了機會
     */
    @Override
    public void initialize(ConfigurableApplicationContext context) {
        // 註冊Bean上下文初始化後處理器,用於手工註冊Bean
        context.addBeanFactoryPostProcessor(this);
    }

    /**
     * 手工註冊ParamUtils工具類,並且是RootBean定義,保障優先初始化,下麵會詳細分析
     */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 在ConfigurationClassPostProcessor前手動註冊Bean,保障優先於其他Bean初始化
        registry.registerBeanDefinition("ntParamUtils", new RootBeanDefinition(NTParamUtils.class));
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }
}

SpringBoot啟動類增加自定義初始化器

原來的方法:SpringApplication.run(NTBootApplication.class, args);

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
@MapperScan(basePackages = "cn.ntopic.das..**.dao", sqlSessionFactoryRef = "ntSqlSessionFactory")
public class NTBootApplication {

    /**
     * SpringBoot啟動
     */
    public static void main(String[] args) {
        // 註冊自定義處理器
        SpringApplication application = new SpringApplication(NTBootApplication.class);
        application.addInitializers(new NTApplicationContextInitializer());

        // SpringBoot啟動
        application.run(args);
    }
}

至此,業務Bean提前初始化的整個代碼完畢,下麵進行驗證!

ParamUtils初始化驗證(符合預期)

我們分表從SpringBoot的啟動日誌實際使用2個方面來驗證我們的設計思路:

SpringBoot啟動日誌:符合預期

第21行第22行日誌,可以看到,ParamUtils優於其他Bean完成初始化:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.3)

2023-09-09 11:40:55,607  INFO (StartupInfoLogger.java:55)- Starting NTBootApplication using Java 1.8.0_281 on OXL-MacBook.local with PID 1371 (/Users/obullxl/CodeSpace/ntopic-boot/ntopic/target/classes started by obullxl in /Users/obullxl/CodeSpace/ntopic-boot)
2023-09-09 11:40:55,612  INFO (SpringApplication.java:659)- No active profile set, falling back to default profiles: default
2023-09-09 11:40:55,692  INFO (DeferredLog.java:255)- Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-09 11:40:55,693  INFO (DeferredLog.java:255)- For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-09-09 11:40:56,834  INFO (TomcatWebServer.java:108)- Tomcat initialized with port(s): 8088 (http)
2023-09-09 11:40:56,842  INFO (DirectJDKLog.java:173)- Initializing ProtocolHandler ["http-nio-8088"]
2023-09-09 11:40:56,842  INFO (DirectJDKLog.java:173)- Starting service [Tomcat]
2023-09-09 11:40:56,842  INFO (DirectJDKLog.java:173)- Starting Servlet engine: [Apache Tomcat/9.0.50]
2023-09-09 11:40:56,901  INFO (DirectJDKLog.java:173)- Initializing Spring embedded WebApplicationContext
2023-09-09 11:40:56,901  INFO (ServletWebServerApplicationContext.java:290)- Root WebApplicationContext: initialization completed in 1208 ms
2023-09-09 11:40:57,043 ERROR (Log4j2Impl.java:58)- testWhileIdle is true, validationQuery not set
2023-09-09 11:40:57,051  INFO (Log4j2Impl.java:106)- {dataSource-1} inited
2023-09-09 11:40:57,127  INFO (NTParamUtils.java:39)- cn.ntopic.NTParamUtils:初始化完成.
2023-09-09 11:40:57,160  INFO (NTUserServiceImpl.java:78)- cn.ntopic.service.impl.NTUserServiceImpl:初始化完成.
2023-09-09 11:40:57,170  INFO (NTExecutorConfig.java:65)- start ntThreadPool
2023-09-09 11:40:57,563  INFO (OptionalLiveReloadServer.java:58)- LiveReload server is running on port 35729
2023-09-09 11:40:57,582  INFO (DirectJDKLog.java:173)- Starting ProtocolHandler ["http-nio-8088"]
2023-09-09 11:40:57,600  INFO (TomcatWebServer.java:220)- Tomcat started on port(s): 8088 (http) with context path ''
2023-09-09 11:40:57,610  INFO (StartupInfoLogger.java:61)- Started NTBootApplication in 2.363 seconds (JVM running for 3.091)

RestController驗證:符合預期

@RestController
public class NTParamAct {

    private final NTUserService ntUserService;

    public NTParamAct(@Qualifier("ntUserService") NTUserService ntUserService) {
        this.ntUserService = ntUserService;
    }

    @RequestMapping("/param")
    public List<NTParamDO> paramList() {
        return this.ntUserService.findUserParamList();
    }

}

打開瀏覽器,訪問:http://localhost:8088/param

可以看到,參數數據被查詢並輸出:

[
    {
        "id": 3,
        "category": "CONFIG",
        "module": "USER",
        "name": "maxAge",
        "content": "60",
        "createTime": "2023-09-08T18:30:20.818+00:00",
        "modifyTime": "2023-09-08T18:30:20.818+00:00"
    },
    {
        "id": 2,
        "category": "CONFIG",
        "module": "USER",
        "name": "minAge",
        "content": "18",
        "createTime": "2023-09-08T18:30:20.818+00:00",
        "modifyTime": "2023-09-08T18:30:20.818+00:00"
    }
]

SpringBoot實現分析

SpringBoot啟動的代碼入口:

public static void main(String[] args) {
    // 註冊自定義處理器
    SpringApplication application = new SpringApplication(NTBootApplication.class);
    application.addInitializers(new NTApplicationContextInitializer());

    // SpringBoot啟動
    application.run(args);
}

有幾個非常核心的點,基本調用鏈路:

  1. SpringApplication類:run() -> prepareContext() -> applyInitializers(本方法:調用自定義NTApplicationContextInitializer上下文器)
  2. SpringApplication類:run() -> refreshContext() -> refresh(ConfigurableApplicationContext)
  3. ConfigurableApplicationContext類:AbstractApplicationContext.refresh() -> finishBeanFactoryInitialization(ConfigurableListableBeanFactory)
  4. ConfigurableListableBeanFactory類,關鍵代碼都在這裡:preInstantiateSingletons()
  • beanDefinitionNames屬性:Spring收集到的所有Bean定義,包括Repository註解、Component註解和我們手工定義的Bean
  • 遍歷beanDefinitionNames的時候,優先RootBeanDefinition初始化,手工定義的ParamUtils也是該類型

至此,問題解決,能解決的原因也搞清楚了!

本文作者:奔跑的蝸牛,轉載請註明原文鏈接:https://ntopic.cn


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

-Advertisement-
Play Games
更多相關文章
  • >我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:琉易 [liuxianyu.cn](https://link.juejin.cn/?target=ht ...
  • 以下是一個Python實現的簡單二分查找演算法的代碼示例: def binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 # 找到中間元素的索引 ...
  • 一條爬蟲抓取一個小網站所有數據 ​ 今天閑來無事,寫一個爬蟲來玩玩。在網上衝浪的時候發現了一個搞笑的段子網,發現裡面的內容還是比較有意思的,於是心血來潮,就想著能不能寫一個Python程式,抓取幾條數據下來看看,一不小心就把這個網站的所有數據都拿到了。 ​ 這個網站主要的數據都是詳情在HTML裡面的 ...
  • 上篇文章12分鐘從Executor自頂向下徹底搞懂線程池中我們聊到線程池,而線程池中包含阻塞隊列 這篇文章我們主要聊聊併發包下的阻塞隊列 阻塞隊列 什麼是隊列? 隊列的實現可以是數組、也可以是鏈表,可以實現先進先出的順序隊列,也可以實現先進後出的棧隊列 那什麼是阻塞隊列? 在經典的生產者/消費者模型 ...
  • 本篇文章深入探討了 Go 語言中類型確定值、類型不確定值以及對應類型轉換的知識點,後續充分解析了常量與變數及其高級用法,並舉出豐富的案例。 關註公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩, ...
  • 0 前言 註冊中心不應僅提供服務註冊和發現功能,還應保證對服務可用性監測,對不健康的服務和過期的進行標識或剔除,維護實例的生命周期,以保證客戶端儘可能的查詢到可用的服務列表。 因此本文介紹Nacos註冊中心的健康檢查機制。 1 註冊中心的健康檢查機制 知道⼀個服務是否還健康的方式: 客戶端主動上報, ...
  • 列表模型(Item Model),老周沒有翻譯為“項目模型”,因為 Project 和 Item 都可以翻譯為“項目”,容易出現歧義。乾脆叫列表模型。這個模型也確實是為數據列表準備的,它以 MVC 的概念為基礎,在原始數據和用戶界面視圖之間搭建橋梁,使兩者可以傳遞數據(提取、修改)。 Qt 裡面使用 ...
  • 什麼是線性表 線性表的插入元素 線性表的刪除元素 線性表順序存儲的缺點 線性表的特點 1.線性表的實例 首先我們創建3個文件,分別如下: liner_data --sqlist.c --sqlist.h --test.c sqlist.h // .h文件中定位數據的結構以及函數的方法 typedef ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...