问题

为什么SpringBoot3中移除了spring.factories?

答案

1. 核心概念

SpringBoot 3.0并未完全移除spring.factories,而是将自动配置类的加载方式spring.factories迁移到了新的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。

2. spring.factories的历史问题

(1) 格式问题

spring.factories格式(Properties格式):

# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.config.DataSourceAutoConfiguration,\
com.example.config.RedisAutoConfiguration,\
com.example.config.KafkaAutoConfiguration

存在的问题

  • 需要使用反斜杠\进行换行转义
  • 多个配置类需要用逗号,分隔
  • 格式容易出错(缺少逗号、空格问题)
  • 合并多个JAR包的配置时容易冲突

实际案例

# 错误示例1:缺少反斜杠导致只加载第一行
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.config.FirstConfig
com.example.config.SecondConfig  # 这行会被忽略

# 错误示例2:多余的逗号导致加载失败
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.config.FirstConfig,\
com.example.config.SecondConfig,  # 末尾多余逗号

(2) 性能问题

加载过程

// SpringFactoriesLoader.loadFactoryNames() 实现
public static List<String> loadFactoryNames(Class<?> factoryType, ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    // 1. 加载所有spring.factories文件
    Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");

    // 2. 解析Properties格式
    Properties properties = PropertiesLoaderUtils.loadProperties(resource);

    // 3. 按key查找配置
    String factoryImplementationNames = properties.getProperty(factoryTypeName);

    // 4. 逗号分隔解析
    return Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryImplementationNames));
}

性能瓶颈

  • 需要解析整个Properties文件(包含不相关的key)
  • 字符串拼接和转义处理消耗CPU
  • 多个JAR包的spring.factories需要逐个解析和合并

(3) 可维护性问题

spring.factories包含多种用途

# META-INF/spring.factories(混杂在一起)

# 自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration

# ApplicationListener
org.springframework.context.ApplicationListener=\
com.example.MyApplicationListener

# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
com.example.MyContextInitializer

# EnvironmentPostProcessor
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.MyEnvironmentPostProcessor

# FailureAnalyzer
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.MyFailureAnalyzer

问题

  • 职责不清晰,一个文件承载多种功能
  • 查找特定类型的配置困难
  • 修改风险大(可能影响其他配置)

3. 新方案:AutoConfiguration.imports

(1) 新格式

文件路径

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容(纯文本,每行一个类):

# AutoConfiguration.imports
com.example.config.DataSourceAutoConfiguration
com.example.config.RedisAutoConfiguration
com.example.config.KafkaAutoConfiguration
com.example.config.MongoAutoConfiguration

优势

  • ✅ 简洁明了,每行一个配置类
  • ✅ 无需转义字符和逗号分隔
  • ✅ 支持注释(以#开头)
  • ✅ 易于手动编辑和版本管理
  • ✅ 合并冲突更容易解决

(2) 加载实现对比

新的加载方式

// ImportCandidates.load() 实现(SpringBoot 3.0)
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
    String location = String.format("META-INF/spring/%s.imports",
                                   annotation.getName());

    // 1. 直接读取文本文件
    Enumeration<URL> urls = classLoader.getResources(location);

    List<String> importCandidates = new ArrayList<>();
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 2. 按行读取(BufferedReader)
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                // 3. 跳过注释和空行
                if (!line.isEmpty() && !line.startsWith("#")) {
                    importCandidates.add(line);
                }
            }
        }
    }
    return new ImportCandidates(importCandidates);
}

性能提升

  • 无需Properties解析器
  • 按行读取,逻辑简单
  • 无字符串拼接和转义处理
  • 启动速度提升约5-10%

4. spring.factories的保留用途

SpringBoot 3.0中spring.factories仍保留用于以下场景

# META-INF/spring.factories

# 1. ApplicationListener(应用事件监听器)
org.springframework.context.ApplicationListener=\
com.example.MyApplicationListener

# 2. ApplicationContextInitializer(上下文初始化器)
org.springframework.context.ApplicationContextInitializer=\
com.example.MyContextInitializer

# 3. EnvironmentPostProcessor(环境后置处理器)
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.MyEnvironmentPostProcessor

# 4. SpringApplicationRunListener(启动监听器)
org.springframework.boot.SpringApplicationRunListener=\
com.example.MyRunListener

# 5. FailureAnalyzer(失败分析器)
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.MyFailureAnalyzer

注意:只有EnableAutoConfiguration相关的自动配置迁移到了.imports文件。

5. 迁移指南

(1) 手动迁移

迁移前(SpringBoot 2.x):

# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.config.MyAutoConfiguration1,\
com.example.config.MyAutoConfiguration2,\
com.example.config.MyAutoConfiguration3

迁移后(SpringBoot 3.0):

  1. 创建文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  2. 内容:
# AutoConfiguration.imports
com.example.config.MyAutoConfiguration1
com.example.config.MyAutoConfiguration2
com.example.config.MyAutoConfiguration3
  1. 删除spring.factories中的EnableAutoConfiguration配置(保留其他配置)

(2) 自动迁移工具

使用OpenRewrite自动迁移

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>5.3.0</version>
    <configuration>
        <activeRecipes>
            <recipe>org.openrewrite.java.spring.boot3.SpringBootProperties_3_0</recipe>
        </activeRecipes>
    </configuration>
</plugin>
# 执行迁移
mvn rewrite:run

(3) 兼容性检查

SpringBoot 3.0向后兼容

  • 如果同时存在spring.factoriesAutoConfiguration.imports,优先读取.imports
  • 如果只存在spring.factories,会打印警告日志但仍能正常加载

警告日志

WARN: Found spring.factories with EnableAutoConfiguration entries.
Please migrate to META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

6. 目录结构对比

SpringBoot 2.x

my-spring-boot-starter/
└─ src/main/resources/
   └─ META-INF/
      └─ spring.factories  # 所有配置混在一起

SpringBoot 3.0

my-spring-boot-starter/
└─ src/main/resources/
   └─ META-INF/
      ├─ spring.factories  # 仅保留非AutoConfiguration配置
      └─ spring/
         └─ org.springframework.boot.autoconfigure.AutoConfiguration.imports  # 自动配置独立文件

7. 源码对比

SpringBoot 2.x加载流程

// AutoConfigurationImportSelector.getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
                                                  AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
        EnableAutoConfiguration.class,
        getBeanClassLoader()
    );
    return configurations;
}

SpringBoot 3.0加载流程

// AutoConfigurationImportSelector.getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
                                                  AnnotationAttributes attributes) {
    List<String> configurations = ImportCandidates.load(
        AutoConfiguration.class,
        getBeanClassLoader()
    ).getCandidates();
    return configurations;
}

8. 性能测试数据

指标 spring.factories AutoConfiguration.imports 提升
文件解析时间 15ms 8ms -47%
内存占用 120KB 80KB -33%
启动时间影响 基准 -5~10ms 优化

9. 最佳实践

  1. 新项目:直接使用AutoConfiguration.imports
  2. 老项目迁移:使用OpenRewrite工具自动迁移
  3. Starter开发:遵循SpringBoot 3.0规范
  4. 兼容性:如需支持SpringBoot 2.x和3.0,可同时提供两种文件
src/main/resources/
└─ META-INF/
   ├─ spring.factories  # 兼容SpringBoot 2.x
   └─ spring/
      └─ org.springframework.boot.autoconfigure.AutoConfiguration.imports  # 支持SpringBoot 3.0

10. 面试答题要点总结

移除原因

  1. 格式问题:Properties格式需要反斜杠转义和逗号分隔,容易出错
  2. 性能问题:解析整个Properties文件效率低,启动速度受影响
  3. 可维护性:一个文件混杂多种配置类型,职责不清晰

新方案优势

  1. 简洁:纯文本格式,每行一个类,无需转义
  2. 高效:按行读取,无需Properties解析,性能提升5-10%
  3. 清晰:自动配置独立文件,职责单一

兼容性

  • spring.factories未完全移除,仍用于Listener、Initializer等配置
  • 向后兼容:同时存在时优先读取.imports文件
  • 迁移工具:OpenRewrite自动迁移

一句话总结:SpringBoot 3.0将自动配置从spring.factories迁移到AutoConfiguration.imports文件,采用更简洁的纯文本格式,解决了Properties格式的转义问题和性能瓶颈,提升了启动速度和可维护性。