SpringBoot源碼閱讀之spring.factories的加載機(jī)制詳解
spring.factories的加載
提到 SpringBoot 的自動(dòng)裝配,不管是文章還是視頻,都會(huì)提到 spring.factories 這個(gè)文件,這篇文章就來(lái)簡(jiǎn)單講講 spring.factories 的作用,以及它是怎么被加載的
位置
以 SpringBoot 本體為例,spring.factories 在 jar 包的 META-INF 目錄下

其他第三方庫(kù)例如 mybatis-spring-boot-starter 等也都遵循這個(gè)規(guī)則

之所以要放在這個(gè)目錄下,是 SpringBoot 提前約定好的,在 SpringFactoriesLoader 中有這樣一個(gè)常量,項(xiàng)目啟動(dòng)時(shí),掃描的就是這個(gè)路徑:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
類型
.factories 是 SpringBoot 專屬的類型,但從文件的格式來(lái)看,其實(shí)就是一個(gè) .properties 文件
實(shí)際上 SpringBoot 在讀取解析 spring.factories 文件時(shí),用的就是 properties 的解析器
內(nèi)容
截取一部分如下
org.springframework.context.ApplicationListener=\ org.springframework.boot.ClearCachesApplicationListener,\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\ org.springframework.boot.context.FileEncodingApplicationListener,\ org.springframework.boot.context.config.AnsiOutputApplicationListener,\ org.springframework.boot.context.config.DelegatingApplicationListener,\ org.springframework.boot.context.logging.LoggingApplicationListener,\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener
反斜杠 \ 可以換行,并且新的一行依舊屬于上面那行
所以上面截取的這部分信息,實(shí)際只有兩條數(shù)據(jù)
等號(hào) = 左邊的 key 是接口(也可以是注解)的全限定名,等號(hào) = 右邊的 value 是以逗號(hào)分隔的實(shí)現(xiàn)類的全限定名
作用
先講講 SpringBoot 自己的 spring.factories,其中定義了各種接口的實(shí)現(xiàn)類,在 SpringBoot 運(yùn)行的某個(gè)過(guò)程中,會(huì)需要用到一些接口的功能,這時(shí)候就會(huì)從 spring.factories 中獲取對(duì)應(yīng)的實(shí)現(xiàn)類并實(shí)例化來(lái)進(jìn)行調(diào)用
好處是極大地降低了耦合度
當(dāng)版本升級(jí)的時(shí)候,只要修改 spring.factories 中的內(nèi)容,而不用修改代碼,就可以替換實(shí)現(xiàn)類
舉個(gè)例子,SpringBoot 的生命周期中有監(jiān)聽器的參與,同一階段,可能會(huì)觸發(fā)好幾個(gè)監(jiān)聽器的事件,而將來(lái)如果某個(gè)版本的 SpringBoot 要對(duì)“啟動(dòng)完成”這個(gè)階段添加一個(gè)監(jiān)聽器,并作出一些處理,那么只需要寫好這個(gè)新的監(jiān)聽器,然后加入 spring.factories 即可,不用修改 SpringBoot 原本的代碼,并且原本的代碼會(huì)從 spring.factories 讀取到這個(gè)新的監(jiān)聽器并進(jìn)行事件傳播
當(dāng)然,這是只針對(duì) SpringBoot 自身而言的作用
對(duì)用戶,或者第三方插件提供商,它的作用類似于 Java 的 SPI 機(jī)制
SPI 的全名是 Service Provider Interface,大概意思是,JDK 只提供某個(gè)模塊的接口規(guī)范,而具體實(shí)現(xiàn)由廠商來(lái)完成
耳熟能詳?shù)挠?JDBC 模塊、日志模塊等等
以 JDBC 為例,初學(xué) JDBC 時(shí)肯定很熟悉一行代碼:
Class.forName("com.mysql.cj.jdbc.Driver");
我們使用廠商提供的 JDBC 實(shí)現(xiàn),都需要手動(dòng)去注冊(cè)并實(shí)例化
而 spring.factories 相比于 SPI,還提供了服務(wù)發(fā)現(xiàn)的機(jī)制,那就是我們只需要導(dǎo)入第三方庫(kù)的 starter,SpringBoot 就能自動(dòng)掃描并且?guī)椭覀冏?cè)為 bean,相信只要熟悉 SpringBoot,就不用我多講了
那么作為第三方庫(kù)的開發(fā)者需要做些什么呢
除了在 META-INF 目錄下留存一個(gè) spring.factories 文件外,在文件內(nèi)容中,還要加上其自身的自動(dòng)配置類
以 mybatis 為例:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\ org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
key 是 SpringBoot 的 @EnableAutoConfiguration 注解,這個(gè)注解就是自動(dòng)裝配的關(guān)鍵,這篇文章的內(nèi)容注重于 spring.factories 的加載,關(guān)于自動(dòng)裝配將寫在下一篇文章
加載
從啟動(dòng)類的 run 方法進(jìn)入,我們第一次見到與 factories 相關(guān)的代碼是在 SpringApplication 的構(gòu)造方法
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 下面三行都調(diào)用了 getSpringFactoriesInstances 方法,從方法名可以大致理解到作用是獲取 factories 文件中某個(gè)接口的實(shí)現(xiàn)類
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// loadFactoryNames 方法獲取 spring.factories 定義的對(duì)應(yīng)接口的所有實(shí)現(xiàn)類的全限定名
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 根據(jù)全限定名,和參數(shù)類型,實(shí)例化上面獲取到的那些類
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 根據(jù) @Order 注解排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
SpringFactoriesLoader#loadFactoryNames
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
// loadSpringFactories 是 spring.factories 加載的核心方法
// 返回值是一個(gè) Map,包含 spring.factories 所有的鍵值對(duì),key 是接口名,value 是實(shí)現(xiàn)類的名字?jǐn)?shù)組
// 這里獲取到所有鍵值對(duì)后,根據(jù)需要的接口名稱,獲取到相應(yīng)的實(shí)現(xiàn)數(shù)組
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
SpringFactoriesLoader#loadSpringFactories
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// cache 是這個(gè)類的靜態(tài)變量,所以是唯一的
Map<String, List<String>> result = cache.get(classLoader);
// 如果緩存中已經(jīng)有了,那么直接返回
if (result != null) {
return result;
}
result = new HashMap<>();
try {
// 使用 classLoader 從 classpath 讀取文件
// 這個(gè)地址常量在上面已經(jīng)貼出來(lái)過(guò)了,也就是 META-INF/spring.factories
// 如果引用了第三方庫(kù),那么就會(huì)獲取到很多 url
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
// 所以要循環(huán)遍歷每一個(gè)庫(kù)里的 spring.factories 文件
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 用 properties 解析器讀取文件
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 下面的內(nèi)容就是把文件中的每一行數(shù)據(jù),變成 result 數(shù)據(jù),也就是 key(文件#String) 變成 key(Map#String)
// value(文件#String) 變成 value(Map#List<String>)
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
// 這個(gè) commaDelimitedListToStringArray 就是以逗號(hào)為分隔符,解析成字符串?dāng)?shù)組
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList變成
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
這個(gè)方法執(zhí)行成功后,spring.factories 文件就已經(jīng)被加載進(jìn)來(lái),并存放在 SpringFactoriesLoader 的緩存中
在 SpringBoot 啟動(dòng)流程的后續(xù)步驟中,也會(huì)多次從這個(gè)緩存獲取相關(guān)數(shù)據(jù)
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot實(shí)現(xiàn)Read Through模式的操作過(guò)程
Read Through模式通常是指一種緩存策略,其中當(dāng)應(yīng)用程序嘗試讀取數(shù)據(jù)時(shí),緩存系統(tǒng)首先被檢查以查看數(shù)據(jù)是否已經(jīng)存在于緩存中,這篇文章主要介紹了SpringBoot實(shí)現(xiàn)Read Through模式,需要的朋友可以參考下2024-07-07
說(shuō)說(shuō)Java異步調(diào)用的幾種方式
本文主要介紹了說(shuō)說(shuō)Java異步調(diào)用的幾種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
SpringBoot實(shí)現(xiàn)方法級(jí)別的環(huán)境隔離的幾種方式
在 Spring Boot 中實(shí)現(xiàn)環(huán)境隔離和多環(huán)境配置管理是項(xiàng)目部署和維護(hù)中的關(guān)鍵部分,通過(guò)靈活的配置機(jī)制,開發(fā)者可以輕松應(yīng)對(duì)開發(fā)、測(cè)試和生產(chǎn)等不同環(huán)境的需求,以下是實(shí)現(xiàn)這些目標(biāo)的核心方法和最佳實(shí)踐,需要的朋友可以參考下2025-07-07

