SpringBoot項目的業務工具類(如:參數工具類ParamUtils,僅包含static方法,依賴DAO訪問DB載入數據),在SpringBoot啟動過程中會被其他業務Bean初始化依賴。由於參數工具類和業務Bean均被Spring框架托管,如何在其他Bean初始化之前,就優雅安全的初始化Par... ...
本博客原文地址:https://ntopic.cn/p/2023090901/
源代碼先行:
- Gitee本文介紹的完整倉庫:https://gitee.com/obullxl/ntopic-boot
- GitHub本文介紹的完整倉庫:https://github.com/obullxl/ntopic-boot
背景介紹
今天走讀一個應用程式代碼,發現一個有趣的現象:有多個不同的業務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優先初始化設置
兩個關鍵點:
- ApplicationContextInitializer類:提供Context初始化入口,業務邏輯可以通過此次註入。
- 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);
}
有幾個非常核心的點,基本調用鏈路:
- SpringApplication類:run() -> prepareContext() -> applyInitializers(本方法:調用自定義
NTApplicationContextInitializer
上下文器) - SpringApplication類:run() -> refreshContext() -> refresh(ConfigurableApplicationContext)
- ConfigurableApplicationContext類:AbstractApplicationContext.refresh() ->
finishBeanFactoryInitialization(ConfigurableListableBeanFactory)
- ConfigurableListableBeanFactory類,
關鍵代碼
都在這裡:preInstantiateSingletons()
- beanDefinitionNames屬性:Spring收集到的所有Bean定義,包括Repository註解、Component註解和我們手工定義的Bean
- 遍歷beanDefinitionNames的時候,優先RootBeanDefinition初始化,手工定義的ParamUtils也是該類型
至此,問題解決,能解決的原因也搞清楚了!
本文作者:奔跑的蝸牛,轉載請註明原文鏈接:https://ntopic.cn