有時候咱們須要在應用啓動時執行一些代碼片斷,這些片斷多是僅僅是爲了記錄 log,也多是在啓動時檢查與安裝證書 ,諸如上述業務要求咱們可能會常常碰到html
Spring Boot 提供了至少 5 種方式用於在應用啓動時執行代碼。咱們應該如何選擇?本文將會逐步解釋與分析這幾種不一樣方式java
CommandLineRunner
是一個接口,經過實現它,咱們能夠在 Spring 應用成功啓動以後
執行一些代碼片斷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()
方法,並傳遞用於啓動應用程序的命令行參數segmentfault
經過以下 maven 命令生成 jar 包:數組
mvn clean package
經過終端命令啓動應用,並傳遞參數:安全
java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar --name=rgyb
查看運行結果:
springboot
到這裏咱們能夠看出幾個問題:bash
--foo=bar
,--name=rgyb
,咱們能夠經過 ApplicationRunner
解析,咱們稍後看run()
方法上有 throws Exception
標記,Spring Boot 會將 CommandLineRunner
做爲應用啓動的一部分,若是運行 run()
方法時拋出 Exception,應用將會終止啓動@Order(2)
註解,當有多個 CommandLineRunner
時,將會按照 @Order
註解中的數字從小到大排序 (數字固然也能夠用複數)⚠️不要使用
@Order
太多看到 order 這個 "黑科技" 咱們會以爲它能夠很是方便將啓動邏輯按照指定順序執行,但若是你這麼寫,說明多個代碼片斷是有相互依賴關係的,爲了讓咱們的代碼更好維護,咱們應該減小這種依賴使用app
若是咱們只是想簡單的獲取以空格分隔的命令行參數,那 MyCommandLineRunner
就足夠使用了maven
上面提到,經過命令行啓動並傳遞參數,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<String> 數組run()
方法上有 throws Exception
標記,Spring Boot 會將 CommandLineRunner
做爲應用啓動的一部分,若是運行 run()
方法時拋出 Exception,應用將會終止啓動ApplicationRunner
也可使用 @Order
註解進行排序,從啓動結果來看,它與 CommandLineRunner
共享 order 的順序,稍後咱們經過源碼來驗證這個結論若是咱們想獲取複雜的命令行參數時,咱們可使用 ApplicationRunner
若是咱們不須要獲取命令行參數時,咱們能夠將啓動邏輯綁定到 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 生命週期三部曲:
那麼你會對下面兩種方式很是熟悉了
建立啓動邏輯的另外一種簡單解決方案是提供一種在 bean 建立期間由 Spring 調用的初始化方法。咱們要作的就只是將 @PostConstruct
註解添加到方法中:
@Component @Slf4j @DependsOn("myApplicationListener") public class MyPostConstructBean { @PostConstruct public void testPostConstruct(){ log.info("MyPostConstructBean"); } }
查看運行結果:
從上面運行結果能夠看出:
@PostConstruct
註解標記的方法,所以咱們沒法使用 @Order
註解對其進行自由排序,由於它可能依賴於 @Autowired
插入到咱們 bean 中的其餘 Spring bean。@DependsOn
註解(雖然能夠排序,可是不建議使用,理由和 @Order
同樣)@PostConstruct
方法固有地綁定到現有的 Spring bean,所以應僅將其用於此單個 bean 的初始化邏輯;
與 @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-...)
afterPropertiesSet
方法調用先於 @PostConstruct
方法,但這和咱們在 Spring Bean 生命週期之緣起 中的調用順序偏偏相反,你知道爲何嗎?MyPostConstructBean
經過 @DependsOn("myApplicationListener")
依賴了 MyApplicationListener,爲何調用結果前者先與後者呢?@Autowired
形式依賴注入在寫 Spring Bean 生命週期時就有朋友問我與之相關的問題,顯然他們在概念上有一些含混,因此,仔細理解上面的問題將會幫助你加深對 Spring Bean 生命週期的理解
歡迎關注個人公衆號 「日拱一兵」,趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地
若是對個人專題內容感興趣,或搶先看更多內容,歡迎訪問個人博客 dayarch.top