@Conditional系列条件装配:SpringBoot自动配置的核心机制
@Conditional系列条件装配:SpringBoot自动配置的核心机制
适读人群:Spring Boot使用者,希望理解自动配置原理并自定义starter的开发者 | 阅读时长:约18分钟
开篇故事
有段时间我在做一个内部中间件的starter。功能写完之后,发现一个问题:用户在引入这个starter的同时自定义了同类型的Bean,我的starter的Bean就不应该再创建,否则会冲突。
我去翻了Spring Boot的数据库starter源码,发现它是这样写的:
@ConditionalOnMissingBean(DataSource.class)如果容器里已经有DataSource了,这个Bean就不创建。就这一行注解,解决了"用户自定义覆盖默认配置"的问题。
这就是@Conditional的威力。Spring Boot的自动配置全靠这套机制运转,不理解它,你就只能傻傻地用,出了问题毫无头绪。今天我们把@Conditional的原理和Spring Boot扩展的各种条件注解都讲透。
一、@Conditional的原理
1.1 基础接口
// spring-context/src/.../Conditional.java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
// Condition接口
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}Condition.matches()返回true则装配,false则跳过。ConditionContext提供了访问BeanFactory、Environment、ClassLoader等的能力。
1.2 执行时机
关键类:org.springframework.context.annotation.ConditionEvaluator
// ConditionEvaluator.java 第80行(简化)
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata,
@Nullable ConfigurationPhase phase) {
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
// 获取所有@Conditional注解的value(Condition实现类)
List<Condition> conditions = getConditions(metadata, this.context.getClassLoader());
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition cc) {
requiredPhase = cc.getConfigurationPhase();
}
if ((requiredPhase == null || requiredPhase == phase)
&& !condition.matches(this.context, metadata)) {
return true; // 有任何一个条件不满足,就跳过
}
}
return false;
}二、Spring Boot内置条件注解详解
2.1 全景图
2.2 @ConditionalOnClass 源码解析
这是最常用的条件注解之一,检查类路径上是否存在某个类:
// OnClassCondition.java 第60行(简化)
@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends FilteringSpringBootCondition {
@Override
protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
// 批量处理,性能优化:使用线程并行检查
// ...
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
// 检查value/name指定的类是否在classpath
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(
ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes")
.items(Style.QUOTE, missing));
}
matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes").items(Style.QUOTE, onClasses);
}
return ConditionOutcome.match(matchMessage);
}
}关键点:filter(onClasses, ClassNameFilter.MISSING, classLoader),通过ClassLoader.loadClass()或ClassUtils.isPresent()检查类是否存在,不会真正初始化类,只是检查存在性。
2.3 @ConditionalOnMissingBean 源码解析
这是最容易踩坑的注解,涉及Bean的判断顺序:
// OnBeanCondition.java(简化关键逻辑)
@Order(Ordered.LOWEST_PRECEDENCE) // 注意:最低优先级,确保用户的Bean先注册
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
// 在REGISTER_BEAN阶段执行(而非PARSE_CONFIGURATION)
// 确保其他@Configuration类已经处理完毕
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
MergedAnnotations annotations = metadata.getAnnotations();
// 处理@ConditionalOnBean
if (annotations.isPresent(ConditionalOnBean.class)) {
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata,
annotations, ConditionalOnBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
// 需要存在但不存在
return ConditionOutcome.noMatch(...);
}
}
// 处理@ConditionalOnMissingBean
if (annotations.isPresent(ConditionalOnMissingBean.class)) {
Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata,
annotations, ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
// 不应该存在但存在了
return ConditionOutcome.noMatch(...);
}
}
return ConditionOutcome.match(matchMessage);
}
}三、完整代码示例
3.1 自定义Starter的完整条件装配实现
// 假设我们在做一个分布式锁的starter
// 1. 定义条件:Redis可用时才装配
public class RedisAvailableCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 检查Jedis或Lettuce是否在classpath
ClassLoader classLoader = context.getClassLoader();
boolean jedisPresent = ClassUtils.isPresent("redis.clients.jedis.Jedis", classLoader);
boolean lettucePresent = ClassUtils.isPresent(
"io.lettuce.core.RedisClient", classLoader);
if (!jedisPresent && !lettucePresent) {
return false;
}
// 检查Redis连接配置是否存在
Environment env = context.getEnvironment();
return env.containsProperty("spring.redis.host") ||
env.containsProperty("spring.data.redis.host");
}
}
// 2. 分布式锁接口
public interface DistributedLockService {
boolean tryLock(String key, long timeout, TimeUnit timeUnit);
void unlock(String key);
}
// 3. Redis实现
@Service
public class RedisDistributedLockService implements DistributedLockService {
private final StringRedisTemplate redisTemplate;
private final String serverId = UUID.randomUUID().toString();
public RedisDistributedLockService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {
String lockKey = "lock:" + key;
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, serverId, timeout, timeUnit);
return Boolean.TRUE.equals(result);
}
@Override
public void unlock(String key) {
String lockKey = "lock:" + key;
String value = redisTemplate.opsForValue().get(lockKey);
if (serverId.equals(value)) {
redisTemplate.delete(lockKey);
}
}
}
// 4. 自动配置类
@AutoConfiguration
@ConditionalOnClass({StringRedisTemplate.class}) // Redis依赖存在
@ConditionalOnProperty(
prefix = "distributed.lock",
name = "enabled",
havingValue = "true",
matchIfMissing = true // 默认开启
)
@Configuration(proxyBeanMethods = false)
public class DistributedLockAutoConfiguration {
@Bean
@ConditionalOnMissingBean(DistributedLockService.class) // 用户没自定义时才装配
@ConditionalOnBean(StringRedisTemplate.class) // 需要RedisTemplate已存在
public DistributedLockService distributedLockService(StringRedisTemplate redisTemplate) {
return new RedisDistributedLockService(redisTemplate);
}
}注册自动配置(META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports):
com.example.lock.DistributedLockAutoConfiguration3.2 @ConditionalOnProperty的高级用法
@Configuration(proxyBeanMethods = false)
public class MonitoringConfig {
// 当 monitoring.type=prometheus 时装配
@Bean
@ConditionalOnProperty(name = "monitoring.type", havingValue = "prometheus")
public MeterRegistry prometheusRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
// 当 monitoring.type=influxdb 时装配
@Bean
@ConditionalOnProperty(name = "monitoring.type", havingValue = "influxdb")
public MeterRegistry influxDbRegistry(
@Value("${monitoring.influxdb.url}") String url) {
return new InfluxMeterRegistry(key -> url, Clock.SYSTEM);
}
// 当没有配置时,装配空实现(matchIfMissing=true)
@Bean
@ConditionalOnMissingBean(MeterRegistry.class)
public MeterRegistry noopRegistry() {
return new SimpleMeterRegistry();
}
}3.3 自定义组合条件注解
// 组合多个条件,只有都满足才装配
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnProductionEnvironmentCondition.class)
public @interface ConditionalOnProduction {
}
public class OnProductionEnvironmentCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
// 1. 必须是prod profile
String[] activeProfiles = env.getActiveProfiles();
boolean isProd = Arrays.asList(activeProfiles).contains("prod");
// 2. 必须配置了production.mode=true
boolean productionMode = env.getProperty("production.mode", Boolean.class, false);
return isProd && productionMode;
}
}
// 使用
@Bean
@ConditionalOnProduction // 只在生产环境装配的严格限流组件
public RateLimiter strictRateLimiter() {
return RateLimiter.create(100); // 生产环境限100 QPS
}四、踩坑实录
坑1:@ConditionalOnMissingBean检测不到用户自定义的Bean
现象:用户定义了DataSource Bean,但starter的@ConditionalOnMissingBean(DataSource.class)没有生效,两个DataSource都被创建了。
根因:用户的@Configuration类和starter的@AutoConfiguration类加载顺序问题。@AutoConfiguration类通过AutoConfigurationImportSelector在所有用户@Configuration之后加载,但如果用户的类也是通过@Import引入的,可能和@AutoConfiguration的处理顺序交叉。
解决方案:使用@AutoConfigureAfter明确指定依赖关系:
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class) // 在DataSource自动配置之后
@ConditionalOnMissingBean(DataSource.class)
public class MyDataSourceAutoConfiguration { ... }坑2:@ConditionalOnBean找不到在@Bean方法上定义的Bean
现象:@ConditionalOnBean(UserService.class),但UserService是通过@Bean方法定义的,条件判断时认为不存在。
根因:@ConditionalOnBean检查的是BeanDefinition注册时机。如果两个配置类在同一个处理批次,检查时对方的BeanDefinition可能还没注册。
解决:把依赖关系拆到不同的@Configuration类,并使用@AutoConfigureAfter排序;或者使用@DependsOn确保顺序。
坑3:条件调试困难——不知道哪个条件没满足
使用--debug参数启动,Spring Boot会打印所有自动配置的匹配/不匹配报告(ConditionEvaluationReport):
java -jar app.jar --debug日志中会看到类似:
CONDITIONS EVALUATION REPORT
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes...
Negative matches:
-----------------
MongoAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'com.mongodb.MongoClient'坑4:@ConditionalOnExpression写错SpEL表达式静默失败
// 错误:SpEL语法错误,但不会报错,条件默认为false(等价于不装配)
@ConditionalOnExpression("#{environment['app.mode'] == 'pro'}")
// 正确:
@ConditionalOnExpression("'${app.mode:dev}' == 'prod'")
// 或更安全的写法
@ConditionalOnExpression("#{environment.getProperty('app.mode', 'dev') == 'prod'}")五、总结与延伸
@Conditional是Spring Boot"约定大于配置"理念的技术基础。理解它的工作原理,你才能:
- 写出可靠的自定义starter
- 快速排查自动配置为什么没生效
- 设计出灵活的多环境配置策略
执行顺序总结:
- 条件在
ConfigurationClassParser解析阶段或REGISTER_BEAN阶段执行 @ConditionalOnMissingBean是REGISTER_BEAN阶段,用户Bean先注册- 多个
@Conditional注解是AND关系,任一不满足则跳过
下一篇聊Spring的ApplicationEvent事件驱动机制,看观察者模式在Spring中的完整实现。
