SpringApplication對象是如何構建的? SpringBoot源碼(八)

註:該源碼分析對應SpringBoot版本為2.1.0.RELEASE

本篇接 SpringBoot的啟動流程是怎樣的?SpringBoot源碼(七)

1 溫故而知新

溫故而知新,我們來簡單回顧一下上篇的內容,上一篇我們分析了SpringBoot的啟動流程,現將關鍵步驟再濃縮總結下:

  1. 構建SpringApplication對象,用於啟動SpringBoot;
  2. spring.factories配置文件中加載EventPublishingRunListener對象用於在不同的啟動階段發射不同的生命周期事件;
  3. 準備環境變量,包括系統變量,環境變量,命令行參數及配置文件(比如application.properties)等;
  4. 創建容器ApplicationContext;
  5. 為第4步創建的容器對象做一些初始化工作,準備一些容器屬性值等,同時調用各個ApplicationContextInitializer的初始化方法來執行一些初始化邏輯等;
  6. 刷新容器,這一步至關重要,是重點中的重點,太多複雜邏輯在這裡實現;
  7. 調用ApplicationRunnerCommandLineRunner的run方法,可以實現這兩個接口在容器啟動後來加載一些業務數據等;

在SpringBoot啟動過程中,每個不同的啟動階段會分別發射不同的內置生命周期事件,然後相應的監聽器會監聽這些事件來執行一些初始化邏輯工作比如ConfigFileApplicationListener會監聽onApplicationEnvironmentPreparedEvent事件來加載環境變量等。

2 引言

上篇文章在講解SpringBoot的啟動流程中,我們有看到新建了一個SpringApplication對象用來啟動SpringBoot項目。那麼,我們今天就來看看SpringApplication對象的構建過程,同時講解一下SpringBoot自己實現的SPI機制。

3 SpringApplication對象的構建過程

本小節開始講解SpringApplication對象的構造過程,因為一個對象的構造無非就是在其構造函數里給它的一些成員屬性賦值,很少包含其他額外的業務邏輯(當然有時候我們可能也會在構造函數里開啟一些線程啥的)。那麼,我們先來看下構造SpringApplication對象時需要用到的一些成員屬性哈:

// SpringApplication.java    /**   * SpringBoot的啟動類即包含main函數的主類   */  private Set<Class<?>> primarySources;  /**   * 包含main函數的主類   */  private Class<?> mainApplicationClass;  /**   * 資源加載器   */  private ResourceLoader resourceLoader;  /**   * 應用類型   */  private WebApplicationType webApplicationType;  /**   * 初始化器   */  private List<ApplicationContextInitializer<?>> initializers;  /**   * 監聽器   */  private List<ApplicationListener<?>> listeners;  

可以看到構建SpringApplication對象時主要是給上面代碼中的六個成員屬性賦值,現在我接着來看SpringApplication對象的構造過程。

我們先回到上一篇文章講解的構建SpringApplication對象的代碼處:

// SpringApplication.java    // run方法是一個靜態方法,用於啟動SpringBoot  public static ConfigurableApplicationContext run(Class<?>[] primarySources,  		String[] args) {  	// 構建一個SpringApplication對象,並調用其run方法來啟動  	return new SpringApplication(primarySources).run(args);  }  

跟進SpringApplication的構造函數中:

// SpringApplication.java    public SpringApplication(Class<?>... primarySources) {      // 繼續調用SpringApplication另一個構造函數  	this(null, primarySources);  }  

繼續跟進SpringApplication另一個構造函數:

// SpringApplication.java    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {  	// 【1】給resourceLoader屬性賦值,注意傳入的resourceLoader參數為null  	this.resourceLoader = resourceLoader;  	Assert.notNull(primarySources, "PrimarySources must not be null");  	// 【2】給primarySources屬性賦值,傳入的primarySources其實就是SpringApplication.run(MainApplication.class, args);中的MainApplication.class  	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));  	// 【3】給webApplicationType屬性賦值,根據classpath中存在哪種類型的類來確定是哪種應用類型  	this.webApplicationType = WebApplicationType.deduceFromClasspath();  	// 【4】給initializers屬性賦值,利用SpringBoot自定義的SPI從spring.factories中加載ApplicationContextInitializer接口的實現類並賦值給initializers屬性  	setInitializers((Collection) getSpringFactoriesInstances(  			ApplicationContextInitializer.class));  	// 【5】給listeners屬性賦值,利用SpringBoot自定義的SPI從spring.factories中加載ApplicationListener接口的實現類並賦值給listeners屬性  	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));  	// 【6】給mainApplicationClass屬性賦值,即這裡要推斷哪個類調用了main函數,然後再賦值給mainApplicationClass屬性,用於後面啟動流程中打印一些日誌。  	this.mainApplicationClass = deduceMainApplicationClass();  }  

可以看到構建SpringApplication對象時其實就是給前面講的6個SpringApplication類的成員屬性賦值而已,做一些初始化工作:

  1. resourceLoader屬性賦值resourceLoader屬性,資源加載器,此時傳入的resourceLoader參數為null
  2. primarySources屬性賦值primarySources屬性即SpringApplication.run(MainApplication.class,args);中傳入的MainApplication.class,該類為SpringBoot項目的啟動類,主要通過該類來掃描Configuration類加載bean
  3. webApplicationType屬性賦值webApplicationType屬性,代表應用類型,根據classpath存在的相應Application類來判斷。因為後面要根據webApplicationType來確定創建哪種Environment對象和創建哪種ApplicationContext,詳細分析請見後面的第3.1小節
  4. initializers屬性賦值initializers屬性為List<ApplicationContextInitializer<?>>集合,利用SpringBoot的SPI機制從spring.factories配置文件中加載,後面在初始化容器的時候會應用這些初始化器來執行一些初始化工作。因為SpringBoot自己實現的SPI機制比較重要,因此獨立成一小節來分析,詳細分析請見後面的第4小節
  5. listeners屬性賦值listeners屬性為List<ApplicationListener<?>>集合,同樣利用利用SpringBoot的SPI機制從spring.factories配置文件中加載。因為SpringBoot啟動過程中會在不同的階段發射一些事件,所以這些加載的監聽器們就是來監聽SpringBoot啟動過程中的一些生命周期事件的;
  6. mainApplicationClass屬性賦值mainApplicationClass屬性表示包含main函數的類,即這裡要推斷哪個類調用了main函數,然後把這個類的全限定名賦值給mainApplicationClass屬性,用於後面啟動流程中打印一些日誌,詳細分析見後面的第3.2小節

3.1 推斷項目應用類型

我們接着分析構造SpringApplication對象的第【3】WebApplicationType.deduceFromClasspath();這句代碼:

// WebApplicationType.java    public enum WebApplicationType {          // 普通的應用  	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";  	private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";  	private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";    	static WebApplicationType deduceFromClasspath() {  		// 若classpath中不存在"org.springframework." + "web.servlet.DispatcherServlet"和"org.glassfish.jersey.servlet.ServletContainer"  		// 則返回WebApplicationType.REACTIVE,表明是reactive應用  		if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)  				&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)  				&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {  			return WebApplicationType.REACTIVE;  		}  		// 若{ "javax.servlet.Servlet",  		//       "org.springframework.web.context.ConfigurableWebApplicationContext" }  		// 都不存在在classpath,則說明是不是web應用  		for (String className : SERVLET_INDICATOR_CLASSES) {  			if (!ClassUtils.isPresent(className, null)) {  				return WebApplicationType.NONE;  			}  		}  		// 最終返回普通的web應用  		return WebApplicationType.SERVLET;  	}  }  

如上代碼,根據classpath判斷應用類型,即通過反射加載classpath判斷指定的標誌類存在與否來分別判斷是Reactive應用,Servlet類型的web應用還是普通的應用。

3.2 推斷哪個類調用了main函數

我們先跳過構造SpringApplication對象的第【4】步和第【5】步,先來分析構造SpringApplication對象的第【6】this.mainApplicationClass = deduceMainApplicationClass();這句代碼:

// SpringApplication.java    private Class<?> deduceMainApplicationClass() {  	try {  		// 獲取StackTraceElement對象數組stackTrace,StackTraceElement對象存儲了調用棧相關信息(比如類名,方法名等)  		StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();  		// 遍歷stackTrace數組  		for (StackTraceElement stackTraceElement : stackTrace) {  			// 若stackTraceElement記錄的調用方法名等於main  			if ("main".equals(stackTraceElement.getMethodName())) {  				// 那麼就返回stackTraceElement記錄的類名即包含main函數的類名  				return Class.forName(stackTraceElement.getClassName());  			}  		}  	}  	catch (ClassNotFoundException ex) {  		// Swallow and continue  	}  	return null;  }  

可以看到deduceMainApplicationClass方法的主要作用就是從StackTraceElement調用棧數組中獲取哪個類調用了main方法,然後再返回賦值給mainApplicationClass屬性,然後用於後面啟動流程中打印一些日誌。

4 SpringBoot的SPI機制原理解讀

由於SpringBoot的SPI機制是一個很重要的知識點,因此這裡單獨一小節來分析。我們都知道,SpringBoot沒有使用Java的SPI機制(Java的SPI機制可以看看筆者的Java是如何實現自己的SPI機制的?,真的是乾貨滿滿),而是自定義實現了一套自己的SPI機制。SpringBoot利用自定義實現的SPI機制可以加載初始化器實現類,監聽器實現類和自動配置類等等。如果我們要添加自動配置類或自定義監聽器,那麼我們很重要的一步就是在spring.factories中進行配置,然後才會被SpringBoot加載。

好了,那麼接下來我們就來重點分析下SpringBoot是如何是實現自己的SPI機制的

這裡接第3小節的構造SpringApplication對象的第【4】步和第【5】步代碼,因為第【4】步和第【5】步都是利用SpringBoot的SPI機制來加載擴展實現類,因此這裡只分析第【4】步的setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));這句代碼,看看getSpringFactoriesInstances方法中SpringBoot是如何實現自己的一套SPI來加載ApplicationContextInitializer初始化器接口的擴展實現類的?

// SpringApplication.java    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {      // 繼續調用重載的getSpringFactoriesInstances方法進行加載      return getSpringFactoriesInstances(type, new Class<?>[] {});  }  

繼續跟進重載的getSpringFactoriesInstances方法:

// SpringApplication.java    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,  		Class<?>[] parameterTypes, Object... args) {  	// 【1】獲得類加載器  	ClassLoader classLoader = getClassLoader();  	// Use names and ensure unique to protect against duplicates  	// 【2】將接口類型和類加載器作為參數傳入loadFactoryNames方法,從spring.factories配置文件中進行加載接口實現類  	Set<String> names = new LinkedHashSet<>(  			SpringFactoriesLoader.loadFactoryNames(type, classLoader));  	// 【3】實例化從spring.factories中加載的接口實現類  	List<T> instances = createSpringFactoriesInstances(type, parameterTypes,  			classLoader, args, names);  	// 【4】進行排序  	AnnotationAwareOrderComparator.sort(instances);  	// 【5】返回加載並實例化好的接口實現類  	return instances;  }  

可以看到,SpringBoot自定義實現的SPI機制代碼中最重要的是上面代碼的【1】,【2】,【3】步,這3步下面分別進行重點分析。

4.1 獲得類加載器

還記得Java是如何實現自己的SPI機制的?這篇文章中Java的SPI機制默認是利用線程上下文類加載器去加載擴展類的,那麼,SpringBoot自己實現的SPI機制又是利用哪種類加載器去加載spring.factories配置文件中的擴展實現類呢?

我們直接看第【1】步的ClassLoader classLoader = getClassLoader();這句代碼,先睹為快:

// SpringApplication.java    public ClassLoader getClassLoader() {  	// 前面在構造SpringApplicaiton對象時,傳入的resourceLoader參數是null,因此不會執行if語句裏面的邏輯  	if (this.resourceLoader != null) {  		return this.resourceLoader.getClassLoader();  	}  	// 獲取默認的類加載器  	return ClassUtils.getDefaultClassLoader();  }  

繼續跟進getDefaultClassLoader方法:

// ClassUtils.java    public static ClassLoader getDefaultClassLoader() {  	ClassLoader cl = null;  	try {  	        // 【重點】獲取線程上下文類加載器  		cl = Thread.currentThread().getContextClassLoader();  	}  	catch (Throwable ex) {  		// Cannot access thread context ClassLoader - falling back...  	}  	// 這裡的邏輯不會執行  	if (cl == null) {  		// No thread context class loader -> use class loader of this class.  		cl = ClassUtils.class.getClassLoader();  		if (cl == null) {  			// getClassLoader() returning null indicates the bootstrap ClassLoader  			try {  				cl = ClassLoader.getSystemClassLoader();  			}  			catch (Throwable ex) {  				// Cannot access system ClassLoader - oh well, maybe the caller can live with null...  			}  		}  	}  	// 返回剛才獲取的線程上下文類加載器  	return cl;  }  

可以看到,原來SpringBoot的SPI機制中也是用線程上下文類加載器去加載spring.factories文件中的擴展實現類的!

4.2 加載spring.factories配置文件中的SPI擴展類

我們再來看下第【2】步中的SpringFactoriesLoader.loadFactoryNames(type, classLoader)這句代碼是如何加載spring.factories配置文件中的SPI擴展類的?

// SpringFactoriesLoader.java    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {          // factoryClass即SPI接口,比如ApplicationContextInitializer,EnableAutoConfiguration等接口  	String factoryClassName = factoryClass.getName();  	// 【主線,重點關注】繼續調用loadSpringFactories方法加載SPI擴展類  	return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());  }  

繼續跟進loadSpringFactories方法:

// SpringFactoriesLoader.java    /**   * The location to look for factories.   * <p>Can be present in multiple JAR files.   */  public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {  	// 以classLoader作為鍵先從緩存中取,若能取到則直接返回  	MultiValueMap<String, String> result = cache.get(classLoader);  	if (result != null) {  		return result;  	}  	// 若緩存中無記錄,則去spring.factories配置文件中獲取  	try {  		// 這裡加載所有jar包中包含"MATF-INF/spring.factories"文件的url路徑  		Enumeration<URL> urls = (classLoader != null ?  				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :  				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));  		result = new LinkedMultiValueMap<>();  		// 遍歷urls路徑,將所有spring.factories文件的鍵值對(key:SPI接口類名 value:SPI擴展類名)  		// 加載放到 result集合中  		while (urls.hasMoreElements()) {  			// 取出一條url  			URL url = urls.nextElement();  			// 將url封裝到UrlResource對象中  			UrlResource resource = new UrlResource(url);  			// 利用PropertiesLoaderUtils的loadProperties方法將spring.factories文件鍵值對內容加載進Properties對象中  			Properties properties = PropertiesLoaderUtils.loadProperties(resource);  			// 遍歷剛加載的鍵值對properties對象  			for (Map.Entry<?, ?> entry : properties.entrySet()) {  				// 取出SPI接口名  				String factoryClassName = ((String) entry.getKey()).trim();  				// 遍歷SPI接口名對應的實現類即SPI擴展類  				for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {  					// SPI接口名作為key,SPI擴展類作為value放入result中  					result.add(factoryClassName, factoryName.trim());  				}  			}  		}  		// 以classLoader作為key,result作為value放入cache緩存  		cache.put(classLoader, result);  		// 最終返回result對象  		return result;  	}  	catch (IOException ex) {  		throw new IllegalArgumentException("Unable to load factories from location [" +  				FACTORIES_RESOURCE_LOCATION + "]", ex);  	}  }  

如上代碼,loadSpringFactories方法主要做的事情就是利用之前獲取的線程上下文類加載器將classpath中的所有spring.factories配置文件中所有SPI接口的所有擴展實現類給加載出來,然後放入緩存中。注意,這裡是一次性加載所有的SPI擴展實現類哈,所以之後根據SPI接口就直接從緩存中獲取SPI擴展類了,就不用再次去spring.factories配置文件中獲取SPI接口對應的擴展實現類了。比如之後的獲取ApplicationListener,FailureAnalyzerEnableAutoConfiguration接口的擴展實現類都直接從緩存中獲取即可。

思考1: 這裡為啥要一次性從spring.factories配置文件中獲取所有的擴展類放入緩存中呢?而不是每次都是根據SPI接口去spring.factories配置文件中獲取呢?

思考2: 還記得之前講的SpringBoot的自動配置源碼時提到的AutoConfigurationImportFilter這個接口的作用嗎?現在我們應該能更清楚的理解這個接口的作用了吧。

將所有的SPI擴展實現類加載出來後,此時再調用getOrDefault(factoryClassName, Collections.emptyList())方法根據SPI接口名去篩選當前對應的擴展實現類,比如這裡傳入的factoryClassName參數名為ApplicationContextInitializer接口,那麼這個接口將會作為key從剛才緩存數據中取出ApplicationContextInitializer接口對應的SPI擴展實現類。其中從spring.factories中獲取的ApplicationContextInitializer接口對應的所有SPI擴展實現類如下圖所示:

4.3 實例化從spring.factories中加載的SPI擴展類

前面從spring.factories中獲取到ApplicationContextInitializer接口對應的所有SPI擴展實現類後,此時會將這些SPI擴展類進行實例化。

此時我們再來看下前面的第【3】步的實例化代碼:
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);

// SpringApplication.java    private <T> List<T> createSpringFactoriesInstances(Class<T> type,  		Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,  		Set<String> names) {  	// 新建instances集合,用於存儲稍後實例化後的SPI擴展類對象  	List<T> instances = new ArrayList<>(names.size());  	// 遍歷name集合,names集合存儲了所有SPI擴展類的全限定名  	for (String name : names) {  		try {  			// 根據全限定名利用反射加載類  			Class<?> instanceClass = ClassUtils.forName(name, classLoader);  			// 斷言剛才加載的SPI擴展類是否屬於SPI接口類型  			Assert.isAssignable(type, instanceClass);  			// 獲得SPI擴展類的構造器  			Constructor<?> constructor = instanceClass  					.getDeclaredConstructor(parameterTypes);  			// 實例化SPI擴展類  			T instance = (T) BeanUtils.instantiateClass(constructor, args);  			// 添加進instances集合  			instances.add(instance);  		}  		catch (Throwable ex) {  			throw new IllegalArgumentException(  					"Cannot instantiate " + type + " : " + name, ex);  		}  	}  	// 返回  	return instances;  }  

上面代碼很簡單,主要做的事情就是實例化SPI擴展類。
好了,SpringBoot自定義的SPI機制就已經分析完了。

思考3: SpringBoot為何棄用Java的SPI而自定義了一套SPI?

5 小結

好了,本片就到此結束了,先將前面的知識點再總結下:

  1. 分析了SpringApplication對象的構造過程;
  2. 分析了SpringBoot自己實現的一套SPI機制。

6 有感而發

從自己2月開始寫源碼分析文章以來,也認識了一些技術大牛,從他們身上看到,越厲害的人越努力。回想一下,自己現在知識面也很窄,更重要的是對自己所涉及的技術沒有深度,一句話概括,還很菜,而看到比自己厲害的大牛們都還那麼拼,自己有啥理由不努力呢?很喜歡丁威老師的一句話:"唯有堅持不懈"。然後自己一步一個腳印,相信自己能取得更大的進步,繼續加油。

點贊和轉發是對筆者最大的激勵哦!

由於筆者水平有限,若文中有錯誤還請指出,謝謝。


公眾號【源碼筆記】,專註於Java後端系列框架的源碼分析。