技術分享:瞭解 Spring Boot 啟動類 SpringApplication

PONY王發表於2024-07-03

在學習上述 Spring Boot 核心功能的過程中,相信大家可能都會嘗試啟動自己新建的 Spring Boot 的專案,
並 Debug 看看具體的執行過程。本篇開始就將從 Spring Boot 的啟動類 SpringApplication 上入手,
帶領大家瞭解 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 resourceLoader :ResourceLoader 為資源載入的介面,它用於在 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);
}
}

最後,我們來執行 DemoApplication 的 main 方法。

從上圖可以看出,我們的應用依然能正常啟動,並完成自動配置。因此,決定 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 應用型別推斷
我們繼續往下翻看原始碼,這裡呼叫了 WebApplicationType 的 deduceFromClasspath 方法來進行 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 存在,並且 DispatcherServlet 和 ServletContainer 都不存在,則返回型別為 WebApplicationType.REACTIVE。

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

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

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

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 推斷應用入口類
最後一步,呼叫 SpringApplication 的 deduceMainApplicationClass 方法來進行入口類的推斷:

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 。

相關文章