SpringBoot 系列-啟動過程

glmapper發表於2019-12-09

微信公眾號:glmapper工作室
掘金專欄:glmapper
微 博:瘋狂的石頭_henu
歡迎關注,一起學習、一起分享

SpringBoot 作為目前非常流行的微服務框架,它使得構建獨立的 Spring 生產級應用變得非常簡單,因此受到很多網際網路企業的青睞。

推薦閱讀

背景

最近在寫 SOFATracer 整合 Spring Cloud Stream RocketMQ 的過程中,遇到了一些問題,比如:BeanPostProcessor 不生效,如何在 BeanPostProcessor 不生效的情況下去修改一個 Bean 等,這些問題其實都是和 Bean 的生命週期有關係的,當然也和容器啟動的過程有關係。SpringBoot 的啟動過程對於我來說其實不算陌生,也可以說是比較熟悉,但是之前沒有完整的梳理過這一款的東西,再實際的應用過程成難免再去踩一些坑。另外想到之前也寫過一篇 SpringBoot系列- FatJar 啟動原理,剛好承接上篇,繼續來探索 SpringBoot 中的一些知識點。

注:本篇基於 SpringBoot 2.1.0.RELEASE 版本,SpringBoot 各個版本之間可能存在差異,不過大體流程基本差不多,所以各位看官在實際的工作過程中也

啟動入口

在這篇SpringBoot系列- FatJar 啟動原理 文章中介紹得到,JarLaunch 最後是構建了一個 MainMethodRunner 例項物件,然後通過反射的方式呼叫了 BootStrap 類中的 main 方法,這裡的 ’BootStrap 類中的 main 方法‘ 實際上就是 SpringBoot 的業務入口,也就是常見的下面的程式碼片段:

@SpringBootApplication
public class GlmapperApplication {
    public static void main(String[] args) {
        SpringApplication.run(GlmapperApplication.class, args);
    }
}
複製程式碼

從程式碼可以非常直觀的瞭解到,啟動是通過呼叫 SpringApplication 的靜態方法 run;這個 run 方法內部其實是會構造一個 SpringApplication 的例項,然後再呼叫這裡例項的 run 方法來啟動 SpringBoot的。

/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
@param primarySources the primary sources to load
@param args the application arguments (usually passed from a Java main method)
@return the running {@link ApplicationContext}
*/

public static ConfigurableApplicationContext run(Class<?>[] primarySources,
    String[] args)
 
{
    return new SpringApplication(primarySources).run(args);
}
複製程式碼

因此,如果要分析 SpringBoot 的啟動過程,我們需要熟悉 SpringApplication 的構造過程以及 SpringApplication 的 run 方法執行過程即可。

SpringApplication 例項的構建

篇幅原因,我們只分析核心的構建流程。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 資源載入器,預設是 null
    this.resourceLoader = resourceLoader;
    // 啟動類 bean 
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 是否是 web 應用
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 設定了 ApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    // 設定 ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 啟動類
    this.mainApplicationClass = deduceMainApplicationClass();
}
複製程式碼

上面程式碼段中,需要關注兩個點:

  • 1、初始化 ApplicationContextInitializer;
  • 2、初始化 ApplicationListener

要注意的是這裡的例項化,並非是通過註解和掃包完成,而是通過一種不依賴 Spring 上下文的載入方法;這種做法是為了能夠使得在 Spring 完成啟動前做各種配置。Spring 的解決方法是以介面的全限定名作為 key,實現類的全限定名作為 value 記錄在專案的 META-INF/spring.factories 檔案中,然後通過SpringFactoriesLoader 工具類提供靜態方法進行類載入並快取下來,spring.factories 是Spring Boot 的核心配置檔案。SpringFactoriesLoader 可以理解為 Spring 自己提供的一種 spi 擴充套件實現。SpringBoot 中提供的預設的 spring.factories 配置如下:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
// ..省略

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
// ..省略

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
// ..省略

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\/
// ..省略

# Application Listeners
org.springframework.context.ApplicationListener=\
// ..省略

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
// ..省略

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
// ..省略

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
// ..省略
複製程式碼

關於 SpringFactoriesLoader 如何載入這些資源這裡就不過多分析,有興趣的讀者可以自行檢視相關原始碼。org.springframework.core.io.support.SpringFactoriesLoader#loadSpringFactories

run 方法主流程

這裡先直觀的看下程式碼,然後再逐個分析:

public ConfigurableApplicationContext run(String... args) {
    // 開啟容器啟動計時
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    // SpringBootExceptionReporter 列表,SpringBoot 允許自定義 Reporter
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 設定java.awt.headless屬性為true還是false
    // 可詳見解釋:https://blog.csdn.net/michaelgo/article/details/81634017
    configureHeadlessProperty();
    // 獲取所有 SpringApplicationRunListener ,也是通過 SpringFactoriesLoader 來獲取的
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 釋出 starting 事件,在首次啟動 run方法時立即呼叫,可用於非常早的初始化,注意此時容器上下文還沒有重新整理
    listeners.starting();
    try {
        // 構建 ApplicationArguments 物件
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 準備上下文重新整理需要的環境屬性 -- 詳見 prepareEnvironment 過程分析
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        // spring.beaninfo.ignore,如果為空設定為true
        configureIgnoreBeanInfo(environment);
        // 列印 SpringBoot 啟動 Banner
        Banner printedBanner = printBanner(environment);
        // 建立上下文,這裡會根據 webApplicationType 型別來建立不同的 ApplicationContext
        context = createApplicationContext();
        // 載入獲取 exceptionReporters
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        // 上下文重新整理之前的準備工作 -- 詳見 prepareContext 過程分析
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        // 重新整理上下文 -- 詳見 refreshContext 過程分析
        refreshContext(context);
        // 重新整理之後回撥,SpringBoot 中這個方法是空實現,可以自行擴充套件
        afterRefresh(context, applicationArguments);
        // 停止計時
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        // 釋出 started 事件 
        listeners.started(context);
        // ApplicationRunner 和 CommandLineRunner 呼叫
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        // 異常處理
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 釋出 running 事件 
        listeners.running(context);
    }
    catch (Throwable ex) {
        // 異常處理
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}
複製程式碼

上面對程式碼基本都做了一些簡單的註釋,有幾個需要關注的點:

  • 1、prepareEnvironment 的處理過程
  • 2、prepareContext 的處理過程
  • 3、refreshContext 的處理過程
  • 4、listeners 執行時機及順序
  • 5、異常處理邏輯

關於 Listeners 執行時機及順序在之前的文章中有做過非常詳細的分析,詳見:SpringBoot 系列-事件機制詳解。下面就對其他的 4 個點做下詳細的分析。

分析啟動過程,本質上是對其整個容器生命週期有個瞭解,包括 listeners 執行各個事件的時機、PostProcessor 執行的時機,Enviroment Ready 的時機等等。掌握這些擴充套件和時機,可以在實際的業務開發中來做很多事情。

prepareEnvironment 的處理過程

prepareEnvironment 過程相對來說是比較早的,這裡主要就是為上下文重新整理提供 Environment。

private ConfigurableEnvironment prepareEnvironment(
            SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments)
 
{
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 配置 PropertySources 和 Profiles
    // 1、將引數和一些預設的屬性配置到 environment
    // 2、啟用 profiles 
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 釋出 ApplicationEnvironmentPreparedEvent 事件
    listeners.environmentPrepared(environment);
    // 繫結 SpringApplication 環境
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader())
                .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
    // 附加的解析器將動態跟蹤底層 Environment 屬性源的任何新增或刪除
    ConfigurationPropertySources.attach(environment);
    return environment;
}
複製程式碼

這裡面做的事情就是將我們的配置,包括系統配置、application.properties、-D 引數等等統統打包給 environment。在 Spring 中,我們最常見的 xml 中使用的 ${xxx} 或者程式碼中使用的 @Value("${xxxx}") 等,最後都是從 environment 中拿值的。

這裡需要關注的一個比較重要的點是釋出 ApplicationEnvironmentPreparedEvent 事件,我們可以通過監聽這個事件來修改 environment。這裡可以參考下 SOFATracer 中 SofaTracerConfigurationListener 是如何利用這個事件來做環境配置處理的。

prepareContext 的處理過程

prepareContext 的處理過程中可以利用的點是非常多的,比如 ApplicationContextInitializer 的執行、ApplicationContextInitializedEvent 和 ApplicationPreparedEvent 事件釋出。

private void prepareContext(ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner)
 
{
    // 設定 environment 給 context,所以需要注意的是,在此之前拿到的 context 中,environment 是沒有的。
    context.setEnvironment(environment);
    // 對 ApplicationContext 的後置處理,比如註冊 BeanNameGenerator 和 ResourceLoader
    postProcessApplicationContext(context);
    // 這裡開始執行所有的 ApplicationContextInitializer
    applyInitializers(context);
    // 釋出 ApplicationContextInitializedEvent 事件
    listeners.contextPrepared(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }
    // Add boot specific singleton beans
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    if (beanFactory instanceof DefaultListableBeanFactory) {
        // 是否允許 bean 覆蓋,這裡如果是 false ,則可能會導致 BeanDefinitionOverrideException 異常
        ((DefaultListableBeanFactory) beanFactory)
                .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }
    // Load the sources
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[0]));
    // 釋出 ApplicationPreparedEvent 事件
    listeners.contextLoaded(context);
}
複製程式碼

ApplicationContextInitializer 是 spring 容器重新整理之前初始化 Spring ConfigurableApplicationContext 的回撥介面,ApplicationContextInitializer 的 initialize 方法執行之前,context 是還沒有重新整理的。可以看到在 applyInitializers 之後緊接著釋出了 ApplicationContextInitializedEvent 事件。其實這兩個點都可以對 context 搞一些事情,ApplicationContextInitializer 更純粹些,它只關注 context;而 ApplicationContextInitializedEvent 事件源中除了 context 之外,還有 springApplication 物件和引數 args。

prepareContext 最後階段是釋出了 ApplicationPreparedEvent 事件,表示上下文已經準備好了,可以隨時執行 refresh 了。

refreshContext 的處理過程

refreshContext 是 Spring 上下文重新整理的過程,這裡實際呼叫的是 AbstractApplicationContext 的 refresh 方法;所以 SpringBoot 也是複用了 Spring 上下文重新整理的過程。

@Override
public void refresh() throws BeansException, IllegalStateException {
    // 加鎖處理
    synchronized (this.startupShutdownMonitor) {
        // 準備重新整理此上下文。主要包括佔位符的替換及驗證所有的 properties
        prepareRefresh();
        // 這裡做了很多事情:
        // 1、讓子類重新整理內部beanFactory ,建立IoC容器(DefaultListableBeanFactory--ConfigurableListableBeanFactory 的實現類)
        // 2、載入解析XML檔案(最終儲存到Document物件中)
        // 3、讀取Document物件,並完成BeanDefinition的載入和註冊工作
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        // 對 beanFactory 進行一些預處理(設定一些公共屬性)
        prepareBeanFactory(beanFactory);

        try {
            // 允許在 AbstractApplicationContext的子類中對 BeanFactory 進行後置處理,postProcessBeanFactory()這個方法是個空實現。
            postProcessBeanFactory(beanFactory);
            // 呼叫 BeanFactoryPostProcessor 後置處理器處理 BeanFactory 例項(BeanDefinition)
            invokeBeanFactoryPostProcessors(beanFactory);
            // 註冊BeanPostProcessor後置處理器,BeanPostProcessors後置處理器用於攔截bean的建立
            // 用於對建立後的bean例項進行處理
            registerBeanPostProcessors(beanFactory);
            // 初始化訊息資源
            initMessageSource();
            //  初始化應用事件廣播器
            initApplicationEventMulticaster();
            // 初始化特殊的bean,這個方法是空實現,讓AbstractApplicationContext的子類重寫
            onRefresh();
            // 註冊監聽器(ApplicationListener)
            registerListeners();
            // 例項化剩餘的單例bean(非懶載入方式), Bean的 IoC、DI 和 AOP 都是發生在此步驟
            finishBeanFactoryInitialization(beanFactory);
            // 完成重新整理
            // 1、釋出 ContextRefreshedEvent 事件
            // 2、處理 LifecycleProcessor
            finishRefresh();
        }
        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                        "cancelling refresh attempt: " + ex);
            }
            // 銷燬已經建立的單例以避免資源懸空。
            destroyBeans();
            // 重置 ”active“ 標記
            cancelRefresh(ex);
            throw ex;
        }
        finally {
            // 重置Spring核心中的常用自檢快取,清空單例bean內快取
            resetCommonCaches();
        }
    }
}
複製程式碼

這個過程涉及到的東西非常多,可擴充套件的點也非常多,包括 BeanFactoryPostProcessor 處理、BeanPostProcessor 處理、LifecycleProcessor 處理已經 釋出 ContextRefreshedEvent 事件等。到這裡容器重新整理已經完成,容器已經 ready,DI 和 AOP 也已經完成。

BeanFactoryPostProcessor 處理

BeanFactoryPostProcessor 可以對我們的 beanFactory 內所有的 beandefinition(未例項化)資料進行修改,這個過程是在 bean 還沒有例項化之前做的。所以在這,我們通過自己去註冊一些 beandefinition ,也可以對 beandefinition 做一些修改。關於 BeanFactoryPostProcessor 的用法在很多框架中都有體現,這裡以 SOFATracer 中修改 Datasource 為例來說明下。

SOFATracer 中為了對有所基於 jdbc 規範的資料來源進行埋點,提供了一個 DataSourceBeanFactoryPostProcessor,用於修改原生 DataSource 來實現一層代理。程式碼詳見:com.alipay.sofa.tracer.boot.datasource.processor.DataSourceBeanFactoryPostProcessor

這裡只看核心程式碼部分,在 postProcessBeanFactory 方法中會根據 Datasource 的型別來建立不同的 DataSourceProxy;建立 DataSourceProxy 的過程就是修改原生 Datasource 的過程。

 private void createDataSourceProxy(ConfigurableListableBeanFactory beanFactory,
                                       String beanName, BeanDefinition originDataSource,
                                       String jdbcUrl)
 
{
    // re-register origin datasource bean
    BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) beanFactory;
    // 先把之前已經存在的 Datasource 的 BeanDefinition 移除
    beanDefinitionRegistry.removeBeanDefinition(beanName);
    boolean isPrimary = originDataSource.isPrimary();
    originDataSource.setPrimary(false);
    // 換個 beanName ,重新註冊到容器中
    beanDefinitionRegistry.registerBeanDefinition(transformDatasourceBeanName(beanName),
        originDataSource);
    // 構建代理的 datasource BeanDefinition,型別為 SmartDataSource
    RootBeanDefinition proxiedBeanDefinition = new RootBeanDefinition(SmartDataSource.class);
    // 設定 BeanDefinition 相關屬性
    proxiedBeanDefinition.setRole(BeanDefinition.ROLE_APPLICATION);
    proxiedBeanDefinition.setPrimary(isPrimary);
    proxiedBeanDefinition.setInitMethodName("init");
    proxiedBeanDefinition.setDependsOn(transformDatasourceBeanName(beanName));
    // 獲取原生 datasource 的屬性值
    MutablePropertyValues originValues = originDataSource.getPropertyValues();
    MutablePropertyValues values = new MutablePropertyValues();
    String appName = environment.getProperty(TRACER_APPNAME_KEY);
    // 修改和新增屬性
    Assert.isTrue(!StringUtils.isBlank(appName), TRACER_APPNAME_KEY + " must be configured!");
    values.add("appName", appName);
    values.add("delegate"new RuntimeBeanReference(transformDatasourceBeanName(beanName)));
    values.add("dbType",
        DataSourceUtils.resolveDbTypeFromUrl(unwrapPropertyValue(originValues.get(jdbcUrl))));
    values.add("database",
        DataSourceUtils.resolveDatabaseFromUrl(unwrapPropertyValue(originValues.get(jdbcUrl))));
    // 將新的 values 設定給代理 BeanDefinition
    proxiedBeanDefinition.setPropertyValues(values);
    // 將代理的 datasource BeanDefinition 註冊到容器中
    beanDefinitionRegistry.registerBeanDefinition(beanName, proxiedBeanDefinition);
 }
複製程式碼

上面這段程式碼就是 BeanFactoryPostProcessor 一種典型的應用場景,就是修改 BeanDefinition。

BeanFactoryPostProcessor 處理過程程式碼比較長,這裡就不在具體分析處理的流程。需要關注的點是:1、BeanFactoryPostProcessor 的作用,它能做哪些事情;2、它是在容器啟動的哪個階段執行的。

registerBeanPostProcessors 的處理過程

registerBeanPostProcessors 是用於註冊 BeanPostProcessor 的。BeanPostProcessor 的作用時機相對於 BeanFactoryPostProcessor 來說要晚一些,BeanFactoryPostProcessor 處理的是 BeanDefinition,Bean 還沒有例項化;BeanPostProcessor 處理的是 Bean,BeanPostProcessor 包括兩個方法,分別用於在 Bean 例項化之前和例項化之後回撥。

開篇有提到,在某些場景下會出現 BeanPostProcessor 不生效。對於 Spring 來說,BeanPostProcessor 本身也會被註冊成一個 Bean,那麼自然就可能會出現,BeanPostProcessor 處理的 bean 在 BeanPostProcessor 本身初始化之前就已經完成了的情況。

registerBeanPostProcessors 大體分為以下幾個部分:

  • 註冊 BeanPostProcessorChecker。(當一個 bean 在 BeanPostProcessor 例項化過程中被建立時,即當一個bean沒有資格被所有 BeanPostProcessor 處理時,它記錄一個資訊訊息)
  • 實現優先排序、排序和其他操作的 BeanPostProcessor 之間進行排序
  • 註冊實現 PriorityOrdered 的 BeanPostProcessor
  • 註冊實現 Ordered 的
  • 註冊所有常規的 BeanPostProcessor
  • 重新註冊所有的內部 BeanPostProcessor
  • 將後處理器註冊為用於檢測內部 bean 的 applicationlistener,將其移動到處理器鏈的末端(用於獲取代理等)。

這裡還是以擴充套件時機為主線,Bean 的 IoC、DI 和 AOP 初始化過程不細究。

LifecycleProcessor 的處理過程

LifecycleProcessor 的處理過程是在 finishRefresh 方法中執行,下面先看下 finishRefresh 方法:

protected void finishRefresh() {
    // 清除上下文級的資源快取(比如掃描的ASM後設資料)。
    clearResourceCaches();
    // 為此上下文初始化 LifecycleProcessor。
    initLifecycleProcessor();
    // 首先將 refresh 傳播到 LifecycleProcessor。
    getLifecycleProcessor().onRefresh();
    // 釋出 ContextRefreshedEvent 事件
    publishEvent(new ContextRefreshedEvent(this));
    // Participate in LiveBeansView MBean, if active.
    LiveBeansView.registerApplicationContext(this);
}
複製程式碼

初始化 initLifecycleProcessor 是從容器中拿到所有的 LifecycleProcessor ,如果業務程式碼中沒有實現 LifecycleProcessor 介面的 bean ,則使用預設的 DefaultLifecycleProcessor。

onRefresh 過程是 最後會呼叫到 Lifecycle 介面的 start 方法。LifeCycle 定義 Spring 容器物件的生命週期,任何 spring 管理物件都可以實現該介面。然後,當 ApplicationContext 本身接收啟動和停止訊號(例如在執行時停止/重啟場景)時,spring 容器將在容器上下文中找出所有實現了 LifeCycle 及其子類介面的類,並一一呼叫它們實現的類。spring 是通過委託給生命週期處理器 LifecycleProcessor 來實現這一點的。Lifecycle 介面定義如下:

public interface Lifecycle {
    /**
     * 啟動當前元件
     * 1、如果元件已經在執行,不應該丟擲異常
     * 2、對於容器,這將把開始訊號傳播到應用的所有元件
     */

    void start();
    /**
     * 通常以同步方式停止該元件,當該方法執行完成後,該元件會被完全停止。當需要非同步停止行為時,考慮實現 SmartLifecycle 和它的 stop
     * (Runnable) 方法變體。注意,此停止通知在銷燬前不能保證到達:在常規關閉時,{@code Lifecycle} bean將首先收到一個停止通知,然後才傳播
     * 常規銷燬回撥;然而,在上下文的生命週期內的熱重新整理或中止的重新整理嘗試上,只呼叫銷燬方法。對於容器,這將把停止訊號傳播到應用的所有元件
     */

    void stop();

    /**
      *  檢查此元件是否正在執行。
      *  1. 只有該方法返回 false 時,start方法才會被執行。
      *  2. 只有該方法返回 true 時,stop(Runnable callback) 或 stop() 方法才會被執行。
      */

    boolean isRunning();
}
複製程式碼

至此,容器重新整理其實已經就完成了。可以看到 Spring 或者 SpringBoot 在整個啟動過程中,有非常多的口子暴露出來,供使用者使用,非常靈活。

異常處理邏輯

與正常流程類似,異常處理流程同樣作為 SpringBoot 生命週期的一個環節,在異常發生時,會通過一些機制來處理收尾過程。異常處理部分 SpringBoot 1.x 版本和 SpringBoot 2.x 版本差異還是比較大的。這裡只分析 SpringBoot 2.x 的處理過程。這裡直接貼一段程式碼:

private void handleRunFailure(ConfigurableApplicationContext context,
            Throwable exception,
            Collection<SpringBootExceptionReporter> exceptionReporters,
            SpringApplicationRunListeners listeners)
 
{
    try {
        try {
            // exitCode
            handleExitCode(context, exception);
            if (listeners != null) {
                // failed
                listeners.failed(context, exception);
            }
        }
        finally {
            // 這裡也是擴充套件的口子
            reportFailure(exceptionReporters, exception);
            if (context != null) {
                context.close();
            }
        }
    }
    catch (Exception ex) {
        logger.warn("Unable to close ApplicationContext", ex);
    }
    ReflectionUtils.rethrowRuntimeException(exception);
}
複製程式碼

上述程式碼片段主要做了以下幾件事:

  • handleExitCode: 這裡會拿到異常的 exitCode,隨後釋出一個 ExitCodeEvent 事件,最後交由 SpringBootExceptionHandler 處理。
  • SpringApplicationRunListeners#failed: 迴圈遍歷呼叫所有 SpringApplicationRunListener 的 failed 方法
  • reportFailure:使用者可以自定義擴充套件 SpringBootExceptionReporter 介面來實現定製化的異常上報邏輯

在 SpringApplicationRunListeners#failed 中,業務產生的異常將直接被丟擲,而不會影響異常處理的主流程。

總結

至此,SpringBoot 啟動的主流程已經全部分析完成了。從擴充套件和擴充套件時機的角度來看,整個過程中,SpringBoot 提供了非常多的擴充套件口子,讓使用者可以在容器啟動的各個階段(無論是啟動,環境準備,容器重新整理等等)做一些定製化的操作。使用者可以利用這些擴充套件介面來修改 bean 、修改環境變數,給使用者極大的空間。

相關文章