springboot載入過程_SpringApplication物件是如何構建的? SpringBoot原始碼(八)
技術標籤:springboot載入過程springboot啟動流程springboot啟動類springboot啟動過程springboot監聽器
注:該原始碼分析對應SpringBoot版本為2.1.0.RELEASE
本篇接 SpringBoot的啟動流程是怎樣的?SpringBoot原始碼(七)
1 溫故而知新
溫故而知新,我們來簡單回顧一下上篇的內容,上一篇我們分析了SpringBoot的啟動流程,現將關鍵步驟再濃縮總結下:
- 構建SpringApplication物件,用於啟動SpringBoot;
- 從spring.factories配置檔案中載入EventPublishingRunListener物件用於在不同的啟動階段發射不同的生命週期事件;
- 準備環境變數,包括系統變數,環境變數,命令列引數及配置檔案(比如application.properties)等;
- 建立容器ApplicationContext;
- 為第4步建立的容器物件做一些初始化工作,準備一些容器屬性值等,同時呼叫各個ApplicationContextInitializer的初始化方法來執行一些初始化邏輯等;
- 重新整理容器,這一步至關重要,是重點中的重點,太多複雜邏輯在這裡實現;
- 呼叫ApplicationRunner和CommandLineRunner的run方法,可以實現這兩個介面在容器啟動後來載入一些業務資料等;
在SpringBoot啟動過程中,每個不同的啟動階段會分別發射不同的內建生命週期事件,然後相應的監聽器會監聽這些事件來執行一些初始化邏輯工作比如ConfigFileApplicationListener會監聽onApplicationEnvironmentPreparedEvent事件來載入環境變數等。
2 引言
上篇文章在講解SpringBoot的啟動流程中,我們有看到新建了一個SpringApplication物件用來啟動SpringBoot專案。那麼,我們今天就來看看SpringApplication物件的構建過程,同時講解一下SpringBoot自己實現的SPI機制。
3 SpringApplication物件的構建過程
本小節開始講解SpringApplication物件的構造過程,因為一個物件的構造無非就是在其建構函式裡給它的一些成員屬性賦值,很少包含其他額外的業務邏輯(當然有時候我們可能也會在建構函式裡開啟一些執行緒啥的)。那麼,我們先來看下構造SpringApplication物件時需要用到的一些成員屬性哈:
// SpringApplication.java/** * SpringBoot的啟動類即包含main函式的主類 */private Set> primarySources;/** * 包含main函式的主類 */private Class> mainApplicationClass;/** * 資源載入器 */private ResourceLoader resourceLoader;/** * 應用型別 */private WebApplicationType webApplicationType;/** * 初始化器 */private List> initializers;/** * 監聽器 */private List> listeners;
可以看到構建SpringApplication物件時主要是給上面程式碼中的六個成員屬性賦值,現在我接著來看SpringApplication物件的構造過程。
我們先回到上一篇文章講解的構建SpringApplication物件的程式碼處:
// SpringApplication.java// run方法是一個靜態方法,用於啟動SpringBootpublic static ConfigurableApplicationContext run(Class>[] primarySources, String[] args) { // 構建一個SpringApplication物件,並呼叫其run方法來啟動 return new SpringApplication(primarySources).run(args);}
跟進SpringApplication的建構函式中:
// SpringApplication.javapublic SpringApplication(Class>... primarySources) { // 繼續呼叫SpringApplication另一個建構函式 this(null, primarySources);}
繼續跟進SpringApplication另一個建構函式:
// SpringApplication.javapublic 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類的成員屬性賦值而已,做一些初始化工作:
- 給resourceLoader屬性賦值,resourceLoader屬性,資源載入器,此時傳入的resourceLoader引數為null;
- 給primarySources屬性賦值,primarySources屬性即SpringApplication.run(MainApplication.class,args);中傳入的MainApplication.class,該類為SpringBoot專案的啟動類,主要通過該類來掃描Configuration類載入bean;
- 給webApplicationType屬性賦值,webApplicationType屬性,代表應用型別,根據classpath存在的相應Application類來判斷。因為後面要根據webApplicationType來確定建立哪種Environment物件和建立哪種ApplicationContext,詳細分析請見後面的第3.1小節;
- 給initializers屬性賦值,initializers屬性為List>集合,利用SpringBoot的SPI機制從spring.factories配置檔案中載入,後面在初始化容器的時候會應用這些初始化器來執行一些初始化工作。因為SpringBoot自己實現的SPI機制比較重要,因此獨立成一小節來分析,詳細分析請見後面的第4小節;
- 給listeners屬性賦值,listeners屬性為List>集合,同樣利用利用SpringBoot的SPI機制從spring.factories配置檔案中載入。因為SpringBoot啟動過程中會在不同的階段發射一些事件,所以這些載入的監聽器們就是來監聽SpringBoot啟動過程中的一些生命週期事件的;
- 給mainApplicationClass屬性賦值,mainApplicationClass屬性表示包含main函式的類,即這裡要推斷哪個類呼叫了main函式,然後把這個類的全限定名賦值給mainApplicationClass屬性,用於後面啟動流程中列印一些日誌,詳細分析見後面的第3.2小節。
3.1 推斷專案應用型別
我們接著分析構造SpringApplication物件的第【3】步WebApplicationType.deduceFromClasspath();這句程式碼:
// WebApplicationType.javapublic 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.javaprivate 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.javaprivate Collection getSpringFactoriesInstances(Class type) { // 繼續呼叫過載的getSpringFactoriesInstances方法進行載入 return getSpringFactoriesInstances(type, new Class>[] {});}
繼續跟進過載的getSpringFactoriesInstances方法:
// SpringApplication.javaprivate Collection getSpringFactoriesInstances(Class type, Class>[] parameterTypes, Object... args) { // 【1】獲得類載入器 ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates // 【2】將介面型別和類載入器作為引數傳入loadFactoryNames方法,從spring.factories配置檔案中進行載入介面實現類 Set names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader)); // 【3】例項化從spring.factories中載入的介面實現類 List 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.javapublic ClassLoader getClassLoader() { // 前面在構造SpringApplicaiton物件時,傳入的resourceLoader引數是null,因此不會執行if語句裡面的邏輯 if (this.resourceLoader != null) { return this.resourceLoader.getClassLoader(); } // 獲取預設的類載入器 return ClassUtils.getDefaultClassLoader();}
繼續跟進getDefaultClassLoader方法:
// ClassUtils.javapublic 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.javapublic static List 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. *
Can be present in multiple JAR files. */public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) { // 以classLoader作為鍵先從快取中取,若能取到則直接返回 MultiValueMap result = cache.get(classLoader); if (result != null) { return result; } // 若快取中無記錄,則去spring.factories配置檔案中獲取 try { // 這裡載入所有jar包中包含"MATF-INF/spring.factories"檔案的url路徑 Enumeration 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,FailureAnalyzer和EnableAutoConfiguration介面的擴充套件實現類都直接從快取中獲取即可。
思考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 instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);`。
// SpringApplication.javaprivate List createSpringFactoriesInstances(Class type, Class>[] parameterTypes, ClassLoader classLoader, Object[] args, Set names) { // 新建instances集合,用於儲存稍後例項化後的SPI擴充套件類物件 List 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 小結
好了,本片就到此結束了,先將前面的知識點再總結下:
- 分析了SpringApplication物件的構造過程;
- 分析了SpringBoot自己實現的一套SPI機制。
6 有感而發
從自己2月開始寫原始碼分析文章以來,也認識了一些技術大牛,從他們身上看到,越厲害的人越努力。回想一下,自己現在知識面也很窄,更重要的是對自己所涉及的技術沒有深度,一句話概括,還很菜,而看到比自己厲害的大牛們都還那麼拼,自己有啥理由不努力呢?很喜歡丁威老師的一句話:"唯有堅持不懈"。然後自己一步一個腳印,相信自己能取得更大的進步,繼續加油。
點贊和轉發是對筆者最大的激勵哦!
由於筆者水平有限,若文中有錯誤還請指出,謝謝。