tips:每個 springBoot 的版本不同,代碼的實現存會存在不同。
上一章,我們聊到 mybatis-spring-boot-starter; 簡單分析了它的結構。 這一章我們將著重分析 Starter 的加載機制,并結合源碼進行分析理解。
一、加載實際
1.1 何時被加載
引入的 Starter 何時被加載到 Spring 容器里面。 解決了這個過程,那么 Starter 的加載機制就明白大半了。
1.2 加載時機
給出幾個步驟。(為了突出重點,時序圖中忽略后置處理器等,下一章會給出全局時序圖)
- 應用啟動始于主啟動類(通常使用 @SpringBootApplication 注解)中的main方法,調用SpringApplication.run(...)
- 在自動配置過程中,Spring Boot會使用 SpringFactoriesLoader 類來加載所有可見的 spring.factories 文件。SpringFactoriesLoader 查找類路徑上所有 META-INF/spring.factories 實例
- 通過后置處理器,spring.factories文件中的自動配置類(通過EnableAutoConfiguration指定)將被實例化。這些自動配置類通常使用@Configuration注解,且可能包含@Conditional注解
整個加載過程,比較關鍵的兩個類:
- SpringFactoriesLoader 加載 spring.factories 文件
- EnableAutoConfigurationImportSelector 導入 autoconfiguration 并進行加載
spring.factories 文件是幫助 SpringBoot 項目包以外的bean(即在pom文件中添加依賴中的bean)注冊到SpringBoot 項目的 Spring 容器中。由于 @ComponentScan注解只能掃描 spring-boot 項目包內的 bean 并注冊到 Spring 容器中,因此需要 @EnableAutoConfiguration 注解來注冊項目包外的 bean。而spring.factories文件,則是用來記錄項目包外需要注冊的 bean 類名。
1.3 SPI 機制
SPI(Service Provider Interface)服務提供接口,在Java中是一種發現和加載可插拔實現的標準機制。目的是實現對組件的動態發現和加載
通過java.util.ServiceLoader類來發現和加載META-INF/services/目錄下相應接口的實現類。
關于 Java 本身提供的 SPI 實現細節
在 SpringBoot 中,SPI 機制允許開發人員在模塊中定義一些服務接口,并且可以為這些接口提供多個可插拔的實現。實現了進一步的便利性和更強大的整合特性。 通過定義約定的 spring.factories 文件,來實現自動配置和條件裝配。
接下來,我們通過 debug 的方式來探索 Starter 被加載的過程。
特別說明:本文 debug 的源碼是:mybatis-starter-apply
, 如果需要跟著 debug 走步驟流程, 下載相關源碼。uzong-starter-learning: 學習 SpringBoot Starter 的工程案例
二、源碼理解
在理解整個 Starter 之前,我們先來了解一下幾個核心類。它將是整個解密 Starter 最為關鍵的幾個類
2.1 SpringFactoriesLoader 類
這個類的作用, 解析 META-INF/spring.factories
文件,并將結果方法到一個 Map 中。
spring.factories 的結果和 properties 非常相似,我們可以查看 mybatis-spring-boot-starter
文件中的 spring.factories
注意: spring.factories 中的 value 值是可以用逗號分隔。
使用的分割方法是 org.springframework.util.StringUtils#commaDelimitedListToStringArray
public static String[] commaDelimitedListToStringArray(@Nullable String str) {return delimitedListToStringArray(str, ",");
}
將 spring.factories 中的 key = value 解析后放入到 MultiValueMap<String, String>
。 這是一個特殊的 map。 它的 value 值是一個 List
public interface MultiValueMap<K, V> extends Map<K, List<V>> {.....
}
繼承至 Map<K, List<V>>。
SpringFactoriesLoader 將當前類加載器下所有的 "META-INF/spring.factories" 文件進行解析,并將解析結果放到一個 Map 中進行緩存。注意:目前都是 String, 還不是 class
附上部分 SpringFactoriesLoader 源碼以及注釋。
核心邏輯:加載 META-INF/spring.factories 文件數據; 把里面的key、value值放入到一個特殊的 Map 中。
public abstract class SpringFactoriesLoader {// JAR 文件的路徑public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";// 將所有類路徑的名稱加載到這個 Map 中private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();// 根據類名獲取value值。注意返回的是 listpublic static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {String factoryClassName = factoryClass.getName();return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}// 解析 spring.factories 文件,將解析的 key=value 放入到 cache 中。private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {MultiValueMap<String, String> result = cache.get(classLoader);if (result != null)return result;try {// jar 文件路徑Enumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = new LinkedMultiValueMap<>();// 遍歷所路徑,加載文件資源while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);// 解析成 Properties 對象,即 key = valueProperties properties = PropertiesLoaderUtils.loadProperties(resource);for (Map.Entry<?, ?> entry : properties.entrySet()) {// 逗號分割值List<String> factoryClassNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));result.addAll((String) entry.getKey(), factoryClassNames);}}cache.put(classLoader, result);return result;}......}// 反射,加載對象@SuppressWarnings("unchecked")private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {try {// 反射創建對象Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);if (!factoryClass.isAssignableFrom(instanceClass)) {throw new IllegalArgumentException("Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");}return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();}.....}
}
通過 debug 斷點,查看 cache 中的數據。
以 debug 方式啟動 com.uzong.instance.MyApplication 類,查看 Map 數據。
斷點地址:SpringFactoriesLoader#142 行
可以看到,mybatis-spring-boot-starter中的 auto-configuration 類被加載了。
回到 mybatis-spring-boot-starter 的 spring.factories 確認一下。
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
確認一致。
讀取 spring.factories 抽取了幾個關鍵步驟。如下所示:
補充: idea 中 debug 窗口,為執行棧幀,如下所示。可以清楚的知道運行經過的鏈路。
總結兩個要點:
- cache 包含多種類型,不僅僅是 Starter 中指定的
org.springframework.boot.autoconfigure.EnableAutoConfiguration
類型。 后續通過指定類型獲取 list 值。 - 此處的 Map 中的值還是 String,不是 Class,那么哪一步才能將 String 變成 Class 呢,接下來關注 AutoConfigurationImportSelector
2.2 AutoConfigurationImportSelector 類
org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports
這個方法的作用是將 org.springframework.boot.autoconfigure.EnableAutoConfiguration
對應的 List 值進行加載到 application 中進行類的初始化
相關源碼,主要從 cache map 中讀取 org.springframework.boot.autoconfigure.EnableAutoConfiguration 值。并進行加載
入口:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {.....try {AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);AnnotationAttributes attributes = getAttributes(annotationMetadata);// 加載 org.springframework.boot.autoconfigure.EnableAutoConfigurationList<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);.....return StringUtils.toStringArray(configurations);}......}
在這個類中,會讀取 spring.factories 中的 org.springframework.boot.autoconfigure.EnableAutoConfiguration
值進行加載。
getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());......return configurations;}
返回 org.springframework.boot.autoconfigure.EnableAutoConfiguration
protected Class<?> getSpringFactoriesLoaderFactoryClass() {return EnableAutoConfiguration.class;
}
特別注意:不同版本源碼略有不同;在 selectImports 中, 也對返回的 list 做了過濾。
在 filter() 方法中,過濾一些不滿足的 configuration,不進一步加載。比如:ConditionalOnClass 條件注解。如果找不到依賴的那個類,則直接過濾掉。
·
下面就是基于ConditionalOnClass org.springframework.boot.autoconfigure.condition.OnClassCondition#getOutcomes
規則做過濾。
如果我們引入的 Starter 發現沒有生效,則可以通過斷點到這一行,來排查對應的文件是否被引入。
2.3 加載過程
找到符合的全路徑名稱后,就交給 org.springframework.context.annotation.ConfigurationClassParser
類進行解析。然后把整個 configuration 所在的 Bean 都一一進行加載。
關于 ConfigurationClassParser
是非常重要的,它是 springframework core 中的核心類。以遞歸性的解析加載所有類;也是非常繁瑣和重要的,后面會用單獨章節進行詳細講解。
2.4 整個加載過程
加載過程,包括 ConfigurationClassParser
以及后置處理器也添加進來的時序圖。
時序圖中的幾個關鍵方法,可以關注一下:
關鍵方法一:refreshContext
org.springframework.boot.SpringApplication#prepareContext
關鍵方法二:invokeBeanFactoryPostProcessors。 激活各種后置處理器
org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors
關鍵方法三:processDeferredImportSelectors,處理延遲導入 importSelector 。
org.springframework.context.annotation.ConfigurationClassParser#processDeferredImportSelectors
Spring 的核心擴展接口。 SpringBoot 的 AutoConfigurationImportSelector 類,實現 ImportSelector(DeferredImportSelector)方法,從而實現 Starter 的擴展裝配能力。
三、本章小結
本文只通過局部了解到 Starter 中的類被加載的時機,主要有兩個核心類
- SpringFactoriesLoader
- AutoConfigurationImportSelector
一個是加載 spring.factories 加載資源放入 Map ; 另外一個觸發 Map 中讀取數據并交給 ConfigurationClassParser
解析。
如果僅僅從局部理解加載過程是局限的。接下來,我們從整個 SpringBoot 的加載順序理解全貌。
已同步發布到公眾號:面湯放鹽?第四節 Starter 加載時機和源碼理解 (qq.com)
掘金賬號:第四節 Starter 加載時機和源碼理解 - 掘金 (juejin.cn)