前言
有時候我們需要在應用啟動時執行一些代碼片段,這些片段可能是僅僅是為了記錄 log,也可能是在啟動時檢查與安裝證書 ,諸如上述業務要求我們可能會經常碰到
Spring Boot 提供了至少 5 種方式用於在應用啟動時執行代碼。我們應該如何選擇?本文將會逐步解釋與分析這幾種不同方式
CommandLineRunner
CommandLineRunner
是一個接口,通過實現它,我們可以在 Spring 應用成功啟動之后
執行一些代碼片段
@Slf4j
@Component
@Order(2)
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("MyCommandLineRunner order is 2");
if (args.length > 0){
for (int i = 0; i < args.length; i++) {
log.info("MyCommandLineRunner current parameter is: {}", args[i]);
}
}
}
}
當 Spring Boot 在應用上下文中找到 CommandLineRunner
bean,它將會在應用成功啟動之后調用 run()
方法,並傳遞用於啟動應用程序的命令行參數
通過如下 maven 命令生成 jar 包:
mvn clean package
通過終端命令啟動應用,並傳遞參數:
java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar --name=rgyb
查看運行結果:
到這里我們可以看出幾個問題:
- 命令行傳入的參數並沒有被解析,而只是顯示出我們傳入的字符串內容
--foo=bar
,--name=rgyb
,我們可以通過ApplicationRunner
解析,我們稍后看 - 在重寫的
run()
方法上有throws Exception
標記,Spring Boot 會將CommandLineRunner
作為應用啟動的一部分,如果運行run()
方法時拋出 Exception,應用將會終止啟動 - 我們在類上添加了
@Order(2)
注解,當有多個CommandLineRunner
時,將會按照@Order
注解中的數字從小到大排序 (數字當然也可以用復數)
⚠️不要使用
@Order
太多看到 order 這個 "黑科技" 我們會覺得它可以非常方便將啟動邏輯按照指定順序執行,但如果你這么寫,說明多個代碼片段是有相互依賴關系的,為了讓我們的代碼更好維護,我們應該減少這種依賴使用
小結
如果我們只是想簡單的獲取以空格分隔的命令行參數,那 MyCommandLineRunner
就足夠使用了
ApplicationRunner
上面提到,通過命令行啟動並傳遞參數,MyCommandLineRunner
不能解析參數,如果要解析參數,那我們就要用到 ApplicationRunner
參數了
@Component
@Slf4j
@Order(1)
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("MyApplicationRunner order is 1");
log.info("MyApplicationRunner Current parameter is {}:", args.getOptionValues("foo"));
}
}
重新打 jar 包,運行如下命令:
java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar,rgyb
運行結果如下:
到這里我們可以看出:
- 同
MyCommandLineRunner
相似,但ApplicationRunner
可以通過 run 方法的ApplicationArguments
對象解析出命令行參數,並且每個參數可以有多個值在里面,因為getOptionValues
方法返回 List數組 - 在重寫的
run()
方法上有throws Exception
標記,Spring Boot 會將CommandLineRunner
作為應用啟動的一部分,如果運行run()
方法時拋出 Exception,應用將會終止啟動 ApplicationRunner
也可以使用@Order
注解進行排序,從啟動結果來看,它與CommandLineRunner
共享 order 的順序,稍后我們通過源碼來驗證這個結論
小結
如果我們想獲取復雜的命令行參數時,我們可以使用 ApplicationRunner
ApplicationListener
如果我們不需要獲取命令行參數時,我們可以將啟動邏輯綁定到 Spring 的 ApplicationReadyEvent
上
@Slf4j
@Component
@Order(0)
public class MyApplicationListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
log.info("MyApplicationListener is started up");
}
}
運行程序查看結果:
到這我們可以看出:
ApplicationReadyEvent
當且僅當 在應用程序就緒之后才被觸發,甚至是說上面的 Listener 要在本文說的所有解決方案都執行了之后才會被觸發,最終結論請稍后看- 代碼中我用
Order(0)
來標記,顯然 ApplicationListener 也是可以用該注解進行排序的,按數字大小排序,應該是最先執行。但是,這個順序僅用於同類型的 ApplicationListener 之間的排序,與前面提到的ApplicationRunners
和CommandLineRunners
的排序並不共享
小結
如果我們不需要獲取命令行參數,我們可以通過 ApplicationListener<ApplicationReadyEvent>
創建一些全局的啟動邏輯,我們還可以通過它獲取 Spring Boot 支持的 configuration properties 環境變量參數
如果你看過我之前寫的 Spring Bean 生命周期三部曲:
那么你會對下面兩種方式非常熟悉了
@PostConstruct
創建啟動邏輯的另一種簡單解決方案是提供一種在 bean 創建期間由 Spring 調用的初始化方法。我們要做的就只是將 @PostConstruct
注解添加到方法中:
@Component
@Slf4j
@DependsOn("myApplicationListener")
public class MyPostConstructBean {
@PostConstruct
public void testPostConstruct(){
log.info("MyPostConstructBean");
}
}
查看運行結果:
從上面運行結果可以看出:
- Spring 創建完 bean之后 (在啟動之前),便會立即調用
@PostConstruct
注解標記的方法,因此我們無法使用@Order
注解對其進行自由排序,因為它可能依賴於@Autowired
插入到我們 bean 中的其他 Spring bean。 - 相反,它將在依賴於它的所有 bean 被初始化之后被調用,如果要添加人為的依賴關系並由此創建一個排序,則可以使用
@DependsOn
注解(雖然可以排序,但是不建議使用,理由和@Order
一樣)
小結
@PostConstruct
方法固有地綁定到現有的 Spring bean,因此應僅將其用於此單個 bean 的初始化邏輯;
InitializingBean
與 @PostConstruct
解決方案非常相似,我們可以實現 InitializingBean
接口,並讓 Spring 調用某個初始化方法:
@Component
@Slf4j
public class MyInitializingBean implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
log.info("MyInitializingBean.afterPropertiesSet()");
}
}
查看運行結果:
從上面的運行結果中,我們得到了和 @PostConstruct
一樣的效果,但二者還是有差別的
⚠️
@PostConstruct
和afterPropertiesSet
區別
- afterPropertiesSet,顧名思義「在屬性設置之后」,調用該方法時,該 bean 的所有屬性已經被 Spring 填充。如果我們在某些屬性上使用
@Autowired
(常規操作應該使用構造函數注入),那么 Spring 將在調用afterPropertiesSet
之前將 bean 注入這些屬性。但@PostConstruct
並沒有這些屬性填充限制- 所以
InitializingBean.afterPropertiesSet
解決方案比使用@PostConstruct
更安全,因為如果我們依賴尚未自動注入的@Autowired
字段,則@PostConstruct
方法可能會遇到 NullPointerExceptions
小結
如果我們使用構造函數注入,則這兩種解決方案都是等效的
源碼分析
請打開你的 IDE (重點代碼已標記注釋):
MyCommandLineRunner
和ApplicationRunner
是在何時被調用的呢?
打開 SpringApplication.java
類,里面有 callRunners
方法
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
//從上下文獲取 ApplicationRunner 類型的 bean
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
//從上下文獲取 CommandLineRunner 類型的 bean
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//對二者進行排序,這也就是為什么二者的 order 是可以共享的了
AnnotationAwareOrderComparator.sort(runners);
//遍歷對其進行調用
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
強烈建議完整看一下 SpringApplication.java
的全部代碼,Spring Boot 啟動過程及原理都可以從這個類中找到一些答案
總結
最后畫一張圖用來總結這幾種方式(高清大圖請查看原文:https://dayarch.top/p/spring-boot-execute-on-startup.html)
靈魂追問
- 上面程序運行結果,
afterPropertiesSet
方法調用先於@PostConstruct
方法,但這和我們在 Spring Bean 生命周期之緣起 中的調用順序恰恰相反,你知道為什么嗎? MyPostConstructBean
通過@DependsOn("myApplicationListener")
依賴了 MyApplicationListener,為什么調用結果前者先與后者呢?- 為什么不建議
@Autowired
形式依賴注入
在寫 Spring Bean 生命周期時就有朋友問我與之相關的問題,顯然他們在概念上有一些含混,所以,仔細理解上面的問題將會幫助你加深對 Spring Bean 生命周期的理解
歡迎持續關注公眾號:「日拱一兵」
- 前沿 Java 技術干貨分享
- 高效工具匯總 | 回復「工具」
- 面試問題分析與解答
- 技術資料領取 | 回復「資料」
以讀偵探小說思維輕松趣味學習 Java 技術棧相關知識,本着將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......