前言

“面试官问:Spring Boot 是怎么实现自动配置的?你说就是加个 @SpringBootApplication 注解…然后就没有然后了。”

这是很多 Java 开发者面试时的尴尬瞬间。我们天天用 Spring Boot,却很少深究它背后的魔法是怎么实现的。

今天这篇文章,我会从你写的第一行 main 方法开始,一步步追踪 Spring Boot 自动配置的完整链路,让你真正理解"约定优于配置"的精髓。

相关阅读Spring Bean生命周期原来是这样 - 一篇讲透实例化、初始化与销毁

在这里插入图片描述


一、从 main 方法说起

每个 Spring Boot 项目都从这几行代码开始:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

看起来平平无奇,但这里藏着两个关键点:

📌 关键点1:@SpringBootApplication 注解

这个注解是一个"三合一"组合:

@SpringBootConfiguration      // 等同于 @Configuration
@EnableAutoConfiguration      // 🔥 核心!开启自动配置
@ComponentScan                 // 开启组件扫描
public @interface SpringBootApplication {
    // ...
}

📌 关键点2:SpringApplication.run()

这是启动的真正入口,它会完成以下工作:

  • 推断应用类型(Servlet/Reactive)
  • 加载所有 Initializer 和 Listener
  • 创建并刷新 ApplicationContext

二、@EnableAutoConfiguration 的秘密

@EnableAutoConfiguration 是自动配置的灵魂,它的定义如下:

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

🔥 核心机制:@Import(AutoConfigurationImportSelector.class)

这个 AutoConfigurationImportSelector 就是自动配置的"搬运工",它会:

  1. 读取配置文件:扫描 META-INF/spring.factories(Spring Boot 2.x)或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)

  2. 加载候选配置类:把所有 EnableAutoConfiguration 对应的类加载进来

  3. 条件过滤:通过 @Conditional 系列注解决定哪些配置真正生效


三、spring.factories 文件长什么样?

spring-boot-autoconfigure 包为例,META-INF/spring.factories 内容类似:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
# ... 还有100多个

Spring Boot 3.x 改用了新文件:

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

格式也更简洁,每行一个类名:

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

四、AutoConfigurationImportSelector 源码解析

前面说了 AutoConfigurationImportSelector 会读取配置文件,那它具体是怎么读的?

4.1 核心调用链

selectImports()                    // 入口方法
    ↓
getAutoConfigurationEntry()        // 获取自动配置条目
    ↓
getCandidateConfigurations()       // 🔥 加载候选配置类
    ↓
┌─────────────────────────────────────────────────┐
│  Spring Boot 2.x: SpringFactoriesLoader         │
│  Spring Boot 3.x: ImportCandidates              │
└─────────────────────────────────────────────────┘

4.2 入口方法:selectImports()

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 1. 检查自动配置是否开启(默认开启)
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    // 2. 获取自动配置条目
    AutoConfigurationEntry autoConfigurationEntry =
        getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

4.3 核心方法:getAutoConfigurationEntry()

protected AutoConfigurationEntry getAutoConfigurationEntry(
        AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // 1. 获取注解属性(exclude 等)
    AnnotationAttributes attributes = getAttributes(annotationMetadata);

    // 2. 🔥 获取候选配置类(这里加载配置文件)
    List<String> configurations = getCandidateConfigurations(
        annotationMetadata, attributes);

    // 3. 去重
    configurations = removeDuplicates(configurations);

    // 4. 排除指定的配置
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    configurations.removeAll(exclusions);

    // 5. 过滤(根据 @Conditional 条件注解)
    configurations = getConfigurationClassFilter().filter(configurations);

    return new AutoConfigurationEntry(configurations, exclusions);
}

4.4 关键方法:getCandidateConfigurations()

Spring Boot 2.7 及之前版本:

protected List<String> getCandidateConfigurations(
        AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 🔥 使用 SpringFactoriesLoader 加载 spring.factories
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
        getSpringFactoriesLoaderFactoryClass(),  // EnableAutoConfiguration.class
        getBeanClassLoader()
    );
    return configurations;
}

Spring Boot 3.x 版本:

protected List<String> getCandidateConfigurations(
        AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 🔥 使用 ImportCandidates 加载新格式的 imports 文件
    ImportCandidates candidates = ImportCandidates.load(
        AutoConfiguration.class,
        getBeanClassLoader()
    );
    List<String> configurations = new ArrayList<>();
    candidates.forEach(configurations::add);

    // 兼容:仍然加载 spring.factories(向后兼容)
    configurations.addAll(SpringFactoriesLoader.loadFactoryNames(
        EnableAutoConfiguration.class,
        getBeanClassLoader()
    ));
    return configurations;
}

4.5 底层加载:SpringFactoriesLoader

public final class SpringFactoriesLoader {

    // 🔥 固定路径
    public static final String FACTORIES_RESOURCE_LOCATION =
        "META-INF/spring.factories";

    public static List<String> loadFactoryNames(
            Class<?> factoryType, ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        // 加载所有 spring.factories 文件,按 key 查找
        return loadSpringFactories(classLoader)
            .getOrDefault(factoryTypeName, Collections.emptyList());
    }

    private static Map<String, List<String>> loadSpringFactories(
            ClassLoader classLoader) {
        // 扫描所有 jar 包中的 META-INF/spring.factories
        Enumeration<URL> urls = classLoader
            .getResources(FACTORIES_RESOURCE_LOCATION);

        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            // 解析 Properties 格式
            Properties properties = PropertiesLoaderUtils
                .loadProperties(new UrlResource(url));
            // 按 key-value 解析...
        }
    }
}

五、为什么 Spring Boot 3 改用新格式?

5.1 两种格式对比

旧格式(spring.factories):

一个 key 后面挂几十甚至上百个配置类,用反斜杠续行:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
# ... 还有几十个,这里省略
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration

痛点:

  • 一眼看不出有多少个配置类
  • 修改时漏掉逗号或反斜杠就报错
  • Git diff 时改一个类,整行都变红

新格式(AutoConfiguration.imports):

每行一个类名,一目了然:

# 每行一个类名,简洁明了
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration

优势:

  • 有多少个配置类一目了然
  • 改一个类只影响一行
  • Git diff 清晰易读

5.2 解析过程对比

Properties 格式的解析开销:

读取整个文件为 Properties 对象
        ↓
    解析 Key-Value
        ↓
  处理转义字符(\ 换行续行)
        ↓
    按逗号分割 Value
        ↓
  去除每个值的空格
        ↓
   从 Map 中按 Key 查找
        ↓
     得到配置类列表

每行一个类名的解析开销:

    按行读取文件
        ↓
  直接得到类名(跳过空行和注释)
        ↓
     得到配置类列表

5.3 性能差异分析

方面 Properties 格式 每行一个类名
解析复杂度 需要 Properties 解析器处理转义、续行、等号 只需要 readLine()
字符串操作 需要按逗号 split + trim 只需要 trim
内存操作 先构建 Map,再按 Key 查找 直接追加到 List
代码量 ~100 行 ~20 行

直观类比:

想象你要从名单中找人:

  • Properties 格式 = 从一本厚厚的电话簿中,先翻到"自动配置"这一页,再把这一页的 100 个名字按逗号分开,逐个抄下来

  • 每行一个类名 = 直接打开一个名单,每行就是一个名字,直接复制就行

5.4 Spring Boot 3 改版原因

  1. 性能优化:新格式解析更快,启动时间略有提升
  2. 职责分离spring.factories 承载太多功能(自动配置、Initializer、Listener 等),现在自动配置独立出来
  3. 更清晰:一个文件只放一种类型的配置,职责单一
  4. 向后兼容:Spring Boot 3 仍然支持读取 spring.factories,给生态迁移时间

六、条件注解:自动配置的"开关"

不是所有配置都会生效!Spring Boot 用条件注解来智能判断:

注解 作用
@ConditionalOnClass classpath 中存在某个类时生效
@ConditionalOnMissingBean 容器中没有某个 Bean 时生效
@ConditionalOnProperty 配置属性满足条件时生效
@ConditionalOnWebApplication 是 Web 应用时生效

举个例子:DataSourceAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({...})
public class DataSourceAutoConfiguration {
    // 只有当 classpath 里有 DataSource 类,
    // 且没有配置 R2DBC 连接工厂时,
    // 这个自动配置才会生效
}

这就是为什么你引入 mysql-connector-java 依赖后,数据源自动就配好了!


七、启动流程完整链路

把整个过程串起来:

main() 调用 SpringApplication.run()
    ↓
创建 SpringApplication 对象
    ├─ 推断应用类型(Servlet/Reactive)
    ├─ 加载 ApplicationContextInitializer(从 spring.factories)
    └─ 加载 ApplicationListener(从 spring.factories)
    ↓
执行 run() 方法
    ├─ 准备环境(Environment)
    ├─ 打印 Banner
    └─ 创建 ApplicationContext
    ↓
执行 @EnableAutoConfiguration
    ↓
AutoConfigurationImportSelector.selectImports()
    ↓
getAutoConfigurationEntry()
    ↓
getCandidateConfigurations()
    ├─ Spring Boot 2.x: SpringFactoriesLoader → spring.factories
    └─ Spring Boot 3.x: ImportCandidates → AutoConfiguration.imports
    ↓
加载所有候选自动配置类(100+个)
    ↓
getConfigurationClassFilter().filter()
    └─ 根据 @Conditional 条件过滤
    ↓
注册生效的 Bean 到容器
    ↓
启动完成!

八、实战:如何自定义自动配置?

理解原理后,你也可以为自己的组件写自动配置:

Step 1:创建配置类

@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyProperties props) {
        return new MyService(props);
    }
}

Step 2:注册到 spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration

Step 3:打包成 Starter

别人只需引入你的 starter 依赖,配置就自动生效了!


九、总结

Spring Boot 自动配置的完整链路:

1. 入口触发

  • @SpringBootApplication 包含 @EnableAutoConfiguration
  • @Import(AutoConfigurationImportSelector.class) 导入选择器

2. 配置加载

  • Spring Boot 2.x:SpringFactoriesLoader 读取 spring.factories
  • Spring Boot 3.x:ImportCandidates 读取 AutoConfiguration.imports

3. 条件过滤

  • @ConditionalOnClass:类路径存在则生效
  • @ConditionalOnMissingBean:容器中没有则生效
  • @ConditionalOnProperty:配置满足则生效

4. 核心优势

  • 约定优于配置:开箱即用,减少样板代码
  • 按需加载:条件注解确保只加载需要的配置
  • 易于扩展:自定义 Starter 只需遵循规范

掌握了这套机制,你不仅能更好地理解 Spring Boot,还能在面试中从容应对,甚至可以自己写一个优雅的 Starter!


参考资料:

https://javaguide.cn/system-design/framework/spring/spring-boot-auto-assembly-principles.html

https://blog.csdn.net/zuiyuelong/article/details/150500212

https://zhuanlan.zhihu.com/p/301063931


欢迎关注公众号 FishTech Notes,一块交流使用心得!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐