1. 程式人生 > >一張圖幫你記憶,Spring Boot 應用在啟動階段執行程式碼的幾種方式

一張圖幫你記憶,Spring Boot 應用在啟動階段執行程式碼的幾種方式

前言

有時候我們需要在應用啟動時執行一些程式碼片段,這些片段可能是僅僅是為了記錄 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

檢視執行結果:

到這裡我們可以看出幾個問題:

  1. 命令列傳入的引數並沒有被解析,而只是顯示出我們傳入的字串內容 --foo=bar--name=rgyb
    ,我們可以通過 ApplicationRunner 解析,我們稍後看
  2. 在重寫的 run() 方法上有 throws Exception 標記,Spring Boot 會將 CommandLineRunner 作為應用啟動的一部分,如果執行 run() 方法時丟擲 Exception,應用將會終止啟動
  3. 我們在類上添加了 @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

執行結果如下:

到這裡我們可以看出:

  1. MyCommandLineRunner 相似,但 ApplicationRunner 可以通過 run 方法的 ApplicationArguments 物件解析出命令列引數,並且每個引數可以有多個值在裡面,因為 getOptionValues 方法返回 List
  2. 在重寫的 run() 方法上有 throws Exception 標記,Spring Boot 會將 CommandLineRunner 作為應用啟動的一部分,如果執行 run() 方法時丟擲 Exception,應用將會終止啟動
  3. 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");
    }
}

執行程式檢視結果:

到這我們可以看出:

  1. ApplicationReadyEvent 當且僅當 在應用程式就緒之後才被觸發,甚至是說上面的 Listener 要在本文說的所有解決方案都執行了之後才會被觸發,最終結論請稍後看
  2. 程式碼中我用 Order(0) 來標記,顯然 ApplicationListener 也是可以用該註解進行排序的,按數字大小排序,應該是最先執行。但是,這個順序僅用於同類型的 ApplicationListener 之間的排序,與前面提到的 ApplicationRunnersCommandLineRunners 的排序並不共享

小結

如果我們不需要獲取命令列引數,我們可以通過 ApplicationListener<ApplicationReadyEvent> 建立一些全域性的啟動邏輯,我們還可以通過它獲取 Spring Boot 支援的 configuration properties 環境變數引數


如果你看過我之前寫的 Spring Bean 生命週期三部曲:

  • Spring Bean 生命週期之緣起
  • Spring Bean 生命週期之緣盡
  • Spring Aware 到底是什麼?

那麼你會對下面兩種方式非常熟悉了

@PostConstruct

建立啟動邏輯的另一種簡單解決方案是提供一種在 bean 建立期間由 Spring 呼叫的初始化方法。我們要做的就只是將 @PostConstruct 註解新增到方法中:

@Component
@Slf4j
@DependsOn("myApplicationListener")
public class MyPostConstructBean {

    @PostConstruct
    public void testPostConstruct(){
        log.info("MyPostConstructBean");
    }
}

檢視執行結果:

從上面執行結果可以看出:

  1. Spring 建立完 bean之後 (在啟動之前),便會立即呼叫 @PostConstruct 註解標記的方法,因此我們無法使用 @Order 註解對其進行自由排序,因為它可能依賴於 @Autowired 插入到我們 bean 中的其他 Spring bean。
  2. 相反,它將在依賴於它的所有 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 一樣的效果,但二者還是有差別的

⚠️ @PostConstructafterPropertiesSet 區別

  1. afterPropertiesSet,顧名思義「在屬性設定之後」,呼叫該方法時,該 bean 的所有屬性已經被 Spring 填充。如果我們在某些屬性上使用 @Autowired(常規操作應該使用建構函式注入),那麼 Spring 將在呼叫afterPropertiesSet 之前將 bean 注入這些屬性。但 @PostConstruct 並沒有這些屬性填充限制
  2. 所以 InitializingBean.afterPropertiesSet 解決方案比使用 @PostConstruct 更安全,因為如果我們依賴尚未自動注入的 @Autowired 欄位,則 @PostConstruct 方法可能會遇到 NullPointerExceptions

小結

如果我們使用建構函式注入,則這兩種解決方案都是等效的


原始碼分析

請開啟你的 IDE (重點程式碼已標記註釋):

MyCommandLineRunnerApplicationRunner 是在何時被呼叫的呢?

開啟 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)

靈魂追問

  1. 上面程式執行結果, afterPropertiesSet 方法呼叫先於 @PostConstruct 方法,但這和我們在 Spring Bean 生命週期之緣起 中的呼叫順序恰恰相反,你知道為什麼嗎?
  2. MyPostConstructBean 通過 @DependsOn("myApplicationListener") 依賴了 MyApplicationListener,為什麼呼叫結果前者先與後者呢?
  3. 為什麼不建議 @Autowired 形式依賴注入

在寫 Spring Bean 生命週期時就有朋友問我與之相關的問題,顯然他們在概念上有一些含混,所以,仔細理解上面的問題將會幫助你加深對 Spring Bean 生命週期的理解

歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本著將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


相關推薦

記憶Spring Boot 應用啟動階段執行程式碼方式

前言 有時候我們需要在應用啟動時執行一些程式碼片段,這些片段可能是僅僅是為了記錄 log,也可能是在啟動時檢查與安裝證書 ,諸如上述業務要求我們可能會經常碰到 Spring Boot 提供了至少 5 種方式用於在應用啟動時執行程式碼。我們應該如何選擇?本文將會逐步解釋與分析這幾種不同方式 CommandLi

秒懂Spring @Scheduled定時任務的fixedRate,fixedDelay,cron執行差異

https://blog.csdn.net/applebomb/article/details/52400154   看字面意思容易理解,但是任務執行長度超過週期會怎樣呢? 不多說,直接上圖: 測試程式碼: import java.text.DateFormat; imp

理解stacking過程

stacking的過程有一張圖非常經典,如下: 下面我們將對此圖進行解釋: 上半部分是用一個基礎模型進行5折交叉驗證,如:用XGBoost作為基礎模型Model1,5折交叉驗證就是先拿出四折作為training data,另外一折作為testing data。注意

Spring Boot統一格式返回資料的方式

  有些時候呢,我們需要統一格式進行返回,之前可能會定義某個實體類在每個方法的響應都是用這個實體類然後包含響應值,其實spring呢,可以有挺多種無侵入的統一包裝方法。   第一種: @RestControllerAdvice public class ResponseHandler impleme

Spring Boot獲取前端頁面引數的方式總結

  Spring Boot的一個好處就是通過註解可以輕鬆獲取前端頁面的引數,之後可以將引數經過一系列處理傳送到後臺資料庫,前段時間正好用到,但是忘得差不多了,獲得的方式有很多種,這種東西不寫下來一段時間不用就忘得差不多了,感覺記性越來越差了呢,這裡稍微總結一下,

Spring Boot中初始化資源的方式

  假設有這麼一個需求,要求在專案啟動過程中,完成執行緒池的初始化,加密證書載入等功能,你會怎麼做?如果沒想好答案,請接著往下看。今天介紹幾種在Spring Boot中進行資源初始化的方式,幫助大家解決和回答這個問題。CommandLineRunner定義初始化類 MyCommandL

PMP專案管理的49個過程全部瞭解

專案管理的49個過程,看錶格顯得比較單調,印象也不是很深,所以今天小編就給大家發一張圖片,可以用一張圖就能生動又詳細的瞭解PMP專案管理的49個過程。   大家看完是不是覺得一目瞭然了呢,圖片上傳後不知道是不是清楚,大家覺得不清楚的可以

掌握Python所有基礎知識Python入門足矣!

  今天用一張思維導圖彙總了Python基礎知識,與大家分享。第一張圖為總圖,之後為總圖的區域性。   總圖   區域性1   區域性2   結語 當然這只是基礎的入門階段,後續學

懂了區塊鏈再去挖礦看明白怎麼回事

  今年簡單就是區塊鏈爆發的一年,每天開啟媒體資訊都會看到各式的區塊鏈產品出現在你的面前。而且有好多粉絲諮詢我區塊鏈產品的問題,我都是以私人回覆的形式回答的。 既然這麼多人追捧對區塊鏈感興趣,今天就單獨用一

告訴SQL使用inner joinleft join 等

sql之left join、right join、inner join的區別 union、union all的區別跳轉https://www.cnblogs.com/logon/p/3748020.html SQL JOINS:   Please refer the

學會Python學習Python的簡單小白的福利

網上有這樣一張圖片,資訊量很大,通常會被配上標題“一張圖讓你學會Python”: 點選圖片可檢視大圖     這張圖流傳甚廣,但我沒有找到明確的出處,圖片上附帶了 UliPad 的作者 Limodou 的資訊,很有可能是原作者。如有知情者可留言告訴我。

10 搞定 TensorFlow 數據讀取機制

小夥伴 圖片 文章 網上 如何 導讀在學習tensorflow的過程中,有很多小夥伴反映讀取數據這一塊很難理解。確實這一塊官方的教程比較簡略,網上也找不到什麽合適的學習材料。今天這篇文章就以圖片的形式,用最簡單的語言,為大家詳細解釋一下tensorflow的數據讀取機制,文章的最後還會給出

告訴angular2所有知識點

技術分享 代碼 自動化 我想 合作 .cn 動畫 image 框架 忙活了半年,從angular2.0到現在angular4.2。從沒AOT到有AOT。我想說,angular2的學習曲線真的有點陡峭。只能說,angular2是一個比較完整的框架,框架就是這樣,一大堆條條框框

理解 docker 基本原理及快速入門

uil dir commit -name name 地址 什麽 生成 作者 http://www.cnblogs.com/SzeCheng/p/6822905.html 寫的非常好的一篇文章,不知道為什麽被刪除了。 利用Google快照,做個存檔。 快照地址:

告訴Raid的玩法

raid 概念一張圖告訴你Raid的玩法

了解傳統項目管理與敏捷項目管理的區別

項目管理 敏捷項目管理 敏捷開發 一張圖助你了解傳統項目管理與敏捷項目管理的區別

徹底理解js原型鏈

function Person() { this.name = 'sanlyshi'; this.age = '23'; this.eat = function () { console.log(this.name +' is eating!')

Python 基礎 告訴PyCharm如何進行斷點除錯

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

辨別程式設計師中的菜鳥、普通、大牛和真大神!

這屆的沙雕網友太厲害了,自創程式設計師鑑定套圖,菜鳥、普通、大牛到大神四個級別的程式設計師,分別被劃分為四個座標象限,真是太有畫面感了~ 每天穿拖鞋背心,看起來吊兒郎當的,往往是程式設計師裡的“掃地僧” 菜鳥程式設計師不出么蛾子,團隊怎麼會有事做 緣分到了,BUG自然就

看懂Java的八基本資料型別

String和Integer不是Java的八種基本資料型別。char只能儲存一個字元(用單引號),String能夠儲存多個字元(用雙引號)。String屬於final類,定義的是物件,Integer 是 java 為 int 提供的封裝類。int 的預設值為 0,