专栏:《Java后端工程师进阶之路》(Day 18 / 90) 主题:自动配置加载流程源码走读,自定义Starter开发实战(含自动配置开关设计)
你有没有这种经历——刚学Spring Boot的时候,觉得"约定大于配置"简直神器,几行代码就能跑起来一个Web应用。但突然有一天,你发现某个Bean莫名其妙被注册了,或者你明明配了某个属性,却完全不起作用。读完这篇,你不仅能看懂自动配置的源码链路,还能自己写一个生产级 Starter。
看起来简单,但@SpringBootApplication本身是个组合注解。打开它的源码(Spring Boot 3.x):
// 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:自动配置的开关与入口
@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(),它的工作链路如下:
@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里:
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:
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:
@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启动过程走一遍:
// 步骤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)实操:查看你的项目到底加载了哪些自动配置类
// 方式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模式后,你会在日志里看到类似这样的报告:
== 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 配置属性类
// 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 核心服务类
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 自动配置类(含开关设计)
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:
com.example.notify.autoconfigure.NotifyAutoConfiguration如果要兼容2.x,额外创建META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.notify.autoconfigure.NotifyAutoConfiguration4.6 用户项目使用
在业务项目的pom.xml中引入Starter:
<dependency> <groupId>com.example</groupId> <artifactId>notify-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>在application.yml中开启:
notify: enabled: true # ← 开关打开,自动配置才生效 channel: dingtalk # ← 覆盖默认值 sender: ops@mycompany.com smtp-host: smtp.mycompany.com smtp-port: 465然后直接注入使用:
@Autowired private NotifyService notifyService; // ← Boot自动帮你配好的 public void onOrderCreated(Order order) { notifyService.send("admin@company.com", "新订单", "订单号:" + order.getId()); }如果用户想自定义NotifyService?直接声明一个@Bean:
@Bean public NotifyService customNotifyService() { return new NotifyService("sms", "custom-sender@company.com"); }Boot的自动配置就会自动退让——这就是@ConditionalOnMissingBean的威力。
五、排除不需要的自动配置
有时候某些自动配置你不想要,有三种排除方式:
// 方式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被自动配置覆盖了你自己的
场景:你手动定义了一个ObjectMapperBean,但发现行为不符合预期——因为你没注意到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的值写错了
# 你以为写了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控制:
@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秒,明天这篇就是给你量身准备的。