一文了解Spring Boot啟動類SpringApplication

华为云开发者联盟發表於2024-07-03

本文分享自華為雲社群《【Spring Boot 原始碼學習】初識 SpringApplication》,作者: Huazie。

引言

往期的博文,Huazie 圍繞 Spring Boot 的核心功能,帶大家從總整體上了解 Spring Boot 自動配置的原理以及自動配置核心元件的運作過程。這些內容大家需要重點關注,只有瞭解這些基礎的元件和功能,我們在後續整合其他三方類庫的 Starters 時,才能夠更加清晰地瞭解它們都運用了自動配置的哪些功能。

在學習上述 Spring Boot 核心功能的過程中,相信大家可能都會嘗試啟動自己新建的 Spring Boot 的專案,並 Debug 看看具體的執行過程。本篇開始就將從 Spring Boot 的啟動類 SpringApplication 上入手,帶領大家瞭解 Spring Boot 啟動過程中所涉及到的原始碼和知識點。

主要內容

1. Spring Boot 應用程式的啟動

在 《【Spring Boot 原始碼學習】@SpringBootApplication 註解》這篇博文中,我們新建了一個基於 Spring Boot 的測試專案。

image.png

如上圖中的 DemoApplication 就是我們這裡 Spring Boot 專案的入口類。

同時,我們可以看到 DemoApplicationmain 方法中,直接呼叫了 SpringApplication 的靜態方法 run,用於啟動整個 Spring Boot 專案。

先來看看 run 方法的原始碼:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

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

閱讀上述 run 方法,我們可以看到實際上是 new 了一個SpringApplication 物件【其構造引數 primarySources 為載入的主要資源類,通常就是 SpringBoot 的入口類】,並呼叫其 run 方法【其引數 args 為傳遞給應用程式的引數資訊】啟動,然後返回一個應用上下文物件 ConfigurableApplicationContext

透過觀察這個內部的 run 方法實現,我們也可以在自己的 Spring Boot 啟動入口類中,像如下這樣去寫 :

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(DemoApplication.class);
        // 這裡可以呼叫 SpringApplication 提供的 setXX 或 addXX 方法來定製化設定
        springApplication.run(args);
    }

}

2. SpringApplication 的例項化

上面已經看到我們在例項化 SpringApplication 了,廢話不多說,直接翻看其原始碼【Spring Boot 2.7.9】:

    public SpringApplication(Class<?>... primarySources) {
        this(null, primarySources);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        // 推斷web應用型別
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        // 載入並初始化 BootstrapRegistryInitializer及其實現類
        this.bootstrapRegistryInitializers = new ArrayList<>(
                getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
        // 載入並初始化 ApplicationContextInitializer及其實現類
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 載入並初始化ApplicationListener及其實現類
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        // 推斷入口類
        this.mainApplicationClass = deduceMainApplicationClass();
    }

由上可知,SpringApplication 提供了兩個構造方法,而其核心的邏輯都在第二個構造方法中實現。

2.1 構造方法引數

我們從上述原始碼可知,SpringApplication 的第二個構造方法有兩個引數,分別是:

ResourceLoader resourceLoaderResourceLoader 為資源載入的介面,它用於在Spring Boot 啟動時列印對應的 banner 資訊,預設採用的就是 DefaultResourceLoader。實操過程中,如果未按照 Spring Boot 的 “約定” 將 banner 的內容放置於 classpath 下,或者檔名不是 banner.* 格式,預設資源載入器是無法載入到對應的 banner 資訊的,此時則可透過 ResourceLoader 來指定需要載入的檔案路徑【這個後面我們專門來實操一下,敬請期待】。

Class<?>... primarySources :主要的 bean 來源,該引數為可變引數,預設我們會傳入 Spring Boot 的入口類【即 main 方法所在的類】,如上面我們的 DemoApplication 。如果作為專案的引導類,該類需要滿足一個條件,就是被註解 @EnableAutoConfiguration 或其組合註解標註。在前面的《【Spring Boot 原始碼學習】@SpringBootApplication 註解》博文中,我們已經知道 @SpringBootApplication 註解中包含了 @EnableAutoConfiguration 註解,因此被 @SpringBootApplication 註解標註的類也可作為引數傳入。當然,primarySources 也可傳入其他普通類,但只有傳入被@EnableAutoConfiguration 標註的類才能夠開啟 Spring Boot 的自動配置。

有些朋友,可能對 primarySources 這個可變引數的描述有點疑惑,下面我們就用例項來演示以其他引導類為入口類進行 Spring Boot 專案啟動:

首先,我們在入口類 DemoApplication 的同級目錄建立一個SecondApplication類,使用 @SpringBootApplication 進行註解。

@SpringBootApplication
public class SecondApplication {
}

然後,將 DemoApplication 修改成如下:

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

最後,我們來執行 DemoApplicationmain 方法。

image.png

從上圖可以看出,我們的應用依然能正常啟動,並完成自動配置。因此,決定 Spring Boot 啟動的入口類並不是一定是 main 方法所在類,而是直接或間接被 @EnableAutoConfiguration 標註的類。

翻看 SpringApplication 的原始碼,我們在其中還能看到它提供了追加 primarySources 的方法,如下所示:

public void addPrimarySources(Collection<Class<?>> additionalPrimarySources) {
    this.primarySources.addAll(additionalPrimarySources);
}

如果採用 1 中最後的方式啟動 Spring Boot ,我們就可以呼叫 addPrimarySources 方法來追加額外的 primarySources

我們繼續回到 SpringApplication 的構造方法裡,可以看到如下的程式碼:

this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

上述這裡將 primarySources 引數轉換為 LinkedHashSet 集合,並賦值給SpringApplication 的私有成員變數 Set<Class<?>> primarySources

知識點: LinkedHashSet 是 Java 集合框架中的類,它繼承自 HashSet,因此具有雜湊表的查詢效能。這是一個同時使用連結串列和雜湊表特性的資料結構,其中連結串列用於維護元素的插入順序。也即是說,當你向 LinkedHashSet 新增元素時,元素將按照新增的順序被儲存,並且能夠被遍歷輸出。

此外,LinkedHashSet 還確保了 元素的唯一性,即重複的元素在集合中只會存在一份。

如果需要頻繁遍歷集合,那麼 LinkedHashSet 可能會比 HashSet 效率更高,因為其透過維護一個雙向連結串列來記錄元素的新增順序,從而支援按照插入順序排序的迭代。但需要注意的是,LinkedHashSet 是非執行緒安全的,如果有多個執行緒同時訪問該集合容器,可能會引發併發問題。

2.2 Web 應用型別推斷

我們繼續往下翻看原始碼,這裡呼叫了 WebApplicationTypededuceFromClasspath 方法來進行 Web 應用型別的推斷。

this.webApplicationType = WebApplicationType.deduceFromClasspath();

我們繼續翻看 WebApplicationType 的原始碼:

public enum WebApplicationType {
    // 非Web應用型別
    NONE,
    // 基於Servlet的Web應用型別
    SERVLET,
    // 基於reactive的Web應用型別
    REACTIVE;

    private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };

    private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

    private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

    private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

    static WebApplicationType deduceFromClasspath() {
        if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
            return WebApplicationType.REACTIVE;
        }
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        return WebApplicationType.SERVLET;
    }
}

WebApplicationType 是一個定義了可能的Web應用型別的列舉類,該列舉類中包含了三塊邏輯:

列舉型別 :非 Web 應用、基於 Servlet 的 Web 應用和基於 reactive 的 Web 應用。

用於下面推斷的常量

推斷型別的方法 deduceFromClasspath :

DispatcherHandler 存在,並且 DispatcherServletServletContainer 都不存在,則返回型別為 WebApplicationType.REACTIVE

ServletConfigurableWebApplicationContext 任何一個不存在時,則說明當前應用為非 Web 應用,返回 WebApplicationType.NONE

當應用不為 reactive Web 應用,並且 ServletConfigurableWebApplicationContext 都存在的情況下,則返回 WebApplicationType.SERVLET

在上述的 deduceFromClasspath 方法中,我們可以看到,在判斷的過程中使用到了 ClassUtilsisPresent 方法。該工具類方法就是透過反射建立指定的類,根據在建立過程中是否丟擲異常來判斷該類是否存在。

image.png

2.3 載入 BootstrapRegistryInitializer

this.bootstrapRegistryInitializers = new ArrayList<>(
        getSpringFactoriesInstances(BootstrapRegistryInitializer.class));

上述邏輯用於載入並初始化 BootstrapRegistryInitializer 及其相關的類。

BootstrapRegistryInitializer 是 Spring Cloud Config 的元件之一,它的作用是在應用程式啟動時初始化 Spring Cloud Config 客戶端。

在 Spring Cloud Config 中,客戶端透過向配置中心(Config Server)傳送請求來獲取應用程式的配置資訊。而 BootstrapRegistryInitializer 就是負責將配置中心的相關資訊註冊到 Spring 容器中的。

由於篇幅有限,有關 BootstrapRegistryInitializer 更詳細的內容,筆者後續專門講解。

2.4 載入 ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

上述程式碼用於載入並初始化 ApplicationContextInitializer 及其相關的類。

ApplicationContextInitializer 是 Spring 框架中的一個介面,它的主要作用是在Spring容器重新整理之前初始化 ConfigurableApplicationContext。這個介面的實現類可以被視為回撥函式,它們的 onApplicationEvent 方法會在Spring 容器啟動時被自動呼叫,從而允許開發人員在容器重新整理之前執行一些自定義的操作。

例如,我們可能需要在這個時刻載入一些配置資訊,或者對某些 bean 進行預處理等。透過實現 ApplicationContextInitializer 介面並重寫其onApplicationEvent 方法,就可以完成這些定製化的需求。

由於篇幅有限,有關 ApplicationContextInitializer 更詳細的內容,筆者後續專門講解。

2.5 載入 ApplicationListener

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

上述程式碼用於載入並初始化 ApplicationListener 及其相關的類。

ApplicationListener 是 Spring 框架提供的一個事件監聽機制,它是Spring 應用內部的事件驅動機制,通常被用於監控應用內部的執行狀況。其實現的原理是 觀察者設計模式,該設計模式的初衷是為了實現系統業務邏輯之間的解耦,從而提升系統的可擴充套件性和可維護性。

我們可以透過自定義一個類來實現 ApplicationListener 介面,然後在這個類中定義需要監聽的事件處理方法。當被監聽的事件發生時,Spring 會自動呼叫這個方法來處理事件。例如,在一個 Spring Boot 專案中,我們可能想要在容器啟動時執行一些特定的操作,如載入配置等,就可以透過實現 ApplicationListener 介面來完成。

由於篇幅有限,有關 ApplicationListener 更詳細的內容,筆者後續專門講解。

2.6 推斷應用入口類

最後一步,呼叫 SpringApplicationdeduceMainApplicationClass 方法來進行入口類的推斷:

private Class<?> deduceMainApplicationClass() {
    try {
        StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
        for (StackTraceElement stackTraceElement : stackTrace) {
            if ("main".equals(stackTraceElement.getMethodName())) {
                return Class.forName(stackTraceElement.getClassName());
            }
        }
    } catch (ClassNotFoundException ex) {
        // 這裡捕獲異常,並繼續執行後續邏輯
    }
    return null;
}

上述程式碼的思路就是:

  • 首先,建立一個執行時異常,並獲得其堆疊陣列。
  • 接著,遍歷陣列,判斷類的方法中是否包含 main 方法。第一個被匹配的類會透過Class.forName 方法建立物件,並將其被返回。
  • 最後,將上述建立的 Class 物件賦值給 SpringApplication 的成員變數 mainApplicationClass

總結

本篇 Huazie 帶大家初步瞭解了 SpringApplication 的例項化過程,當然由於篇幅受限,還有些內容暫時無法詳解,Huazie 將在後續的博文中繼續深入分析。

只有瞭解 Spring Boot 在啟動時都做了些什麼,我們才能在後續的實踐的過程中更好地理解其執行機制,以便遇到問題能更快地定位和排查,使我們應用能夠更容易、更方便地接入 Spring Boot 。

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章