Spring SPI:SpringFactoriesLoader原理与Spring Boot自动装配的关系
Spring SPI:SpringFactoriesLoader原理与Spring Boot自动装配的关系
适读人群:想搞懂Spring Boot"零配置"魔法背后原理的Java开发者 | 阅读时长:约16分钟
开篇故事
我带过一个刚入职的同学,他问我:"老张,我就加了个 spring-boot-starter-data-redis 依赖,什么都没配,为什么 RedisTemplate 就能直接注入了?Spring 是怎么知道要创建这个 Bean 的?"
我说:"你去看看 Redis starter 里有没有一个叫 spring.factories 的文件,再看看里面写了什么。"
他翻了五分钟,回来跟我说:"找到了,里面写了个 RedisAutoConfiguration,但是我不理解 Spring 是怎么读到这个文件的。"
这就是今天要讲的主题:SpringFactoriesLoader —— Spring 自己实现的增强版 SPI 机制,以及它和 Spring Boot 自动装配的完整关系链。
一、SpringFactoriesLoader vs Java SPI
上篇讲了 Java 原生 SPI,先做个对比,看看 Spring 为什么要"另起炉灶":
| 特性 | Java SPI (ServiceLoader) | Spring SPI (SpringFactoriesLoader) |
|---|---|---|
| 配置文件位置 | META-INF/services/接口全限定名 | META-INF/spring.factories |
| 配置格式 | 每个文件只能对应一个接口 | 一个文件,key=value 多类型 |
| key 类型 | 只支持接口 | 任意类,不限于接口 |
| 加载结果 | 实例化后的对象列表 | 类名列表(不自动实例化) |
| 缓存 | 无 | 有(ConcurrentReferenceHashMap) |
| 条件加载 | 不支持 | 配合 @Conditional 支持 |
Spring SPI 的最大优势:一个文件管理所有扩展点,且只返回类名,实例化由 Spring 容器来做,能配合所有 Spring 特性(条件注解、Bean 生命周期等)。
二、SpringFactoriesLoader 源码解析
2.1 完整加载流程
2.2 核心源码
// SpringFactoriesLoader.java(Spring 5.x)
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 使用弱引用缓存,避免类加载器泄漏
private static final Map<ClassLoader, MultiValueMap<String, String>> cache =
new ConcurrentReferenceHashMap<>();
/**
* 加载指定类型的所有实现类名
*/
public static List<String> loadFactoryNames(Class<?> factoryType, ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
// 加载所有factories,然后按key过滤
return loadSpringFactories(classLoaderToUse)
.getOrDefault(factoryTypeName, Collections.emptyList());
}
/**
* 加载所有 spring.factories 文件并解析
*/
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// 先检查缓存
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
// 扫描所有JAR包里的 META-INF/spring.factories
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历所有 key-value,按key合并
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, k -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// 去重
result.replaceAll((factoryType, implementations) ->
implementations.stream().distinct().collect(Collectors.toList()));
// 存入缓存
cache.put(classLoader, result);
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
}2.3 spring.factories 文件格式
# Spring Boot 2.x 使用这个文件
# key = 扩展点类型(通常是接口或父类)
# value = 实现类,多个用逗号分隔,可以用反斜杠换行
# 自动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
# 应用监听器(Spring 启动生命周期)
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener
# 失败分析器(启动失败时给出友好提示)
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer
# 环境后处理器
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor三、Spring Boot 3.x 的变化
Spring Boot 3.x 开始,自动配置类不再从 spring.factories 读取,改用了新的文件格式。
新文件路径:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# Spring Boot 3.x 新格式:每行一个类名,格式更简洁
com.laozhang.sms.SmsAutoConfiguration
com.laozhang.redis.RedisAutoConfiguration
com.laozhang.oss.OssAutoConfiguration为什么要改?
- 安全性:
spring.factories里混放了太多不同类型的扩展点,容易被恶意 JAR 插入不相关内容 - 性能:新格式只加载自动配置类,解析更快
- 清晰度:职责单一,
spring.factories里其他扩展点(如ApplicationListener)继续用老格式
兼容处理:如果你的 Starter 要同时支持 Boot 2.x 和 3.x,两个文件都提供:
src/main/resources/
├── META-INF/
│ ├── spring.factories # Boot 2.x 用
│ └── spring/
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports # Boot 3.x 用四、完整代码实现
4.1 自定义扩展点:应用启动健康检查
利用 SpringFactoriesLoader 机制,实现一个可插拔的启动健康检查框架。
定义扩展点接口:
package com.laozhang.health;
/**
* 启动健康检查接口
* 各组件实现这个接口,在应用启动时自检
*/
public interface StartupHealthChecker {
/**
* 检查名称
*/
String name();
/**
* 执行检查
*
* @return 检查结果
*/
CheckResult check();
/**
* 是否是必须通过的检查(失败时是否阻止启动)
*/
default boolean isCritical() {
return true;
}
record CheckResult(boolean healthy, String message) {
public static CheckResult ok(String message) {
return new CheckResult(true, message);
}
public static CheckResult fail(String message) {
return new CheckResult(false, message);
}
}
}数据库连接检查实现:
package com.laozhang.health.db;
import com.laozhang.health.StartupHealthChecker;
import org.springframework.jdbc.core.JdbcTemplate;
public class DatabaseHealthChecker implements StartupHealthChecker {
private final JdbcTemplate jdbcTemplate;
public DatabaseHealthChecker(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public String name() {
return "数据库连接检查";
}
@Override
public CheckResult check() {
try {
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
return CheckResult.ok("数据库连接正常");
} catch (Exception e) {
return CheckResult.fail("数据库连接失败: " + e.getMessage());
}
}
}spring.factories 注册:
com.laozhang.health.StartupHealthChecker=\
com.laozhang.health.db.DatabaseHealthChecker健康检查运行器(利用 SpringFactoriesLoader 加载所有检查器):
package com.laozhang.health;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
/**
* 在 Spring 容器启动后执行所有健康检查
* 这个类本身也通过 spring.factories 注册
*/
public class StartupHealthCheckRunListener implements SpringApplicationRunListener {
private final SpringApplication application;
// Spring 要求这个构造器签名
public StartupHealthCheckRunListener(SpringApplication application, String[] args) {
this.application = application;
}
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
// 通过 SpringFactoriesLoader 加载所有健康检查器类名
List<String> checkerClassNames = SpringFactoriesLoader.loadFactoryNames(
StartupHealthChecker.class,
context.getClassLoader()
);
boolean allPassed = true;
for (String className : checkerClassNames) {
try {
Class<?> clazz = Class.forName(className, true, context.getClassLoader());
// 尝试从Spring容器获取(享受依赖注入),没有则反射创建
StartupHealthChecker checker;
try {
checker = (StartupHealthChecker) context.getBean(clazz);
} catch (Exception e) {
checker = (StartupHealthChecker) clazz.getDeclaredConstructor().newInstance();
}
StartupHealthChecker.CheckResult result = checker.check();
if (result.healthy()) {
System.out.println("[HealthCheck] PASS - " + checker.name() + ": " + result.message());
} else {
System.err.println("[HealthCheck] FAIL - " + checker.name() + ": " + result.message());
if (checker.isCritical()) {
allPassed = false;
}
}
} catch (Exception e) {
System.err.println("[HealthCheck] ERROR - 加载检查器失败: " + className);
}
}
if (!allPassed) {
System.err.println("[HealthCheck] 关键健康检查未通过,应用将停止启动");
System.exit(1);
}
}
}注册 SpringApplicationRunListener(这个类型也是通过 spring.factories 发现的):
org.springframework.boot.SpringApplicationRunListener=\
com.laozhang.health.StartupHealthCheckRunListener五、踩坑实录
坑1:spring.factories 里同一个 key 出现在多个 JAR 中
疑问:我的 Starter 里的 spring.factories 和 Spring Boot 官方的 spring.factories 都有 EnableAutoConfiguration,会不会冲突?
解答:不会。SpringFactoriesLoader 会扫描所有 JAR 里的 spring.factories,把同一个 key 下的 value 合并,最终返回一个合并后的完整列表。这是它和 Java SPI"后来者覆盖"不同的地方。
坑2:SpringFactoriesLoader 的缓存坑(测试场景)
症状:写单元测试,第一个测试修改了 spring.factories(通过自定义 ClassLoader),第二个测试拿到的还是旧数据。
根因:SpringFactoriesLoader 有一个静态 cache,按 ClassLoader 缓存,测试用的是同一个 ClassLoader。
解决:测试里手动清除缓存,或者用不同的 ClassLoader 隔离:
// 测试结束后清除缓存
@AfterEach
void clearCache() throws Exception {
Field cacheField = SpringFactoriesLoader.class.getDeclaredField("cache");
cacheField.setAccessible(true);
Map<?, ?> cache = (Map<?, ?>) cacheField.get(null);
cache.clear();
}坑3:Spring Boot 3.x 迁移后自动配置消失
症状:项目从 Spring Boot 2.x 升级到 3.x,原来好用的自定义 Starter 突然不生效了。
根因:Spring Boot 3.x 自动配置类不再从 spring.factories 的 EnableAutoConfiguration 读取,改用 AutoConfiguration.imports。
解决:补充新文件,同时保留旧文件做兼容:
# 新增文件
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# 内容示例
com.laozhang.sms.SmsAutoConfiguration坑4:@AutoConfiguration 和 @Configuration 的区别
Spring Boot 3.x 新增了 @AutoConfiguration 注解,它和 @Configuration 的区别很多人不清楚。
主要区别:
// @Configuration:普通配置类,可以被 @ComponentScan 扫描到
// @AutoConfiguration:自动配置类,只通过 imports 文件加载,不被 @ComponentScan 扫描
// 这样可以避免自动配置类被意外扫描到,导致重复注册
@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
public class RedisAutoConfiguration {
// ...
}另一个差异:@AutoConfiguration 内部的 @Bean 方法默认是 lite mode(不走 CGLIB 代理),性能更好,但不能调用同类其他 @Bean 方法来触发 Spring 代理。
五、总结与延伸
SpringFactoriesLoader 是 Spring Boot 自动装配的基石:
- 加载阶段:扫描所有 JAR 里的
spring.factories(或AutoConfiguration.imports),合并所有 key 对应的类名列表 - 筛选阶段:
AutoConfigurationImportSelector过滤@Conditional不满足的类 - 注册阶段:将满足条件的配置类注册到
BeanFactory
和 Java SPI 的本质差异:Spring SPI 只返回类名,把实例化的控制权交给 Spring 容器,这样才能配合条件注解、Bean 覆盖等所有 Spring 特性。
下一篇(457)讲 Dubbo 的 SPI,它在 Spring SPI 的基础上又往前走了一步:支持按名称获取实现、支持自适应扩展、支持依赖注入——可以说是目前 Java 生态里设计最精妙的 SPI 机制。
