SpringBoot執行原理

鄧曉暉發表於2020-12-20


一、執行原理:

每個Spring Boot專案都有一個主程式啟動類,在主程式啟動類中有一個啟動專案的main()方法, 在該方法中通過執行SpringApplication.run()即可啟動整個Spring Boot程式。

Q:

那麼SpringApplication.run()方法到底是如何做到啟動Spring Boot專案的呢?

@SpringBootApplication  //能夠掃描Spring元件並自動配置SpringBoot
public class Springboot01DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot01DemoApplication.class, args);
    }
}

上述是一個SpringBoot的啟動類,進入SpringApplication.run()方法

image-20201215233706067

如圖所示,進入了run方法後,緊接著,呼叫了過載方法,過載方法做了兩件事:

  1. 例項化SpringApplication物件
  2. 呼叫run方法

1. 例項化SpringApplication物件

public SpringApplication(Class<?>... primarySources) {
	this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	//......設定了一些引數....這裡省略,下面是重點
    //......設定了一些引數....這裡省略,下面是重點
    //......設定了一些引數....這裡省略,下面是重點

	//專案啟動類 SpringbootDemoApplication.class設定為屬性儲存起來
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

	//設定應用型別是SERVLET應用(Spring 5之前的傳統MVC應用)還是REACTIVE應用(Spring 5開始出現的WebFlux互動式應用)
	this.webApplicationType = WebApplicationType.deduceFromClasspath();

	// 設定初始化器(Initializer),最後會呼叫這些初始化器
	//所謂的初始化器就是org.springframework.context.ApplicationContextInitializer的實現類,在Spring上下文被重新整理之前進行初始化的操作
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

	// 設定監聽器(Listener)
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

	// 初始化 mainApplicationClass 屬性:用於推斷並設定專案main()方法啟動的主程式啟動類
	this.mainApplicationClass = deduceMainApplicationClass();
}

SpringApplication的構造方法中,首先設定了一些引數,然後做了5件事

1.1 專案啟動類 SpringbootDemoApplication.class設定為屬性儲存起來

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

給這個成員變數賦值,把傳入的primarySources進行轉換,然後賦值,這個primarySources就是我們Springboot啟動類的Main方法中傳入的:
image-20201215234948784

1.2 設定應用型別是SERVLET應用(Spring 5之前的傳統MVC應用)還是REACTIVE應用

this.webApplicationType = WebApplicationType.deduceFromClasspath();

判斷當前的web應用型別是servlet應用還是reactive應用,那麼如何判斷的? 進入.deduceFromClasspath()方法:
image-20201215235441994

  1. 首先判斷類路徑下Reactive相關的class是否存在,如果存在就說明當前應用是內嵌的 Reactive Web 應用。例如說,Spring Webflux 。
  2. 判斷類路徑下是否存在Servlet型別的類。如果不存在,則返回NONE,表示當前應用是非內嵌的 Web 應用
  3. 否則,表示當前應用是內嵌的 Servlet Web 應用。例如說,Spring MVC 。

1.3 設定初始化器(Initializer),最後會呼叫這些初始化器

所謂的初始化器就是org.springframework.context.ApplicationContextInitializer的實現類,在Spring上下文被重新整理之前進行初始化的操作.

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

這裡傳入了一個ApplicationContextInitializer.class

進入getSpringFactoriesInstances()方法(下圖如果看不清請右鍵另存為):
image-20201216001358494

這段程式碼主要做了如下幾件事:

  1. SpringFactoriesLoader.loadFactoryNames(type, classLoader)
    這裡的type就是剛才傳入的,ApplicationContextInitializer.class

  2. loadFactoryNames 呼叫了 loadSpringFactories方法

  3. loadSpringFactories方法做了如下的事:

    Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
    LinkedMultiValueMap result = new LinkedMultiValueMap();
    

    判斷classLoader是否為空,如果不為空載入META-INF下的spring.factories,如上圖所示,根據傳入的引數值(ApplicationContextInitializer.class)的型別,在spring.factories中進行查詢,根據當前傳入的型別找到兩個類,這兩個類就是初始化器:

    org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
    org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
    

得到這兩個類後,把它們存入set去重,然後進行例項化,然後排序,最終返回,到此初始化器已經設定完成了。然後存入List<ApplicationContextInitializer<?>> initializers,等待之後使用

image-20201216002412069

1.4 設定監聽器(Listener)

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

和1.3同理,也是通過呼叫getSpringFactoriesInstances,只不過傳遞的引數發生了改變。變成了ApplicationListener.class ,所以它就是在spring.factories中根據ApplicationListener.class找,然後例項化,然後返回存入Listeners中。

1.5 初始化 mainApplicationClass 屬性

用於推斷並設定專案main()方法啟動的主程式啟動類

this.mainApplicationClass = deduceMainApplicationClass();
	private Class<?> deduceMainApplicationClass() {
		try {
		    // 獲得當前 StackTraceElement 陣列
			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
			// 判斷哪個執行了 main 方法
			for (StackTraceElement stackTraceElement : stackTrace) {
				if ("main".equals(stackTraceElement.getMethodName())) {
					return Class.forName(stackTraceElement.getClassName());
				}
			}
		} catch (ClassNotFoundException ex) {
			// Swallow and continue
		}
		return null;
	}

判斷哪個類執行了main方法,然後返回。

1.6 總結

例項化SpringApplication物件做了哪些事?

  1. 專案啟動類 SpringbootDemoApplication.class設定為屬性儲存起來
  2. 設定應用型別是SERVLET應用(Spring 5之前的傳統MVC應用)還是REACTIVE應用(Spring 5開始出現的WebFlux互動式應用)
  3. 設定初始化器(Initializer),最後會呼叫這些初始化器
  4. 設定監聽器(Listener)
  5. 初始化 mainApplicationClass 屬性:用於推斷並設定專案main()方法啟動的主程式啟動類

2. 呼叫run方法

回憶一下,在SpringBoot啟動類的Main方法中,執行了SpringApplication.run(Main方法所在的當前類.class, args);,這個方法主要做了兩件事:

  • 例項化SpringApplication物件 (已上述)
  • 呼叫run方法

進入run方法:
image-20201216005004395

run方法大體上做了9件比較重要的事。

2.1 獲取並啟動監聽器

SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
//args是啟動Spring應用的命令列引數,該引數可以在Spring應用中被訪問。如:--server.port=9000
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

image-20201216005219434

它其實還是通過getSpringFactoriesInstances()這個方法來獲取,這個方法已經很熟悉了, 1.3,1.4都使用到了,不再贅述。

那麼本步驟就是通過getSpringFactoriesInstances()拿到了一個SpringApplicationRunListeners型別的監聽器,然後呼叫.starting()啟動。

2.2 專案執行環境Environment的預配置

建立並配置當前SpringBoot應用將要使用的Environment,並遍歷呼叫所有的SpringApplicationRunListener的environmentPrepared()方法

ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

configureIgnoreBeanInfo(environment);
// 準備Banner列印器 - 就是啟動Spring Boot的時候列印在console上的ASCII藝術字型
Banner printedBanner = printBanner(environment);

進入prepareEnvironment()方法:
image-20201216010027603

  1. 查詢environment,有就返回,沒有的話建立後返回。
  2. 配置環境
    1. PropertySources:載入執行的配置檔案
    2. Profiles:多環境配置,針對不同的環境,載入不同的配置
  3. listeners環境準備(就是廣播ApplicationEnvironmentPreparedEvent事件)。
  4. 將建立的環境繫結到SpringApplication物件上
  5. 是否是web環境,如果不是就轉換為標準環境
  6. 配置PropertySources對它自己的遞迴依賴
  7. 返回

此時已經拿到了ConfigurableEnvironment 環境物件,然後執行configureIgnoreBeanInfo(environment),使其生效。

2.3 建立Spring容器

context = createApplicationContext();
// 獲得異常報告器 SpringBootExceptionReporter 陣列
//這一步的邏輯和例項化初始化器和監聽器的一樣,
// 都是通過呼叫 getSpringFactoriesInstances 方法來獲取配置的異常類名稱並例項化所有的異常處理類。
exceptionReporters = getSpringFactoriesInstances(
      SpringBootExceptionReporter.class,
      new Class[] { ConfigurableApplicationContext.class }, context);

image-20201216011031124

根據 webApplicationType 型別,獲得 ApplicationContext 型別,這裡建立容器的型別 還是根據webApplicationType進行判斷的,該型別為SERVLET型別,所以會通過反射裝載對應的位元組碼,也就是AnnotationConfigServletWebServerApplicationContext

然後通過getSpringFactoriesInstances()獲得異常報告器。

2.4 Spring容器前置處理

這一步主要是在容器重新整理之前的準備動作。包含一個非常關鍵的操作:將啟動類注入容器,為後續開啟自動化配置奠定基礎。

prepareContext(context, environment, listeners, applicationArguments,
      printedBanner);

image-20201216011633102

這塊會對整個上下文進行一個預處理,比如觸發監聽器的響應事件、載入資源、設定上下文環境等等。

2.5 重新整理容器

refreshContext(context);

image-20201216011805415

  • IOC容器初始化
  • 向JVM執行時註冊一個關機鉤子(函式),在JVM關機時關閉這個上下文,除非它當時已經關閉。(如果jvm變關閉了,當前上下文物件也可以被關閉了)

//TODO refresh方法在springioc章節中會有詳細說明(挖個坑- - )。

2.6 Spring容器後置處理

afterRefresh(context, applicationArguments);

image-20201216012326686

擴充套件介面,設計模式中的模板方法,預設為空實現。
如果有自定義需求,可以重寫該方法。比如列印一些啟動結束log,或者一些其它後置處理。

2.7 發出結束執行的事件通知

listeners.started(context);

image-20201216012516833

2.8 執行Runners執行器

callRunners(context, applicationArguments);

image-20201216012757179

用於呼叫專案中自定義的執行器XxxRunner類,使得在專案啟動完成後立即執行一些特定程式。

Runner 執行器用於在服務啟動時進行一些業務初始化操作,這些操作只在服務啟動後執行一次。

Spring Boot提供了ApplicationRunnerCommandLineRunner兩種服務介面

2.9 釋出應用上下文就緒事件

listeners.running(context);

表示在前面一切初始化啟動都沒有問題的情況下,使用執行監聽器SpringApplicationRunListener持續執行配置好的應用上下文ApplicationContext.

這樣整個Spring Boot專案就正式啟動完成了。

2.10 返回容器

return context;

完成~

總結:

  1. 獲取並啟動監聽器
  2. 專案執行環境Environment的預配置
  3. 建立Spring容器
  4. Spring容器前置處理
  5. 重新整理容器
  6. Spring容器後置處理
  7. 發出結束執行的事件通知
  8. 執行Runners執行器
  9. 釋出應用上下文就緒事件
  10. 返回容器

相關文章