本文分享自華為雲社群《【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 的測試專案。
如上圖中的 DemoApplication
就是我們這裡 Spring Boot 專案的入口類。
同時,我們可以看到 DemoApplication
的 main
方法中,直接呼叫了 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 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 。
點選關注,第一時間瞭解華為雲新鮮技術~