专栏:《Java后端工程师进阶之路》(Day 18 / 90) 主题:自动配置加载流程源码走读,自定义Starter开发实战(含自动配置开关设计)
你有没有这种经历------刚学Spring Boot的时候,觉得"约定大于配置"简直神器,几行代码就能跑起来一个Web应用。但突然有一天,你发现某个Bean莫名其妙被注册了,或者你明明配了某个属性,却完全不起作用。读完这篇,你不仅能看懂自动配置的源码链路,还能自己写一个生产级 Starter。
看起来简单,但@SpringBootApplication本身是个组合注解。打开它的源码(Spring Boot 3.x):
java
// Spring Boot 3.2+ 源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // ← 本质就是 @Configuration
@EnableAutoConfiguration // ← 自动配置的核心开关
@ComponentScan // ← 组件扫描,默认扫描同包及子包
public @interface SpringBootApplication {
// exclude 可以手动排除不想加载的自动配置类
Class<?>[] exclude() default {};
String[] excludeName() default {};
boolean proxyBeanMethods() default true;
}
三个注解各司其职:
- @SpringBootConfiguration :就是
@Configuration的马甲,标记当前类是配置类,可以声明@Bean。 - @ComponentScan :扫描当前包及子包下的
@Component、@Service、@Repository等。 - @EnableAutoConfiguration :这才是自动配置的灵魂,下面我们重点拆它。
二、@EnableAutoConfiguration:自动配置的开关与入口
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage // ← 注册包路径到AutoConfigurationPackages
@Import(AutoConfigurationImportSelector.class) // ← 加载自动配置类列表
public @interface EnableAutoConfiguration {
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
关键在@Import(AutoConfigurationImportSelector.class)。这个ImportSelector才是真正的加载引擎。
2.1 AutoConfigurationImportSelector的工作流程
AutoConfigurationImportSelector的核心方法是selectImports(),它的工作链路如下:
java
@EnableAutoConfiguration
→ @Import(AutoConfigurationImportSelector)
→ selectImports()
→ getAutoConfigurationEntry()
→ 第1步:从 spring.factories / spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 加载候选类列表
→ 第2步:用 @Conditional 条件过滤(去掉不满足条件的)
→ 第3步:应用 exclude 排除
→ 第4步:去重、排序(@AutoConfigureOrder / @AutoConfigureBefore / @AutoConfigureAfter)
→ 返回最终的自动配置类列表
Spring Boot 2.7之前 ,自动配置类列表写在META-INF/spring.factories里:
java
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
...
Spring Boot 3.x开始 ,改用了新文件路径META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
java
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
...
每行一个类名,不再需要那种反斜杠拼接的格式。这是一个很重要的变化------如果你还在用2.x的spring.factories方式写Starter,升级到3.x时需要迁移。
2.2 条件过滤:@Conditional家族
加载出候选列表后,不是所有类都会被注册。每个自动配置类头上都有一堆条件注解,不满足就跳过:
| 注解 | 作用 | 示例 |
|---|---|---|
@ConditionalOnClass |
classpath中存在指定类才生效 | 有DataSource才配置数据库 |
@ConditionalOnMissingClass |
classpath中不存在指定类才生效 | 没有某个类才用替代方案 |
@ConditionalOnBean |
容器中已存在指定Bean才生效 | 有DataSource才配置JdbcTemplate |
@ConditionalOnMissingBean |
容器中不存在指定Bean才生效 | 用户没自定义就帮你配一个默认的 |
@ConditionalOnProperty |
配置文件中指定属性满足条件才生效 | spring.mvc.enabled=true才配置MVC |
@ConditionalOnWebApplication |
当前是Web应用才生效 | 只有Web环境才配DispatcherServlet |
举个真实例子,DataSourceAutoConfiguration:
java
@AutoConfiguration(after = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass(DataSource.class) // ← classpath有DataSource才进来
@ConditionalOnMissingBean(DataSource.class) // ← 用户没自己定义DataSource才配默认的
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
}
@ConditionalOnMissingBean 是Spring Boot自动配置最重要的设计哲学:如果你自己定义了,Boot就不干预;你没定义,Boot帮你配一个合理的默认值。这就是"约定大于配置"的本质------不是没有配置,而是有合理的默认配置,你随时可以覆盖。
三、自动配置加载全流程源码走读
我们把整个链路串联起来,跟着Spring Boot启动过程走一遍:
java
// 步骤1: SpringApplication.run() → 创建ApplicationContext
SpringApplication.run(MyApp.class, args);
// 步骤2: ApplicationContext.refresh() → 12步刷新流程
// 其中第5步: invokeBeanFactoryPostProcessors()
// → 处理 @Import 导入的 AutoConfigurationImportSelector
// 步骤3: AutoConfigurationImportSelector.selectImports()
// → getCandidateConfigurations() 从imports文件加载候选列表
// → getAutoConfigurationEntry() 进行条件过滤
// 步骤4: 条件评估 - Spring Boot 3.x 使用 AutoConfigurationSorter
// → 处理 @AutoConfigureBefore/@After/@Order 排序
// → 执行每个类上的 @Conditional 判断
// 步骤5: 满足条件的自动配置类被注册为BeanDefinition
// → 后续走正常的Bean实例化流程(getBean → createBean → populateBean → initializeBean)
实操:查看你的项目到底加载了哪些自动配置类
java
// 方式1:启动时打印自动配置报告(在application.properties中设置)
// debug=true (Spring Boot 2.x)
// --debug 启动参数 或 application.yml:
spring:
autoconfigure:
report:
enabled: true # Spring Boot 3.x
// 方式2:用Actuator端点查看
// GET /actuator/conditions
// 会返回匹配和不匹配的自动配置类及原因
// 方式3:代码方式,直接打印已注册的Bean类型
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
// 打印所有自动配置相关的Bean
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.stream(beanNames)
.filter(name -> name.contains("AutoConfiguration")
|| name.contains("Configuration"))
.forEach(System.out::println);
}
}
启动debug模式后,你会在日志里看到类似这样的报告:
java
== AUTO-CONFIGURATION REPORT ==
Positive matches:(匹配成功的)
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found DataSource (OnClassCondition)
- @ConditionalOnMissingBean found no DataSource (OnBeanCondition)
Negative matches:(条件不满足被跳过的)
RedisAutoConfiguration did not match:
- @ConditionalOnClass did not find RedisOperations (OnClassCondition)
Excluded:(手动排除的)
None
这段日志是你排查"为什么某个Bean没被配置"的第一手证据。
四、实战:自定义一个Starter
光看原理还不够,咱们来写一个真正能用的自定义Starter。场景:为你的项目封装一个统一的邮件通知组件。
4.1 Starter的标准目录结构
自定义Starter的命名规范:xxx-spring-boot-starter(官方Starter是spring-boot-starter-xxx,反过来)。
notify-spring-boot-starter/
├── pom.xml
├── src/main/java/com/example/notify/
│ ├── autoconfigure/
│ │ ├── NotifyAutoConfiguration.java ← 自动配置类
│ │ └── NotifyProperties.java ← 配置属性绑定类
│ └── service/
│ └── NotifyService.java ← 核心服务类
├── src/main/resources/
│ ├── META-INF/spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ← 3.x方式
│ └── META-INF/spring.factories ← 2.x兼容(可选)
4.2 配置属性类
java
// Spring Boot 3.2+ --- 使用 @ConfigurationProperties 绑定 application.yml 配置
package com.example.notify.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "notify") // ← prefix决定yaml中的键名
public class NotifyProperties {
/** 是否启用通知功能,默认false --- 这就是"自动配置开关" */
private boolean enabled = false;
/** 默认通知渠道:email / sms / dingtalk */
private String channel = "email";
/** 邮件发送人地址 */
private String sender = "system@company.com";
/** SMTP服务器地址 */
private String smtpHost = "localhost";
/** SMTP端口 */
private int smtpPort = 25;
}
4.3 核心服务类
java
package com.example.notify.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NotifyService {
private static final Logger log = LoggerFactory.getLogger(NotifyService.class);
private final String channel;
private final String sender;
public NotifyService(String channel, String sender) {
this.channel = channel;
this.sender = sender;
}
/** 发送通知 --- 实际项目中可以对接JavaMail/SMS SDK/钉钉API */
public void send(String recipient, String subject, String content) {
log.info("[Notify] channel={}, sender={}, to={}, subject={}, content={}",
channel, sender, recipient, subject, content);
// 实际实现:根据channel选择不同发送方式
// 这里简化为日志输出,生产代码请替换为真实发送逻辑
}
}
4.4 自动配置类(含开关设计)
java
package com.example.notify.autoconfigure;
import com.example.notify.service.NotifyService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration // ← Spring Boot 3.x 用 @AutoConfiguration 替代 @Configuration
@EnableConfigurationProperties(NotifyProperties.class) // ← 注册属性绑定类
@ConditionalOnProperty(
prefix = "notify",
name = "enabled",
havingValue = "true", // ← notify.enabled=true 才生效
matchIfMissing = false // ← 默认不启用(安全!)
)
public class NotifyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // ← 用户自定义了NotifyService就不注册默认的
public NotifyService notifyService(NotifyProperties props) {
return new NotifyService(props.getChannel(), props.getSender());
}
}
注意这个设计的关键点:
- @ConditionalOnProperty开关 :
matchIfMissing = false意味着配置文件里不写notify.enabled=true就不加载------这是Starter的安全开关设计,避免引入Starter就自动加载一堆Bean。 - @ConditionalOnMissingBean :用户可以通过
@Bean自己定义一个NotifyService,Boot的默认配置就自动退让。 - @AutoConfiguration :Spring Boot 3.x的新注解,替代原来的
@Configuration,语义更明确。
4.5 注册文件(3.x方式)
创建文件src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
java
com.example.notify.autoconfigure.NotifyAutoConfiguration
如果要兼容2.x ,额外创建META-INF/spring.factories:
java
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.notify.autoconfigure.NotifyAutoConfiguration
4.6 用户项目使用
在业务项目的pom.xml中引入Starter:
java
<dependency>
<groupId>com.example</groupId>
<artifactId>notify-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
在application.yml中开启:
java
notify:
enabled: true # ← 开关打开,自动配置才生效
channel: dingtalk # ← 覆盖默认值
sender: ops@mycompany.com
smtp-host: smtp.mycompany.com
smtp-port: 465
然后直接注入使用:
java
@Autowired
private NotifyService notifyService; // ← Boot自动帮你配好的
public void onOrderCreated(Order order) {
notifyService.send("admin@company.com", "新订单",
"订单号:" + order.getId());
}
如果用户想自定义NotifyService?直接声明一个@Bean:
java
@Bean
public NotifyService customNotifyService() {
return new NotifyService("sms", "custom-sender@company.com");
}
Boot的自动配置就会自动退让------这就是@ConditionalOnMissingBean的威力。
五、排除不需要的自动配置
有时候某些自动配置你不想要,有三种排除方式:
java
// 方式1:@SpringBootApplication 的 exclude 属性
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MyApp { ... }
// 方式2:application.yml 配置排除
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
// 方式3:Spring Boot 3.x 的 @AutoConfigurationExcludeFilter(框架内部机制)
什么时候需要排除?
- 你用了NoSQL数据库(如MongoDB),不需要DataSource自动配置。
- 你用的是WebFlux而非Web MVC,需要排除MVC相关配置。
- 测试环境不想加载某些重量级组件(如Redis、RabbitMQ)。
六、自动配置的"陷阱"与排查思路
陷阱1:Bean被自动配置覆盖了你自己的
场景:你手动定义了一个ObjectMapper Bean,但发现行为不符合预期------因为你没注意到JacksonAutoConfiguration也注册了一个,而Spring Boot 3.x对同名Bean的处理策略变了。
排查 :启动debug模式,看Conditions Report,确认你的Bean和自动配置的Bean谁赢了。从Spring Boot 2.1开始,默认不允许Bean定义覆盖(spring.main.allow-bean-definition-overriding=false),同名会直接报错------这反而帮你提前发现问题。
陷阱2:@ConditionalOnProperty的值写错了
java
# 你以为写了true,但YAML解析的是字符串"true"
my-feature:
enabled: true # ← 这个true是boolean还是string?取决于havingValue定义
如果@ConditionalOnProperty的havingValue = "true",而YAML中enabled: true被解析为布尔值,条件匹配没问题。但如果配置写成了enabled: "true"(字符串),匹配也没问题。真正坑的是:配置键名写错了,prefix或name没对齐,条件永远不匹配,而你以为是功能bug。
陷阱3:自动配置类的加载顺序问题
两个自动配置类互相依赖,先后顺序不对就会出问题。用@AutoConfigureBefore和@AutoConfigureAfter控制:
java
@AutoConfiguration(after = DataSourceAutoConfiguration.class) // ← 我要在DataSource之后加载
public class MyAutoConfiguration { ... }
实战建议
-
每个Starter都设计开关 :用
@ConditionalOnProperty(prefix = "xxx", name = "enabled", havingValue = "true", matchIfMissing = false)做入口条件。默认关闭,用户显式开启。这不是过度设计------引入Starter却莫名其妙加载一堆Bean,才是真正的灾难。 -
排查自动配置问题的三板斧:
- 第一斧:启动debug模式,看Conditions Report,哪些匹配/哪些不匹配、原因一目了然。
- 第二斧:Actuator的
/actuator/conditions端点,运行时动态查看。 - 第三斧:IDEA的Spring Boot插件,编辑器里就能看到每个自动配置类的条件状态。
-
自定义Starter的命名和目录规范要严格遵守 :
xxx-spring-boot-starter命名、autoconfigure子包放配置类、META-INF/spring/...imports注册文件。这不是形式主义------你的Starter将来可能被团队其他项目、甚至其他团队引用,规范是最低成本的沟通方式。
金句:Spring Boot的自动配置不是魔法,是条件注解 + 属性绑定 + 退让设计的精妙组合。读懂它,你就能驾驭它;驾驭它,你就能设计自己的生态组件。
下篇预告:《Spring Boot启动优化:启动时间从8秒降到2秒》------你的Spring Boot项目启动要多久?如果超过5秒,明天这篇就是给你量身准备的。