2199 字
约 7 分钟
16
S07-X10 · Spring 条件装配与自动配置原理

引一个 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. 自测

  1. @SpringBootApplication 拆开是哪三个注解?
  2. AutoConfigurationImportSelector 为什么必须是 DeferredImportSelector?
  3. @ConditionalOnBean 在普通 @Configuration 上为什么不可靠?
  4. 两个阶段分别看得见什么?
  5. havingValue 与 matchIfMissing 组合,四种状态各判什么?
  6. starter 少写哪一样会静默不生效?
S07-X10 · Spring 条件装配与自动配置原理
https://www.clxhxhhr.top/posts/5951/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。