當前位置: 妍妍網 > 碼農

架構師必備知識: 一張長圖透徹理解 SpringBoot 啟動原理

2024-06-11碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

雖然Java程式設計師大部份工作都是CRUD,但是工作中常用的中介軟體必須和Spring整合,如果不知道Spring的原理,很難理解這些中介軟體和框架的原理。

一張長圖透徹解釋 Spring啟動順序

測試對Spring啟動原理的理解程度

我舉個例子,測試一下,你對Spring啟動原理的理解程度。

  • Rpc框架和Spring的整合問題。Rpc框架何時註冊暴露服務,在哪個Spring擴充套件點註冊呢? init-method 中行不行?

  • MQ 消費組和Spring的整合問題。MQ消費者何時開始消費,在哪個Spring擴充套件點」註冊「自己? init-method 中行不行?

  • SpringBoot 整合Tomcat問題。如果出現已開啟Http流量,Spring還未啟動完成,怎麽辦?Tomcat何時開啟埠,對外服務?

  • SpringBoot計畫常見的流量入口無外乎 Rpc、Http、MQ 三種方式。一名合格的架構師必須精通服務的入口流量何時開啟,如何正確開啟?最近我遇到的兩次線上故障都和Spring啟動過程相關。

    故障的具體表現是:Kafka消費組已經開始消費,已開啟流量,然而Spring 還未啟動完成。因為業務程式碼中使用的Spring Event事件訂閱元件還未啟動(訂閱者還未註冊到Spring),所以處理異常,出了線上故障。根本原因是————計畫在錯誤的時機開啟 MQ 流量,然而Spring還未啟動完成,導致出現故障。

    正確的做法是:計畫在Spring啟動完成後開啟入口流量,然而我司的Kafka消費組 在Spring init-method bean 例項化階段就開啟了流量,導致故障發生。出現這樣的問題,說明計畫初期的程式設計師沒有深入理解Spring的啟動原理。

    接下來,我再次丟擲 11 個問題,說明這個問題————深入理解Spring啟動原理的重要性。

    1. Spring還未完全啟動,在 PostConstruct 中呼叫 getBeanByAnnotation 能否獲得準確的結果?

    2. 計畫應該如何監聽 Spring 的啟動就緒事件?

    3. 計畫如何監聽Spring 重新整理事件?

    4. Spring就緒事件和重新整理事件的執行順序和區別?

    5. Http 流量入口何時啟動完成?

    6. 計畫中在 init-method 方法中註冊 Rpc 是否合理?什麽是合理的時機?

    7. 計畫中在 init-method 方法中註冊 MQ 消費組是否合理?什麽是合理的時機?

    8. PostConstruct 中方法依賴 ApplicationContextAware 拿到 ApplicationContext ,兩者的順序誰先誰後?是否會出現空指標!

    9. init-method PostConstruct afterPropertiesSet 三個方法的執行順序?

    10. 有兩個 Bean聲明了初始化方法。A使用 PostConstruct 註解聲明,B使用 init-method 聲明。Spring一定先執行 A 的 PostConstruct 方法嗎?

    11. Spring 何時裝配Autowire內容, PostConstruct 方法中參照 Autowired 欄位什麽場景會空指標?

    精通Spring 啟動原理,以上問題則迎刃而解。接下來,大家一起學習Spring的啟動原理,看看Spring的擴充套件點分別在何時執行。

    一起數數 Spring啟動過程的擴充套件點有幾個?

    Spring的擴充套件點極多,這裏為了講清楚啟動原理,所以只列舉和啟動過程有關的擴充套件點。

    1. BeanFactoryAware 可在Bean 中獲取 BeanFactory 例項

    2. ApplicationContextAware 可在Bean 中獲取 ApplicationContext 例項

    3. BeanNameAware 可以在Bean中得到它在IOC容器中的Bean的例項的名字。

    4. ApplicationListener 可監聽 ContextRefreshedEvent 等。

    5. CommandLineRunner 整個計畫啟動完畢後,自動執行

    6. SmartLifecycle#start 在Spring Bean例項化完成後,執行start 方法。

    7. 使用 @PostConstruct 註解,用於Bean例項初始化

    8. 實作 InitializingBean 介面,用於Bean例項初始化

    9. xml 中聲明 init-method 方法,用於Bean例項初始化

    10. Configuration 配置類 透過@Bean註解 註冊Bean到Spring

    11. BeanPostProcessor 在Bean的初始化前後,植入擴充套件點!

    12. BeanFactoryPostProcessor BeanFactory 建立後植入 擴充套件點!

    透過打印日誌學習Spring的執行順序

    首先我們先透過 程式碼實驗,驗證一下以上擴充套件點的執行順序。

    1.聲明 TestSpringOrder 分別繼承以下介面,並且在介面方法實作中,日誌打印該介面的名稱。

    public classTestSpringOrderimplements
    ApplicationContextAware,
    BeanFactoryAware
    InitializingBean
    SmartLifecycle
    BeanNameAware
    ApplicationListener<ContextRefreshedEvent>, 
    CommandLineRunner,
    SmartInitializingSingleton
    {

    @Override
    publicvoidafterPropertiesSet()throws Exception {
    log.error("啟動順序:afterPropertiesSet");
    }
    @Override
    publicvoidsetApplicationContext(ApplicationContext applicationContext)throws BeansException {
    log.error("啟動順序:setApplicationContext");
    }

    2. TestSpringOrder 使用 PostConstruct 註解初始化,聲明 init-method 方法初始化。

    @PostConstruct
    publicvoidpostConstruct(){
    log.error("啟動順序:post-construct");
    }
    publicvoidinitMethod(){
    log.error("啟動順序:init-method");
    }

    3.新建 TestSpringOrder2 繼承

    public classTestSpringOrder3implements
    BeanPostProcessor
    BeanFactoryPostProcessor
    {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)throws BeansException {
    log.error("啟動順序:BeanPostProcessor postProcessBeforeInitialization beanName:{}", beanName);
    return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)throws BeansException {
    log.error("啟動順序:BeanPostProcessor postProcessAfterInitialization beanName:{}", beanName);
    return bean;
    }
    @Override
    publicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throws BeansException {
    log.error("啟動順序:BeanFactoryPostProcessor postProcessBeanFactory ");
    }
    }

    執行以上程式碼後,可以在日誌中看到啟動順序!

    實際的執行順序

    2023-11-25 18:10:53,748 [main] ERROR (TestSpringOrder3:37) - 啟動順序:BeanFactoryPostProcessor postProcessBeanFactory 
    2023-11-25 18:10:59,299 [main] ERROR (TestSpringOrder:53) - 啟動順序:建構函式 TestSpringOrder
    2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:127) - 啟動順序: Autowired
    2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:129) - 啟動順序:setBeanName
    2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:111) - 啟動順序:setBeanFactory
    2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:121) - 啟動順序:setApplicationContext
    2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder3:25) - 啟動順序:BeanPostProcessor postProcessBeforeInitialization beanName:testSpringOrder
    2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:63) - 啟動順序:post-construct
    2023-11-25 18:10:59,317 [main] ERROR (TestSpringOrder:116) - 啟動順序:afterPropertiesSet
    2023-11-25 18:10:59,317 [main] ERROR (TestSpringOrder:46) - 啟動順序:init-method
    2023-11-25 18:10:59,320 [main] ERROR (TestSpringOrder3:31) - 啟動順序:BeanPostProcessor postProcessAfterInitialization beanName:testSpringOrder
    2023-11-25 18:17:21,563 [main] ERROR (SpringOrderConfiguartion:21) - 啟動順序: @Bean 註解方法執行
    2023-11-25 18:17:21,668 [main] ERROR (TestSpringOrder:58) - 啟動順序:SmartInitializingSingleton
    2023-11-25 18:17:21,675 [main] ERROR (TestSpringOrder:74) - 啟動順序:start
    2023-11-25 18:17:23,508 [main] ERROR (TestSpringOrder:68) - 啟動順序:ContextRefreshedEvent
    2023-11-25 18:17:23,574 [main] ERROR (TestSpringOrder:79) - 啟動順序:CommandLineRunner

    我透過在以上擴充套件點 添加 debug 斷點,偵錯程式碼,整理出 Spring啟動原理的 長圖。過程省略…………

    一張長圖透徹解釋 Spring啟動順序

    例項化和初始化的區別

    new TestSpringOrder() :new 建立物件例項,即為例項化一個物件;執行該Bean的 init-method 等方法 為初始化一個Bean。註意初始化和例項化的區別。


    Spring 重要擴充套件點的啟動順序

    1.BeanFactoryPostProcessor

    BeanFactory初始化之後,所有的Bean定義已經被載入,但Bean例項還沒被建立(不包括 BeanFactoryPostProcessor 型別)。Spring IoC容器允許 BeanFactoryPostProcessor 讀取配置後設資料,修改bean的定義,Bean的內容值等。

    2.例項化Bean

    Spring 呼叫java反射API 例項化 Bean。等同於 new TestSpringOrder() ;

    3.Autowired 裝配依賴

    Autowired是 借助於 AutowiredAnnotationBeanPostProcessor 解析 Bean 的依賴,裝配依賴。如果被依賴的Bean還未初始化,則先初始化 被依賴的Bean。在 Bean例項化完成後,Spring將首先裝配Bean依賴的內容。

    4.BeanNameAware

    setBeanName

    5.BeanFactoryAware

    setBeanFactory

    6.ApplicationContextAware setApplicationContext

    在Bean例項化前,會率先設定Aware介面,例如 BeanNameAware BeanFactoryAware ApplicationContextAware

    7.BeanPostProcessor postProcessBeforeInitialization

    如果我想在 bean初始化方法前後要添加一些自己邏輯處理。可以提供 BeanPostProcessor 介面實作類,然後註冊到Spring IoC容器中。在此介面中,可以建立Bean的代理,甚至替換這個Bean。

    8.PostConstruct 執行

    接下來 Spring會依次呼叫 Bean例項初始化的 三大方法。

    9.InitializingBean

    afterPropertiesSet

    10.init-method

    方法執行

    11.BeanPostProcessor postProcessAfterInitialization

    在 Spring 對Bean的初始化方法執行完成後,執行該方法

    12.其他Bean 例項化和初始化

    Spring 會迴圈初始化Bean。直至所有的單例Bean都完成初始化

    13.所有單例Bean 初始化完成後

    14.SmartInitializingSingleton Bean例項化後置處理

    該介面的執行時機在 所有的單例Bean執行完成後。例如Spring 事件訂閱機制的 EventListener 註解,所有的訂閱者 都是 在這個位置被註冊進 Spring的。而在此之前,Spring Event訂閱機制還未初始化完成。所以如果有 MQ、Rpc 入口流量在此之前開啟,Spring Event就可能出問題!

    所以強烈建議 Http、MQ、Rpc 入口流量在 SmartInitializingSingleton 之後開啟流量。

    Http、MQ、Rpc 入口流量必須在 SmartInitializingSingleton 之後開啟流量。

    15.Spring 提供的擴充套件點,在所有單例Bean的 EventListener等元件全部啟動完成後,即Spring啟動完成,則執行 start 方法。在這個位置適合開啟入口流量!

    Http、MQ、Rpc 入口流量適合 在 SmartLifecyle 中開啟

    16.釋出 ContextRefreshedEvent 方法

    該事件會執行多次,在 Spring Refresh 執行完成後,就會釋出該事件!

    17.註冊和初始化 Spring MVC

    SpringBoot 套用,在父級 Spring啟動完成後,會嘗試啟動 內嵌式 tomcat容器。在此之前,SpringBoot會初始化 SpringMVC 和註冊 DispatcherServlet 到Web容器。

    18.Tomcat/Jetty 容器開啟埠

    SpringBoot 呼叫內嵌式容器,會開啟並監聽埠,此時Http流量就開啟了。

    19.套用啟動完成後,執行 CommandLineRunner

    SpringBoot 特有的機制,待所有的完全執行完成後,會執行該介面 run方法。值得一提的是,由於此時Http流量已經開啟,如果此時進行本地緩存初始化、預熱緩存等,稍微有些晚了!在這個間隔期,可能緩存還未就緒!

    所以預熱緩存的時機應該發生在 入口流量開啟之前,比較合適的機會是在 Bean初始化的階段。雖然 在Bean初始化時 Spring尚未完成啟動,但是呼叫 Bean預熱緩存也是可以的。但是註意:不要在 Bean初始化時 使用 Spring Event,因為它還未完成初始化 。

    回答 關於 Spring 啟動原理的若幹問題

    1.init-method、PostConstruct、afterPropertiesSet 三個方法的執行順序。

    回答: PostConstruct afterPropertiesSet init-method

    2.有兩個 Bean聲明了初始化方法。A使用 PostConstruct註解聲明,B使用 init-method 聲明。Spring一定先執行 A 的PostConstruct 方法嗎?

    回答:Spring 會迴圈初始化Bean例項,初始化完成1個Bean,再初始化下一個Bean。Spring並沒有使用這種機制啟動,即所有的Bean先執行 PostConstruct ,再統一執行 afterProperfiesSet

    此外,A、B兩個Bean的初始化順序不確定,誰先誰後不確定。無法保證 A 的 PostConstruct 一定先執行。除非使用 Order註解,聲明Bean的初始化順序!

    3.Spring 何時裝配Autowire內容,PostConstruct方法中參照 Autowired 欄位是否會空指標?

    Autowired裝配依賴發生在 PostConstruct 之前,不會出現空指標!

    4.PostConstruct 中方法依賴ApplicationContextAware拿到 ApplicationContext,兩者的順序誰先誰後?是否會出現空指標!

    ApplicationContextAware 會先執行,不會出現空指標!但是當Autowired沒有找到對應的依賴,並且聲明了非強制依賴時,該欄位會為空,有潛在 空指標風險。

    5.計畫應該如何監聽 Spring 的啟動就緒事件。

    透過 SmartLifecyle start 方法,監聽Spring就緒 。適合在此開啟入口流量!

    6.計畫如何監聽Spring 重新整理事件。

    監聽 Spring Event ContextRefreshedEvent

    7.Spring就緒事件和重新整理事件的執行順序和區別。

    Spring就緒事件會先於 重新整理事件。兩者都可能多次執行,要確保方法的冪等處理,避免重復註冊問題

    8.Http 流量入口何時啟動完成。

    SpringBoot 最後階段,啟動完成Spring 上下文,才開啟Http入口流量,此時 SmartLifecycle#start 已執行。所有單例Bean和SpringEvent等元件都已經就緒!

    9.計畫中在 init-method 方法中註冊 Rpc是否合理?什麽是合理的時機?

    init 開啟Rpc流量非常不合理。因為Spring尚未啟動完成,包括 Spring Event尚未就緒!

    10.計畫中在 init-method 方法中註冊 MQ消費組是否合理?什麽是合理的時機?

    init 開啟 MQ 流量非常不合理。因為Spring尚未啟動完成,包括 Spring Event尚未就緒!

    11.Spring還未完全啟動,在 PostConstruct 中呼叫 getBeanByAnnotation能否獲得準確的結果?

    雖然未啟動完成,但是Spring執行該 getBeanByAnnotation 方法時,會率先檢查 Bean定義,如果Bean定義對應的 Bean尚未初始化,則初始化這些Bean。所以即便是Spring初始化過程中呼叫,呼叫結果是準確的。

    源碼級別介紹

    SmartInitializingSingleton 介面的執行位置

    下圖程式碼說明了,Spring在初始化全部 單例Bean以後,會執行 SmartInitializingSingleton 介面。

    Autowired 何時裝配Bean的依賴

    在Bean例項化之後,但初始化之前, AutowiredAnnotationBeanPostProcessor 會註入Autowired欄位。

    SpringBoot 何時開啟Http埠

    下圖程式碼中可以看到,SpringBoot會首先啟動 Spring上下文,完成後才啟動 嵌入式Web容器,初始化SpringMVC,監聽埠

    Spring 初始化Bean的關鍵程式碼

    下圖我加了註釋,Spring初始化Bean的關鍵程式碼,全在 這個方法裏,感興趣的可以自行查閱程式碼 。

    AbstractAutowireCapableBeanFactory#initializeBean

    Spring CommandLineRunner 執行位置

    Spring Boot外部,當啟動完Spring上下文以後,最後才啟動 CommandLineRunner

    總結

    SpringBoot 會在Spring完全啟動完成後,才開啟Http流量。這給了我們啟示:應該在Spring啟動完成後開啟入口流量。Rpc和 MQ流量 也應該如此,所以建議大家 在 SmartLifecype 或者 ContextRefreshedEvent 等位置 註冊服務,開啟流量。

    例如 Spring Cloud Eureka 服務發現元件,就是在 SmartLifecype 中註冊服務的!

    整理 10 個小時寫完本篇文章,希望大家有所收獲。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:五陽

    來源:juejin.cn/post/7308610896803659812

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]