[深入剖析Spring Boot]啟動、事件通知與配置加載原理

概述

Spring歷來一直是JAVA研發中不可或缺的框架,它提供了完美的控制反轉功能,使應用能夠達到低耦合的設計規范。在微服務時代,Spring Boot的免配置設計更是丟棄了原來復雜的xml聲明模式,使程序員能更加專心業務代碼的編寫。

最近在面試時,我問過很多同學關于Spring生命周期的問題,可能完整的回答上來的人寥寥無幾。Spring其實并不難,反而是很經典。其架構設計巧妙,代碼邏輯清晰,即便對編程不太了解的人也能看得懂,不信你繼續往下看。

本文主要關注Spring Boot是如何創建啟動類和如何管理bean的生命周期這兩點內容,我會深入到Spring Boot框架的源碼,從神秘的 SpringApplication 類開始,一步步揭開Spring Boot美麗的面紗。如果你是初學者,這篇文章可能會帶給你更多迷茫,請移步搜索Spring Boot基礎使用教程。

問題

首先來看一個最簡單的Spring Boot應用,代碼如下。

@SpringBootApplication
public class PeopleBatchApplication {

    public static void main(String[] args) {
        SpringApplication.run(PeopleBatchApplication.class, args);
    }
}

public static void main 多簡單熟悉的語句,就差一個System.out.println("Hello World")就是我們幾年前初次接觸java時的第一個程序。這段代碼中的 SpringApplication.run(PeopleBatchApplication.class, args);語句就是Spring Boot的 Hello World,這一句代碼就完整的創建了一個Spring的運行環境。

我們的問題就此開始,這句代碼是如何完成Spring上下文創建以及相關bean的聲明呢?那個在主類上標記的奇怪的@SpringBootApplication注解是什么?Spring Boot如何識別web環境,并創建Servlet上下文環境?Spring Boot是如何識別

Spring Boot 如何創建上下文環境

創建 SpringApplication

SpringApplication類是Spring Boot應用的標配,它可以啟動Spring應用并加載配置文件,并創建Spring上下文環境。

源碼展示

SpringApplication類部分源碼如下。

public class SpringApplication {

    /**
    *這個是在入門示例中所調用的方法
    **/
    public static ConfigurableApplicationContext run(Object source, String... args) {
        return run(new Object[] { source }, args);
    }
    
    /**
    * 創建可配置的應用上下文
    * @param sources 要作為配置類的對象
    * @param args 程序啟動參數
    */
    public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return new SpringApplication(sources).run(args);
    }

    /**
    * 構造方法
    */
    public SpringApplication(Object... sources) {
        initialize(sources);
    }
    
    /**
    * 初始化 SpringApplication 的方法
    **/
    private void initialize(Object[] sources) {
        if (sources != null && sources.length > 0) {
            this.sources.addAll(Arrays.asList(sources));
        }
        this.webEnvironment = deduceWebEnvironment();
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }

    private boolean deduceWebEnvironment() {
        for (String className : WEB_ENVIRONMENT_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return false;
            }
        }
        return true;
    }
}

初始化SpringApplication對象流程

SpringApplication對象的初始化經過了以下步驟:

  1. 記錄下用戶指定的Spring Boot配置類信息,并將配置類對象存入到全局變量 Set<Object> sources
  2. 驗證運行環境是否為web環境,并將驗證結果存入 boolean webEnvironment全局變量。
  3. 設置需要新初始化的上下文配置對象(需要在 META_INFO/spring.factories 文件中指定的bean工廠創建類的實例,默認實現類有org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer,org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,org.springframework.boot.context.ContextIdApplicationContextInitializer,org.springframework.boot.context.config.DelegatingApplicationContextInitializer,org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer這幾個類)。并將初始化類放入到 List<ApplicationContextInitializer<?> initializers 全局變量。
  4. 設置默認bean監聽器(默認加載spring-boot-autoconfigure.jar包中META_INFO/spring.factories文件所指定的監聽器實現org.springframework.boot.autoconfigure.BackgroundPreinitializer)。并將初始化監聽器放入到 List<ApplicationListener<?>> listeners
  5. 設置main方法所在的類到全局變量 Class mainApplicationClass。

小結

SpringApplication對象在初始化時主要將應用的各個配置都存入全部變量,并沒有進行邏輯操作。如果我們需要自定義bean工廠或者監聽器的話,可以選擇在classpath下建立自己的META_INF/spring.factories文件分別指定初始化bean工廠和bean監聽器。注意自建的初始化工廠或監聽器并沒有覆蓋spring原有的bean工廠。

從上面的代碼我們可以看出,只要classpath中存在javax.servlet.Servletorg.springframework.web.context.ConfigurableWebApplicationContext類,Spring Boot都自動構建Servlet運行環境。

SpringApplication 的 run 方法

run方法主要用于創建或刷新一個應用上下文,是 Spring Boot的核心。

run 方法源碼

比起 SpringApplication 的創建的代碼,run方法的邏輯復雜了許多。不但有加載配置文件,創建上下文環境的邏輯,還有對創建Spring上下文的計時信息,另外我們啟用Spring Boot應用時所打印的那個字符串圖像標識,也是在這個方法上控制的(如果你很不喜歡這個標識,可以考慮在這里去掉)。

    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            analyzers = new FailureAnalyzers(context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            listeners.finished(context, null);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, analyzers, ex);
            throw new IllegalStateException(ex);
        }
    }

run 方法執行流程

  1. 創建計時器,用于記錄SpringBoot應用上下文的創建所耗費的時間。
  2. 開啟所有的SpringApplicationRunListener監聽器,用于監聽Sring Boot應用加載與啟動信息。
  3. 創建應用配置對象(main方法的參數配置) ConfigurableEnvironment
  4. 創建要打印的Spring Boot啟動標記 Banner
  5. 創建 ApplicationContext應用上下文對象,web環境和普通環境使用不同的應用上下文。
  6. 創建應用上下文啟動失敗原因分析對象 FailureAnalyzers
  7. 刷新應用上下文,并從xml、properties、yml配置文件或數據庫中加載配置信息,并創建已配置的相關的單例bean。到這一步,所有的非延遲加載的Spring bean都應該被創建成功。
  8. 調用實現了*Runner類型的bean的run方法,開始應用啟動。
  9. 完成Spring Boot啟動監聽
  10. 打印Spring Boot上下文啟動耗時
  11. 如果在上述步驟中有異常發生則日志記錄下才創建上下文失敗的原因并拋出IllegalStateException異常。

運行事件

事件就是Spring Boot啟動過程的狀態描述,在啟動Spring Boot時所發生的事件一般指:

  • 開始啟動事件
  • 環境準備完成事件
  • 上下文準備完成事件
  • 上下文加載完成
  • 應用啟動完成事件

Spring啟動時的事件都是繼承自 SpringApplicationEvent 抽象類,每一個事件都包含了應用的 SpringApplication 對象和應用程序啟動時的參數。

SpringApplicationRunListener 運行監聽器

顧名思意,運行監聽器的作用就是為了監聽 SpringApplication 的run方法的運行情況。在設計上監聽器使用觀察者模式,以總信息發布器 SpringApplicationRunListeners 為基礎平臺,將Spring啟動時的事件分別發布到各個用戶或系統在 META_INF/spring.factories文件中指定的應用初始化監聽器中。使用觀察者模式,在Spring應用啟動時無需對啟動時的其它業務bean的配置關心,只需要正常啟動創建Spring應用上下文環境。各個業務'監聽觀察者'在監聽到spring開始啟動,或環境準備完成等事件后,會按照自己的邏輯創建所需的bean或者進行相應的配置。觀察者模式使run方法的結構變得清晰,同時與外部耦合降到最低。

運行時監聽器繼承自 SpringApplicationRunListener 接口,其代碼如下:

package org.springframework.boot;
public interface SpringApplicationRunListener {

    /**
     * 在run方法業務邏輯執行、應用上下文初始化前調用此方法
     */
    void starting();

    /**
     * 當環境準備完成,應用上下文被創建之前調用此方法
     */
    void environmentPrepared(ConfigurableEnvironment environment);

    /**
     * 在應用上下文被創建和準備完成之后,但上下文相關代碼被加載執行之前調用。因為上下文準備事件和上下文加載事件難以明確區分,所以這個方法一般沒有具體實現。
     */
    void contextPrepared(ConfigurableApplicationContext context);

    /**
     *當上下文加載完成之后,自定義bean完全加載完成之前調用此方法。
     */
    void contextLoaded(ConfigurableApplicationContext context);

    /**
     *當run方法執行完成,或執行過程中發現異常時調用此方法。
     */
    void finished(ConfigurableApplicationContext context, Throwable exception);
}

默認情況下Spring Boot會實例化EventPublishingRunListener作為運行監聽器的實例。在實例化運行監聽器時需要SpringApplication對象和用戶對象作為參數。其內部維護著一個事件廣播器(被觀察者對象集合,前面所提到的在META_INF/spring.factories中注冊的初始化監聽器的有序集合 ),當監聽到Spring啟動等事件發生后,就會將創建具體事件對象,并廣播推送給各個被觀察者。

運行事件廣播

下面的代碼來自SimpleApplicationEventMulticaster,主要描述如何將Spring Boot啟動時的各個事件推送到被觀察者。

/**
* 將接受的事件進行廣播
*/
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        Executor executor = getTaskExecutor();
        if (executor != null) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    invokeListener(listener, event);
                }
            });
        }
        else {
            invokeListener(listener, event);
        }
    }
}

/**
* 將給定的事件發送到指定的監聽器
*/
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
        try {
            doInvokeListener(listener, event);
        }
        catch (Throwable err) {
            errorHandler.handleError(err);
        }
    }
    else {
        doInvokeListener(listener, event);
    }
}

@SuppressWarnings({"unchecked", "rawtypes"})
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
    try {
        listener.onApplicationEvent(event);
    }
    catch (ClassCastException ex) {
        String msg = ex.getMessage();
        if (msg == null || msg.startsWith(event.getClass().getName())) {
            // Possibly a lambda-defined listener which we could not resolve the generic event type for
            Log logger = LogFactory.getLog(getClass());
            if (logger.isDebugEnabled()) {
                logger.debug("Non-matching event type for listener: " + listener, ex);
            }
        }
        else {
            throw ex;
        }
    }
}

細讀這段代,之所以我們的應用會啟動的很慢,很大的原因就是因為在創建應用時我們對事件的處理機制都是同步的,如果業務邏輯允許,我們將廣播方法改為異步的(通過 public void setTaskExecutor(Executor taskExecutor)可以借助線程池實現異步),可能會大幅提高應用啟動速度。

運行業務監聽器

這里的'運行業務監聽器'指的是每個組件對Spring Boot啟動事件的監聽器,其主要作用對在Spring啟動狀態做出明確響應。如日志監聽器LoggingApplicationListener會對啟動時的狀態做日志記錄,ConfigFileApplicationListener會在接收到環境配置完成事件后解析加載配置文件。

下面以ConfigFileApplicationListener為例,簡要的看看運行業務監聽器時怎么處理事件的。

package org.springframework.boot.context.config;
public class ConfigFileApplicationListener
        implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
            
    /*
    *收到事件請求后執行這個方法
    *配置文件監聽器只監聽環境配置完成事件和上下文加載完成事件
    **/
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

    /**
    * 獲取所有環境配置處理器,并根據事件所給出的環境執行加載文件配置任務
    **/
    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }
    
    
    /**
    * 環境配置方法
    * 首先加載應用配置文件 application.properties 包括已激活的profiles的配置文件
    * 其次配置是否忽略bean的信息
    * 最后將配置文件的配置信息綁定到 SpringApplication中
    */
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
        configureIgnoreBeanInfo(environment);
        bindToSpringApplication(environment, application);
    }
        }

onApplicationEnvironmentPreparedEvent()方法相關代碼的主要作用是首先從META_INF/spring.factories中找到所有屬性名為org.springframework.boot.env.EnvironmentPostProcessor的環境前置處理器,并將其加入到有序的環境處理器列表中,且ConfigFileApplicationListener類就恰恰在這個前置處理器列表里。然后逐個執行每個環境前置處理器的前置處理方法。

@Enable* 注解就是通過在 META_INF/spring.factories 中配置 org.springframework.boot.autoconfigure.EnableAutoConfiguration 屬性,其值指向對注解的解析類而實現的。學會了這段內容后,我們也可以自己設計一個 spring-boot-*-start.jar 包,并完成其自動配置了。

對于ConfigFileApplicationListener的環境前置處理方法請注意,當收到環境配置完成事件后從classpath中加載并解析application.properties/application.yml 配置文件,在以下位置的應用配置文件都會被掃描到:
<ul>
<li>file:./config/:</li>
<li>file:./</li>
<li>classpath:config/</li>
<li>classpath:</li>
</ul>
注意路徑的掃描順序,在不同路徑下的應用配置文件中如果有相同的屬性,后加載的屬性會覆蓋先加載的屬性。另外如果在應用配置文件中指定了 spring.config.location 屬性,該路徑下的配置文件也會自動被spring掃描并加載。

Spring Boot配置屬性的優先級(前面會覆蓋后面的):

  1. 命令行啟動參數的配置
  2. 后加載的配置文件中的屬性會覆蓋先加載的
  3. 后加載的 application-profiles.yml 中的屬性會覆蓋先加載的 application.yml

引用

本文是對Spring Boot源碼的深度分析,為為在此首先特別感謝Spring團隊無私的將經典的代碼開源,另外感謝我的同事對我寫作此文期間給予真切的鼓勵與支持。

關于

文章內容著重源碼分析與設計詳解,可能沒有對實際使用的用例,因此不太適合Spring Boot的初學者。

后記

本文主要介紹了Spring Boot啟動時的主體流程、事件解耦設計與配置加載原理,后續下篇文章將深入剖析上下文環境 ApplicationContext 的加載原理與流程。

本文內容主要是對 Spring Boot 1.5.9RELEASE的源碼解析,不過作者水平有限,有不盡然的地方敬請指出。本項目和文檔中所用的內容僅供學習和研究之用,轉載或引用時請指明出處。如果你對文檔有疑問或問題,請在項目中給我留言或發email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技術能夠改變世界 。

鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容