Spring Boot自动配置原理:从@EnableAutoConfiguration源码一步步看懂
本文从启动类上的
@EnableAutoConfiguration注解入手,跟踪源码调用链,搞清楚spring.factories加载机制、条件装配原理,最后用一个自定义 Starter 实操验证。全程贴代码 + 踩坑记录,看完就能自己写一个 Starter。
一、问题引入
刚学 Spring 的时候,搭一个 SSM 项目要写一堆 XML:web.xml、applicationContext.xml、springmvc.xml......配错一个标签启动就报错,光配文件就能搞一下午。
后来换 Spring Boot,一个 main 方法启动就完了,当时觉得"这什么魔法"。
直到有一天面试官问:"你说说 Spring Boot 自动配置是怎么实现的?" ------我愣住了。
用 Spring Boot 这么久,只知道 @SpringBootApplication 往类上一贴就能跑,但从没想过它底层到底做了什么。这篇文章就把这个问题彻底搞清楚,沿着调用链一步步走一遍源码,最后手写一个 Starter 验证理解。
我们要回答三个问题:
- Spring Boot 凭什么做到"约定大于配置"?
- 自动配置的触发点在哪?
- 它怎么知道该配什么、不该配什么?
二、入口:从启动类开始追踪
2.1 @SpringBootApplication 是个组合注解
每个 Spring Boot 项目的启动类都长这样:
java
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
点开 @SpringBootApplication 的源码,会发现它是一个组合注解:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ...
}
三个核心注解:
| 注解 | 作用 |
|---|---|
@SpringBootConfiguration |
本质就是 @Configuration,标记这是一个配置类 |
@ComponentScan |
包扫描,扫描 @Component、@Service、@Controller 等注解的 Bean |
@EnableAutoConfiguration |
自动配置的真正入口 |
一句话:@SpringBootApplication = 配置类 + 包扫描 + 自动配置,三个功能合在一个注解里。前两个是 Spring Framework 本来就有的,真正让 Spring Boot"自动"的,是第三个。
2.2 @EnableAutoConfiguration 做了什么
点开 @EnableAutoConfiguration 源码:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// ...
}
两个关键点:
@AutoConfigurationPackage:将启动类所在的包作为自动配置的"基础包",让后续扫描在这个包范围内进行@Import(AutoConfigurationImportSelector.class):这里是核心 ------通过@Import引入了AutoConfigurationImportSelector,这个 Selector 就是自动配置的"采购员",它负责决定加载哪些配置类
2.3 追踪调用链
把调用链画出来就是这样的:
less
@SpringBootApplication
→ @EnableAutoConfiguration
→ @Import(AutoConfigurationImportSelector.class)
→ selectImports() 方法
→ SpringFactoriesLoader.loadFactoryNames()
→ 读取 META-INF/spring.factories 文件
→ 拿到所有自动配置类的全限定名
→ 通过条件过滤(@Conditional 系列注解)
→ 只有满足条件的配置类才会生效
接下来逐层拆开看。
三、核心:spring.factories 加载机制
3.1 spring.factories 长什么样
在你的 Spring Boot 项目依赖里找到 spring-boot-autoconfigure 这个 jar 包(Maven 仓库路径一般是 org/springframework/boot/spring-boot-autoconfigure/),打开它,在 META-INF/spring.factories 文件里能看到:
properties
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
# ... 还有上百个
格式很直白:
- key 是
@EnableAutoConfiguration的全限定名 - value 是所有自动配置类的全限定名列表,用逗号分隔
- 这个文件就是"配置类的采购清单"------Spring Boot 把常见场景需要用到的配置类全列在这里了
3.2 SpringFactoriesLoader 怎么加载的
AutoConfigurationImportSelector 的核心方法 selectImports() 会调用 SpringFactoriesLoader.loadFactoryNames(),这个方法的核心逻辑(精简版):
java
public static List<String> loadFactoryNames(Class<?> factoryType, ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// 读取 classpath 下所有 META-INF/spring.factories 文件
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
// 遍历每个文件,解析成 key-value 结构
// key = 接口/注解的全限定名
// value = 实现类的全限定名列表
// 最终返回一个 Map<String, List<String>>
}
关键点:
- 不止 Spring Boot 自己的 jar 包有
spring.factories,你引入的任何 Starter 都可以有 - 这就是为什么引入一个 Starter 就能自动配置 ------它的
spring.factories里注册了对应的AutoConfiguration类 - 比如你引入
spring-boot-starter-data-redis,它依赖的spring-boot-autoconfigure里的spring.factories就有RedisAutoConfiguration,加载时就会被扫描到
3.3 踩坑记录
我在 debug 这一块的时候发现一个问题:AutoConfigurationImportSelector 加载了一百多个配置类,但最终生效的只有十几个------为什么?
这就引出了下一节的核心:条件装配。
另外一个坑:我一开始以为 spring.factories 是 Spring Boot 特有的机制,后来发现它是 Spring Framework 的 SpringFactoriesLoader 提供的,Spring Boot 只是用了这个已有机制来注册自动配置类。所以严格来说,spring.factories 不是 Spring Boot 的发明,而是 Spring Framework 的基础设施。
四、关键:条件装配------为什么只生效一部分
spring.factories 里列了一百多个配置类,但你的项目不可能全部需要。比如你的项目不用 Redis,那 RedisAutoConfiguration 就不该生效。Spring Boot 怎么做到的?
答案就是 @Conditional 系列注解。
4.1 @Conditional 系列注解
常用的条件注解:
| 注解 | 生效条件 |
|---|---|
@ConditionalOnClass |
classpath 中存在指定的类时生效 |
@ConditionalOnMissingBean |
容器中不存在指定 Bean 时生效 |
@ConditionalOnBean |
容器中存在指定 Bean 时生效 |
@ConditionalOnProperty |
配置文件中存在指定属性时生效 |
@ConditionalOnWebApplication |
是 Web 应用时生效 |
@ConditionalOnNotWebApplication |
不是 Web 应用时生效 |
用大白话说:spring.factories 里列了所有"候选配置类",但每个配置类上都标了条件注解------条件满足才装配,不满足就跳过。这就是"自动"背后的"智能"所在。
4.2 拿 RedisAutoConfiguration 举例
打开 RedisAutoConfiguration 的源码:
java
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ RedisConfiguration.class })
public class RedisAutoConfiguration {
// ...
}
逐行解读:
@Configuration:这是一个配置类,里面可以定义@Bean方法@ConditionalOnClass(RedisOperations.class):classpath 里要有RedisOperations类------你没引 Redis 依赖就不会有这个类,条件不满足,整个配置类直接跳过@EnableConfigurationProperties(RedisProperties.class):把配置文件里的spring.redis.*属性绑定到RedisProperties对象上@Import({ RedisConfiguration.class }):再引入更细粒度的配置
这样就完全串起来了:
kotlin
你引了 spring-boot-starter-data-redis
→ classpath 中有了 Redis 相关类
→ @ConditionalOnClass(RedisOperations.class) 满足
→ RedisAutoConfiguration 生效
→ RedisTemplate、StringRedisTemplate 等 Bean 自动装配到容器
→ 你直接 @Autowired 就能用
你只需要做一件事:引依赖。剩下的 Spring Boot 全帮你搞定了。
4.3 实操验证:启动项目看生效了哪些配置类
教你一个 debug 技巧。在 application.properties(或 application.yml)里加一行:
properties
debug=true
Spring Boot 启动时会在控制台输出一个 CONDITIONS EVALUATION REPORT,大致长这样:
markdown
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
RedisAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.data.redis.core.RedisOperations' (OnClassCondition)
Negative matches:
-----------------
DataSourceAutoConfiguration:
- @ConditionalOnClass did not find required class 'com.zaxxer.hikari.HikariDataSource' (OnClassCondition)
KafkaAutoConfiguration:
- @ConditionalOnClass did not find required class 'org.apache.kafka.clients.producer.Producer' (OnClassCondition)
- Positive matches:条件满足,已生效的配置类
- Negative matches:条件不满足,未生效的配置类
- Unconditional classes:无条件的配置类
这个报告是最直观的验证方式------照着做就能看到自己项目里哪些配置类生效了、哪些没生效、为什么没生效。面试官如果问"你怎么排查自动配置不生效的问题",把这个报告搬出来就是答案。
五、实战:手写一个自定义 Starter
光讲原理不够,能自己写一个 Starter 才算真懂。下面从零开始写一个 hello-spring-boot-starter。
5.1 目标
- 引入这个 Starter 后,自动注入一个
HelloServiceBean HelloService有一个sayHello()方法,返回"Hello, xxx!"- 可以通过配置文件
hello.name=xxx自定义名字
5.2 步骤
第一步:建 Maven 模块
创建一个 Maven 模块 hello-spring-boot-starter,pom.xml 关键依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
注意:Starter 本身不引入
spring-boot-starter-web之类的完整启动包,只需要spring-boot-autoconfigure提供自动配置注解支持。
第二步:写 HelloProperties(配置属性绑定)
java
package com.example.hello.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
private String name = "World";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@ConfigurationProperties(prefix = "hello") 会把配置文件里以 hello. 开头的属性映射到这个类的字段上。比如 hello.name=张三 → name 字段值为 "张三"。默认值 "World",不配置就用默认。
第三步:写 HelloService(自动配置的 Bean)
java
package com.example.hello.service;
import com.example.hello.properties.HelloProperties;
public class HelloService {
private HelloProperties properties;
public String sayHello() {
return "Hello, " + properties.getName() + "!";
}
public void setProperties(HelloProperties properties) {
this.properties = properties;
}
}
注意这里没有加 @Service 或 @Component------因为这个类不在启动类的包扫描范围内,加了也扫不到。它要靠下一步的自动配置类来注入。
第四步:写 HelloAutoConfiguration(自动配置类)
java
package com.example.hello.config;
import com.example.hello.properties.HelloProperties;
import com.example.hello.service.HelloService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HelloService.class)
public HelloService helloService(HelloProperties properties) {
HelloService service = new HelloService();
service.setProperties(properties);
return service;
}
}
逐行解读:
@Configuration:标记为配置类@ConditionalOnClass(HelloService.class):classpath 里有HelloService类才生效(这个条件总是满足,因为HelloService就在这个模块里。实战中如果是依赖外部类才用的条件,这里演示用)@EnableConfigurationProperties(HelloProperties.class):启用HelloProperties的属性绑定,把hello.*配置注入进来@Bean+@ConditionalOnMissingBean(HelloService.class):如果容器里还没有HelloService,就创建一个。如果用户自己定义了HelloService,Starter 的就不生效------这就是"用户配置优先"的设计
第五步:注册到 spring.factories
在 src/main/resources/META-INF/ 目录下创建 spring.factories 文件:
properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.hello.config.HelloAutoConfiguration
这个文件的位置必须是
META-INF/spring.factories,放在别的目录 Spring Boot 找不到。
第六步:安装到本地仓库
bash
mvn clean install
第七步:在另一个项目里引入 Starter 并测试
在测试项目的 pom.xml 中引入:
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
在 application.properties 中配置:
properties
hello.name=小明
写一个 Controller 测试:
java
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello() {
return helloService.sayHello();
}
}
启动项目,访问 http://localhost:8080/hello,返回:
Hello, 小明!
不配置 hello.name 则返回默认值:
Hello, World!
5.3 踩坑记录
写这个 Starter 的过程中踩了几个坑,记录一下:
坑一:spring.factories 放错位置
一开始把 spring.factories 放在了 src/main/resources/ 根目录下,启动项目发现 HelloService 根本没注入。Debug 了半天才发现------必须放在 src/main/resources/META-INF/ 目录下,SpringFactoriesLoader 是按 META-INF/spring.factories 这个固定路径去加载的。
坑二:忘了 @EnableConfigurationProperties
HelloProperties 上加了 @ConfigurationProperties(prefix = "hello"),但在 HelloAutoConfiguration 上忘了加 @EnableConfigurationProperties(HelloProperties.class)。结果配置文件里的 hello.name=张紫康 死活绑不进去,HelloService 返回的永远是默认值 "Hello, World!"。
@ConfigurationProperties 单独加在类上不会自动生效,需要配合 @EnableConfigurationProperties 或 @ConfigurationPropertiesScan 来启用属性绑定。
坑三:用户配置优先的验证
在测试项目里手动定义了一个 HelloService:
java
@Bean
public HelloService helloService(HelloProperties properties) {
HelloService service = new HelloService();
service.setProperties(properties);
return service;
}
因为 Starter 里的 @ConditionalOnMissingBean(HelloService.class) 检测到容器里已经有了,所以 Starter 的那个就不生效了。验证了"用户配置优先于自动配置"的设计原则------这也是 Spring Boot"约定大于配置"的核心:它帮你配好了,但你随时可以覆盖。
六、总结:一张图收束全文
Spring Boot 的自动配置不是魔法,就是三步:
markdown
1. spring.factories 里列了所有可能的配置类(采购清单)
2. SpringFactoriesLoader 读取清单,拿到所有配置类的全限定名
3. @Conditional 系列注解逐一判断,条件满足的才装配
用一张完整的流程图收束:
less
@SpringBootApplication
└─ @EnableAutoConfiguration
└─ @Import(AutoConfigurationImportSelector.class)
└─ selectImports()
└─ SpringFactoriesLoader.loadFactoryNames()
└─ 读取所有 META-INF/spring.factories
└─ 拿到 N 个 AutoConfiguration 类名
└─ 逐一过 @Conditional 条件判断
├─ 条件满足 → 装配生效
└─ 条件不满足 → 跳过
所以"约定大于配置"的本质是:Spring Boot 提前帮你把常见场景的配置写好了,放在 spring.factories 里,你引了对应的 Starter 就触发了条件,配置自动生效。你要做的只是改配置文件里的属性值------甚至不改都有默认值。
而当你需要覆盖默认配置时,自己定义一个 Bean 就行,@ConditionalOnMissingBean 会保证你的优先级------这就是"约定大于配置"但"不绑架配置"的设计哲学。
文末互动
你有没有遇到过 Spring Boot 自动配置不生效的情况?是怎么排查的?评论区聊聊。
如果觉得有帮助,点个赞收藏一下,后面会持续更新 Java + AI Agent 成长之路 系列的后续文章。