引一个 starter,DataSource 就自己进了容器;想换掉默认的 RestTemplate,两个 Bean 却抢同一注入点;@ConditionalOnMissingBean 写了不生效。三个现象指向同一套机制:条件在哪个阶段评估、那个阶段看得见什么、自动配置相对你的配置排第几位。
@SpringBootApplication 只负责开灯,干活的是 AutoConfigurationImportSelector,判去留的是 @Conditional 家族。以下按 Boot 3.3.x / Framework 6.1.x 核对。
1. 从三个注解到 imports 清单
| 组成 | 作用 | 去掉的后果 |
|---|---|---|
@SpringBootConfiguration |
标记主配置类 | 找不到主配置 |
@EnableAutoConfiguration |
引入选择器 | starter 的 Bean 全消失 |
@ComponentScan |
扫描所在包 | 自己的组件不注册 |
@AutoConfigurationPackage // 登记主类所在包
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
Class<?>[] exclude() default {};
}
AutoConfigurationImportSelector 的身份是 DeferredImportSelector,这点决定第 4 节的全部结论。getAutoConfigurationEntry 做五件事:
| 步骤 | 动作 | 异常表现 |
|---|---|---|
| 1 | 读 ...AutoConfiguration.imports |
缺文件则零装配、不报错 |
| 2 | 去重 | 重复类静默合并 |
| 3 | OnClassCondition 粗筛 |
缺类的候选提前剪掉 |
| 4 | 合并 exclude 配置 |
名字写错只"没生效" |
| 5 | 发导入事件 | 条件报告才有数据 |
清单一行一个全限定类名,路径是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。Boot 3.0 起不再读 spring.factories 里的同名键;自写 starter 少一层目录、多一个后缀,整个 starter 静默失效。
2. @Conditional 家族的判据
| 注解 | 判据 | 常见误用 |
|---|---|---|
@ConditionalOnClass |
classpath 有该类 | value 引可选依赖 → 运行期 NoClassDefFoundError,改 name |
@ConditionalOnMissingClass |
该类不存在 | 同上,须写字符串 |
@ConditionalOnBean |
已有该类型/名称 | 写在用户配置类上 |
@ConditionalOnMissingBean |
没有该类型/名称 | 与用户注册顺序赛跑 |
@ConditionalOnSingleCandidate |
唯一候选或唯一 @Primary |
多候选无 @Primary 同样判负 |
@ConditionalOnProperty |
配置值匹配 | 漏 matchIfMissing |
@ConditionalOnWebApplication |
是否 Web | WebFlux 用 ANY 误判 |
@ConditionalOnResource |
资源存在 | 漏 classpath: |
@ConditionalOnExpression |
SpEL 为 true | 写长了没法维护 |
@ConditionalOnProperty 的四态:
| 配置状态 | 判定 | 结果 |
|---|---|---|
| 无该键 | 走 matchIfMissing=false |
不成立 |
enabled=true |
等于 havingValue |
成立 |
enabled=false |
不等 | 不成立 |
enabled=TRUE |
忽略大小写后相等 | 成立 |
判负全程静默:不抛异常、不打日志,只能靠 --debug 确认。
3. 条件评估的两个阶段
| 阶段 | 常量 | 何时 | 看得见 |
|---|---|---|---|
| 解析 | PARSE_CONFIGURATION |
解析配置类、定义未注册 | 注册表 + Environment |
| 注册 | REGISTER_BEAN |
@Bean 待转定义 |
已注册定义,含用户贡献 |
内置条件中,OnClass、OnResource、OnProperty、OnWebApplication 都在解析阶段;只有 OnBeanCondition(OnBean/MissingBean/SingleCandidate)在注册阶段,所以它才能看见别的配置类声明的 Bean。
自定义条件要读 Bean 定义,必须显式声明阶段:
public class OnClarityModuleCondition implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN; // 默认解析阶段
}
@Override
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
return ctx.getBeanFactory()
.getBeanNamesForType(ClarityModule.class, true, false).length > 0;
}
}
allowEagerInit=false 是关键:在条件里触发实例化会连带一串提前初始化,@ConditionalOnBean 内部同样这么调。
4. 为什么 @ConditionalOnBean 在用户配置类上不可靠
DeferredImportSelector 的 selectImports 被推迟到所有用户配置类解析完之后执行,目的是让自动配置里的 @ConditionalOnMissingBean 看得见用户已声明的 Bean:顺序永远是"用户先、自动配置后"。
反过来就出事:
@Configuration
public class MyConfig {
@Bean
@ConditionalOnBean(DataSource.class) // 自动配置尚未处理
public JdbcTemplate jdbcTemplate(DataSource ds) {
return new JdbcTemplate(ds);
}
}
两个配置类谁先解析并无保证。判负 → 方法跳过 → 稍后 DataSource 真的来了,JdbcTemplate 却已不在。现象是"注入点找不到 Bean",不是"条件判错",方向极易跑偏。
可靠写法只有三种:把方法挪进自动配置类;改用 ObjectProvider<T> 表达可选依赖;自动配置之间用 @AutoConfigureAfter 显式排序(对用户配置无效)。@ConditionalOnMissingBean 有同样的方向性,写在用户配置类里就是"我和别人赛跑"。
5. 自动配置之间的排序
@AutoConfiguration(after = DataSourceAutoConfiguration.class,
before = JdbcTemplateAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
public class ClarityJdbcAutoConfiguration { }
| 注解 | 语义 | 建议 |
|---|---|---|
@AutoConfigureBefore/After |
相对某配置的先后 | 写多会成环 |
@AutoConfigureOrder |
绝对顺序 | 跨不过显式 before/after |
@AutoConfiguration 的 before/after |
3.x 推荐 | 新代码统一用 |
排序只决定配置类被处理的顺序,不决定 Bean 的实例化顺序。以为写了 @AutoConfigureAfter 就能保证某 Bean 先初始化,是另一类误解。
6. 自定义 starter 的最小结构
clarity-spring-boot-starter/
└── src/main/
├── java/com/clarity/starter/ClarityAutoConfiguration.java
└── resources/META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
@ConfigurationProperties(prefix = "clarity")
public class ClarityProperties {
private Duration timeout = Duration.ofSeconds(3); // 或 3s
// getter/setter 省略
}
@AutoConfiguration
@ConditionalOnClass(ClarityClient.class)
@EnableConfigurationProperties(ClarityProperties.class)
public class ClarityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ClarityClient clarityClient(ClarityProperties p) {
return new ClarityClient(p.getTimeout());
}
}
必备三件:清单文件、@AutoConfiguration(内含 @Configuration(proxyBeanMethods=false))、@ConditionalOnMissingBean 兜底。再加 spring-boot-configuration-processor(optional),IDE 才有属性补全。
7. 覆盖自动配置的三种方式
| 方式 | 做法 | 代价 |
|---|---|---|
| 定义同类型 Bean | 靠对方的 @ConditionalOnMissingBean 让位 |
依赖对方写了该条件 |
| 排除 | exclude / spring.autoconfigure.exclude |
该类其它能力一并消失 |
| 属性开关 | 多数带 enabled |
并非全都有 |
排除按全限定类名匹配,写错不报错——"排除了却没变化"的头号原因。改完用 /actuator/conditions 的 Exclusions 段确认。
8. 配置属性的绑定
@ConfigurationProperties(prefix = "clarity.cache")
@Validated
public class CacheProperties {
@NotBlank private String name;
private Duration ttl = Duration.ofMinutes(10);
private DataSize maxSize = DataSize.ofMegabytes(64);
}
| 特性 | @ConfigurationProperties |
@Value |
|---|---|---|
| 松散绑定 | 支持 | 须精确 |
| 嵌套/集合 | 支持 | 需 SpEL 拼装 |
| JSR-303 | 类上 @Validated |
粒度粗 |
| IDE 补全 | 需 processor | 无 |
单位后缀是绑定器特产:30s、5m、PT2H 解析为 30 秒、5 分钟、2 小时;10MB 走 DataSize,纯数字按字节解释。缺 setter 是最常见的静默失败:字段 private final 或只写 getter,绑定器不报错,打出来全是默认值。
9. 读 CONDITIONS EVALUATION REPORT
Positive matches:
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required class 'javax.sql.DataSource'
Negative matches:
RedisAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find class 'redis.clients.jedis.Jedis'
Exclusions: None
| 段落 | 含义 | 动作 |
|---|---|---|
Positive matches |
已生效并列出判据 | 核对生效原因 |
Negative matches |
判负并给 Did not match |
直接告诉你缺哪个类 |
Exclusions |
被排除的 | 出现意外类名 → 配置写错 |
Unconditional classes |
无条件的配置类 | 最易成为意外来源 |
报告按层级组织,父级判负时子级不再展开。某段整块消失,先看父级为什么判负。
10. 常见坑
| 现象 | 根因 | 处理 |
|---|---|---|
| starter 不生效 | imports 路径写错 |
按 3.x 路径核对 |
OnMissingBean 仍出两个 |
条件写在用户配置上赛跑 | 挪进自动配置 |
运行期 NoClassDefFoundError |
OnClass(value=...) 引可选依赖 |
改 name |
OnBean 判负但 Bean 存在 |
跑在解析阶段 | 返回 REGISTER_BEAN |
| 排除了仍生效 | 类名错或被间接引入 | 查 Exclusions |
| 属性绑定为 null | 缺 setter 或未注册 | 显式注册属性类 |
第三条最隐蔽:IDE 能索引到那个类(optional 依赖里出现过),编译期不报错,运行期条件判负。
11. 边界
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 按环境切换实现 | @Profile |
条件表达依赖是否满足,不是在哪套环境 |
| 条件逻辑超三行 | 抽 Condition 类 |
长 SpEL 无人能维护 |
| 运行时增删 Bean | BeanDefinitionRegistryPostProcessor |
条件只评估一次 |
| 只改某 Bean 属性 | 覆盖配置属性值 | 不必排除整个自动配置 |
条件装配是"启动期的一次性决议":结论固化进 BeanDefinition,运行期不再变。所有"这个 Bean 为什么多了/没了"的答案都在启动那一刻,而那一刻可以被完整打印出来。
12. 自测
@SpringBootApplication拆开是哪三个注解?AutoConfigurationImportSelector为什么必须是DeferredImportSelector?@ConditionalOnBean在普通@Configuration上为什么不可靠?- 两个阶段分别看得见什么?
havingValue与matchIfMissing组合,四种状态各判什么?- starter 少写哪一样会静默不生效?