引入 spring-boot-starter-web,DispatcherServlet、Jackson、Tomcat 自动就位;加上 @EnableWebMvc,同一批自动配置又整体退让。背后是条件求值 + 注册顺序两件事在裁决,不是"约定"。
不清楚这套机制,症状很固定:自己写的 ObjectMapper 不生效、@ConditionalOnMissingBean 判定为真却仍注册两份、排除项写了没反应。
1. 自动配置的加载路径
Spring Boot 3.x 只认一个入口:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,每行一个全限定类名,没有 key、没有等号。
| 对比项 | AutoConfiguration.imports(2.7+) |
spring.factories(旧) |
|---|---|---|
| 路径 | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
META-INF/spring.factories |
| 格式 | 纯类名,一行一个 | key=类名,类名 |
| 3.0 之后 | 唯一入口 | 该 key 已移除 |
| 合并方式 | 多 jar 条目直接拼接 | 同一 key 逗号合并 |
| 常见错误 | 仍写 spring.factories → 整类不加载 |
换行续写被解析成非法类名 |
com.example.starter.RedisAutoConfiguration
com.example.starter.MetricsAutoConfiguration
文件里不要写 # 注释:读取器会把注释行当类名,启动报 ClassNotFoundException。
2. 条件注解判断什么、何时判断
| 注解 | 判断对象 | 求值阶段 | 典型用法 |
|---|---|---|---|
@ConditionalOnClass |
类路径是否有某类 | 解析配置类(读 ASM,不加载类) | 整个自动配置的开关 |
@ConditionalOnMissingClass |
类不存在 | 同上 | 提供降级实现 |
@ConditionalOnBean |
是否已注册某 Bean | 注册 Bean 定义阶段 | 依赖其他配置的产物 |
@ConditionalOnMissingBean |
是否尚无该类型 | 注册 Bean 定义阶段 | 让用户配置优先 |
@ConditionalOnProperty |
Environment 属性值 | 解析配置类 | enabled、mode 开关 |
@ConditionalOnWebApplication |
应用类型 | 解析配置类 | 仅 Web 环境生效 |
@AutoConfiguration
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(prefix = "example.redis", name = "enabled", havingValue = "true", matchIfMissing = true)
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory f) {
RedisTemplate<String, Object> t = new RedisTemplate<>();
t.setConnectionFactory(f);
return t;
}
}
@ConditionalOnClass 能写在类路径没有该类的配置类上,是因为它读字节码注解元数据(ASM)而非反射加载;代价是注解值必须是字面量,${} 不被求值。@ConditionalOnMissingBean 不写类型时按方法返回类型推断,返回 Object 等于"容器里没有任何 Bean"。
3. 求值顺序与 @AutoConfigureAfter
| 阶段 | 常量 | 求值的条件 | 含义 |
|---|---|---|---|
| 一 | PARSE_CONFIGURATION |
类级:OnClass、OnWebApplication、OnProperty |
决定配置类是否进入后续处理 |
| 二 | REGISTER_BEAN |
Bean 级:OnBean、OnMissingBean、OnSingleCandidate |
决定 @Bean 方法是否注册 |
@ConditionalOnMissingBean 在阶段二执行,看到的是"到目前为止已注册的 Bean 定义",所以结果取决于谁先被处理。两个自动配置类都提供同类型 Bean 时,加载顺序决定谁生效——这就是 @AutoConfigureAfter 存在的理由:
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@ConditionalOnBean(DataSource.class)
public class JdbcTemplateAutoConfiguration { /* ... */ }
它只约束自动配置类之间的顺序;用户 @Configuration 与自动配置的先后由"用户先注册、自动配置后注册"保证,不需要注解干预。
4. 用户配置为什么能覆盖自动配置
覆盖不是替换,而是自动配置发现已有 Bean 后不注册。前提两条:自动配置的 @Bean 带 @ConditionalOnMissingBean;用户配置先完成注册。
@Configuration
public class MyJacksonConfig {
@Bean
public ObjectMapper objectMapper() { // 不带条件
return new ObjectMapper().findAndRegisterModules();
}
}
| 用户写法 | 结果 | 原因 |
|---|---|---|
同类型 @Bean,名字不同 |
自动配置让位 | OnMissingBean 按类型判定 |
同类型多个 @Bean |
仍让位,注入点可能 NoUniqueBeanDefinitionException |
需 @Primary 或 @Qualifier |
| 用子类/接口类型声明 | 类型匹配规则适用 | 按可赋值性判断 |
| 继承自动配置类覆盖方法 | 不推荐 | 条件注解不继承,易产生两个定义 |
最后两行常被忽视:把 ObjectMapper 包成子类,@ConditionalOnMissingBean(ObjectMapper.class) 依然判"已存在"。
5. 排除自动配置的两种方式
| 方式 | 生效时机 | 适用 |
|---|---|---|
@SpringBootApplication(exclude = X.class) |
编译期写在注解 | 应用永久不要 |
spring.autoconfigure.exclude |
启动时读 Environment | 按环境临时关闭 |
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
重复排除是幂等的。要注意连锁失效:排掉 DataSourceAutoConfiguration 后,依赖 DataSource 的自动配置因 @ConditionalOnBean 不满足一起消失,而报错只说"缺少 DataSource Bean"。
6. 自定义 starter 的最小结构
example-redis-spring-boot-autoconfigure/
├── pom.xml
└── src/main/
├── java/com/example/starter/RedisAutoConfiguration.java
├── java/com/example/starter/RedisProperties.java
└── resources/META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
example-redis-spring-boot-starter/
└── pom.xml # 只依赖 autoconfigure 模块
autoconfigure 模块放条件与代码,starter 只放依赖。AutoConfiguration.imports 若放在 starter 里,autoconfigure 被单独 @Import 时不生效。
7. @ConfigurationProperties 的绑定与校验
| 特性 | 说明 | 常见误用 |
|---|---|---|
| 松散绑定 | example.redis.max-idle ↔ maxIdle ↔ MAX_IDLE |
以为 @Value 也支持 |
| 构造器绑定 | 单构造器参数自动绑定,3.x 无需 @ConstructorBinding |
仍标旧注解 |
| 嵌套对象 | 内部类需访问器或参与构造器绑定 | private static 且无 getter |
| 校验 | 类上 @Validated + jakarta.validation 约束 |
只写 @Valid,绑定不触发校验 |
| 注册 | @EnableConfigurationProperties 或 @ConfigurationPropertiesScan |
只写注解不注册,Bean 不存在 |
@ConfigurationProperties(prefix = "example.redis")
@Validated
public record RedisProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
@DefaultValue("2000") Duration timeout) { }
record 走构造器绑定,@DefaultValue 在参数上生效。@Validated 必须加在类上;加在 @Bean 方法上时校验发生在方法调用而非绑定阶段,留不住"写错就启动失败"。
8. 调试自动配置
$ java -jar app.jar --debug
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
RedisAutoConfiguration matched:
- @ConditionalOnClass found required class '...RedisTemplate' (OnClassCondition)
Negative matches:
DataSourceAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.sql.DataSource'
Exclusions:
SecurityAutoConfiguration
| 区块 | 含义 | 排查价值 |
|---|---|---|
Positive matches |
条件全满足、已生效 | 确认目标配置是否加载 |
Negative matches |
有条件不满足 | 直接读到缺哪个类/属性 |
Exclusions |
被显式排除 | 区分"排除"与"条件不满足" |
Unconditional classes |
无条件配置 | 通常只有少数基础类 |
运行期按需取快照:
$ curl -s localhost:8080/actuator/conditions | jq '.contexts[].positiveMatches | keys'
需先暴露 conditions endpoint,且返回的是启动时快照,不是实时状态。
9. 排错
| 现象 | 根因 | 处理 |
|---|---|---|
自定义 @Bean 不生效 |
类型不匹配,或用户配置注册顺序靠后 | --debug 核对 OnMissingBean 的判定类型 |
OnMissingBean 判真却出现两个 Bean |
求值时对方尚未注册(顺序问题) | @AutoConfigureAfter 固定顺序 |
自动配置类被 @ComponentScan 扫到,条件失效 |
扫描范围覆盖了 starter 包 | 收窄 scanBasePackages,starter 包不与业务包重叠 |
spring.autoconfigure.exclude 无反应 |
类名错 / 用了旧包路径 | 从 --debug 的 Exclusions 区块核对 |
@ConfigurationProperties Bean 不存在 |
缺 @EnableConfigurationProperties / @ConfigurationPropertiesScan |
补注册,注解本身不是 Bean 定义 |
| 配置校验不触发 | 类上只写 @Valid |
改 @Validated,确认 validation starter 在类路径 |
| 升级 Boot 3 后 starter 全失效 | 仍写在 spring.factories |
迁到 AutoConfiguration.imports |
10. 边界
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 运行期动态开关能力 | @ConditionalOnProperty + 策略 Bean |
条件只在启动时求值一次 |
| 按租户/请求切换实现 | 策略模式 + 路由 | 条件是容器级,不是请求级 |
| 自动配置之间互相依赖 | 显式 @Import 或 @AutoConfigureOrder |
条件无法表达循环依赖 |
大量 OnBean 串联 |
构造器显式注入 | 条件链难调试、失败信息模糊 |
| 要求用户必须提供某 Bean | 自动配置里直接注入,缺失即失败 | OnMissingBean 是"可选"语义 |
条件是"装配时装不装",不是"运行时用不用"。
11. 自测
AutoConfiguration.imports与spring.factories在 3.x 里是什么关系?@ConditionalOnClass为什么能写在"类不存在"的配置类上?它读的是什么?- 两个自动配置类都用
@ConditionalOnMissingBean,结果不确定,靠什么固定? - 用户自定义了
ObjectMapper的子类,@ConditionalOnMissingBean(ObjectMapper.class)怎么判? - 排除
DataSourceAutoConfiguration后,为什么别的自动配置也一起失效? --debug报告里Negative matches与Exclusions的区别?