Spring @Conditional 注解源码深度解析:从 ConditionEvaluator 到条件化 Bean 注册
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
导读
本文基于当前仓库 docs/Spring/clazz/Spring-Conditional.md 的源码阅读笔记,深入剖析 Spring 框架@Conditional条件注解的底层实现原理。@Conditional是 Spring 条件化配置的基石,也是 Spring Boot 自动装配(@ConditionalOnClass、@ConditionalOnBean等)赖以运转的核心机制。读完本文,你将掌握@Conditional的注解定义、Condition匹配器的执行流程、ConditionEvaluator.shouldSkip的两阶段跳过逻辑,并能独立编写自定义条件配置。
认识核心注解与接口
@Conditional 注解定义
@Conditional是一个作用于类型(ElementType.TYPE)和方法(ElementType.METHOD)的运行时注解,它的唯一属性是多个条件匹配器类:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { /** * 多个匹配器接口 */ Class<? extends Condition>[] value(); }它既可以标注在@Configuration配置类上,也可以标注在@Bean方法上,从而决定"整个配置类"或"单个 Bean 方法"是否参与容器初始化。
Condition 匹配器接口
Condition是一个函数式接口(@FunctionalInterface),只有一个核心方法matches:
@FunctionalInterface public interface Condition { /** * 匹配,如果匹配返回true进行初始化,返回false跳过初始化 */ boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }方法签名中的两个参数是条件判断的全部信息来源:
ConditionContext context:条件上下文,封装了 BeanDefinition 注册表、BeanFactory、Environment 环境、资源加载器、类加载器等容器运行时信息;AnnotatedTypeMetadata metadata:注解元数据,描述了当前被@Conditional标注的类或方法上的注解信息。
只要matches返回false,Spring 就会跳过对应配置类或 Bean 的初始化;返回true则正常注册。
两个关键参数:ConditionContext 与 AnnotatedTypeMetadata
ConditionContext:条件判断的"运行环境"
public interface ConditionContext { /** * bean的定义 */ BeanDefinitionRegistry getRegistry(); /** * bean 工厂 */ @Nullable ConfigurableListableBeanFactory getBeanFactory(); /** * 环境 */ Environment getEnvironment(); /** * 资源加载器 */ ResourceLoader getResourceLoader(); /** * 类加载器 */ @Nullable ClassLoader getClassLoader(); }五个方法分别暴露了容器中五个维度的资源:
| 方法 | 返回类型 | 用途 |
|---|---|---|
getRegistry() | BeanDefinitionRegistry | 读取/注册 BeanDefinition,判断某个 Bean 是否已定义 |
getBeanFactory() | ConfigurableListableBeanFactory | 操作 BeanFactory,查询 BeanDefinition 等 |
getEnvironment() | Environment | 读取系统属性、环境变量、配置文件中的属性 |
getResourceLoader() | ResourceLoader | 加载资源,配合@ConditionalOnResource等场景 |
getClassLoader() | ClassLoader | 判断某个类是否存在于 classpath(配合@ConditionalOnClass场景) |
唯一实现是内部类:org.springframework.context.annotation.ConditionEvaluator.ConditionContextImpl。其构造方法会在创建时根据传入参数推导出完整的上下文信息:
public ConditionContextImpl(@Nullable BeanDefinitionRegistry registry, @Nullable Environment environment, @Nullable ResourceLoader resourceLoader) { this.registry = registry; this.beanFactory = deduceBeanFactory(registry); this.environment = (environment != null ? environment : deduceEnvironment(registry)); this.resourceLoader = (resourceLoader != null ? resourceLoader : deduceResourceLoader(registry)); this.classLoader = deduceClassLoader(resourceLoader, this.beanFactory); }从源码结构看,registry直接透传;beanFactory通过deduceBeanFactory(registry)推导;environment、resourceLoader在显式传入时直接使用,否则分别通过deduceEnvironment(registry)与deduceResourceLoader(registry)从注册表推导;classLoader则由resourceLoader与beanFactory共同推导。也就是说,即使调用方只传入一个BeanDefinitionRegistry,容器也能把其余四项环境信息补齐。
AnnotatedTypeMetadata:注解元数据
public interface AnnotatedTypeMetadata { /** * 获取所有注解 */ MergedAnnotations getAnnotations(); /** * 是否有注解 */ default boolean isAnnotated(String annotationName) { return getAnnotations().isPresent(annotationName); } /** * 获取注解的属性 */ @Nullable default Map<String, Object> getAnnotationAttributes(String annotationName) { return getAnnotationAttributes(annotationName, false); } }这是一个元数据接口,Spring 通过它对外暴露"被评估的类/方法上标注了哪些注解、注解属性是什么"。MergedAnnotations是 Spring 5.2 引入的合并注解视图,能统一处理注解的@AliasFor别名与元注解(meta-annotation)继承关系。isAnnotated与getAnnotationAttributes均为默认方法,底层都委托给getAnnotations()。
源码核心:ConditionEvaluator.shouldSkip 两阶段跳过逻辑
条件判断的核心入口是org.springframework.context.annotation.ConditionEvaluator#shouldSkip,它决定了"要不要跳过这个配置类或 Bean 的注册":
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } if (phase == null) { if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } List<Condition> conditions = new ArrayList<>(); // 获取注解 Conditional 的属性值 for (String[] conditionClasses : getConditionClasses(metadata)) { for (String conditionClass : conditionClasses) { // 序列化成注解 Condition condition = getCondition(conditionClass, this.context.getClassLoader()); // 插入注解列表 conditions.add(condition); } } AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } // matches 进行验证 if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } return false; }第一阶段:没有标注 @Conditional,直接跳过
shouldSkip的第一步是快速失败判断:如果metadata为null,或目标上没有标注@Conditional注解,直接返回false(不跳过,正常注册)。
第二阶段:phase 为 null 时自动推导阶段
ConfigurationPhase是ConfigurationCondition接口中定义的枚举,标记条件在哪个阶段生效:
PARSE_CONFIGURATION(配置类解析阶段):在ConfigurationClassParser解析@Configuration类时执行条件判断,此时@Bean方法尚未处理,适用于"决定某个配置类整体是否加载";REGISTER_BEAN(Bean 注册阶段):在配置类解析完成后、Bean 注册时执行,适用于"决定某个@Bean方法产生的 Bean 是否注册"。
当调用方没有显式传入phase时,Spring 会根据metadata的类型自动推导:
- 若
metadata是AnnotationMetadata且ConfigurationClassUtils.isConfigurationCandidate判定其为配置类候选(即标注了@Configuration或@Component系列注解),则按PARSE_CONFIGURATION阶段执行; - 否则按
REGISTER_BEAN阶段执行。
第三阶段:加载并排序所有 Condition
Spring 通过getConditionClasses(metadata)读取@Conditional注解的value属性(多个条件类),然后用getCondition实例化每个条件类(该过程会合并元注解,即支持通过元注解间接标注@Conditional)。实例化后的条件列表会用AnnotationAwareOrderComparator.sort(conditions)排序——这意味着条件类可以通过实现Ordered接口或标注@Order注解来控制判断先后顺序,这一点在 Spring Boot 的组合条件场景中非常重要。
第四阶段:逐条执行 matches
遍历排序后的条件列表,对每个Condition调用matches:
- 若条件实现了
ConfigurationCondition,则取出其声明的requiredPhase; - 只有当
requiredPhase为null(即普通Condition,任何阶段都参与)或与当前phase相等时,才执行matches; - 一旦某个条件
matches返回false,shouldSkip立即返回true(跳过注册); - 所有条件都通过,才返回
false(不跳过)。
也就是说,@Conditional的多个条件之间是"AND"关系——任意一个不满足,整个配置即被跳过。
调用链:条件判断发生在 Bean 注册的最前面
ConditionEvaluator.shouldSkip的调用点位于org.springframework.context.annotation.AnnotatedBeanDefinitionReader#doRegisterBean——这是注册 Bean 时执行的第一个方法:
private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] customizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); // 和条件注解相关的函数 if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } // 省略其他 }流程非常清晰:doRegisterBean先把目标类包装成AnnotatedGenericBeanDefinition,紧接着调用shouldSkip(abd.getMetadata())(此时phase为null,由 Spring 自动推导)。若返回true则直接return,后续的 BeanDefinition 注册、依赖注入统统不再执行;只有通过条件判断的 Bean 才会继续走完注册流程。
可以推断,shouldSkip的调用点不限于AnnotatedBeanDefinitionReader:在ConfigurationClassParser解析配置类、ClassPathBeanDefinitionScanner扫描组件时,同样会通过ConditionEvaluator做条件过滤,这也正是@Conditional能作用于扫描组件、配置类、@Bean方法等多个场景的原因。
官方测试用例验证:ConfigurationClassWithConditionTests
Spring 官方针对该机制提供了专门的测试类org.springframework.context.annotation.ConfigurationClassWithConditionTests,仓库笔记中摘录了其中conditionalOnMissingBeanMatch用例,直接印证了"条件不满足则跳过注册"的完整行为:
@Test public void conditionalOnMissingBeanMatch() throws Exception { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(BeanOneConfiguration.class, BeanTwoConfiguration.class); ctx.refresh(); assertThat(ctx.containsBean("bean1")).isTrue(); assertThat(ctx.containsBean("bean2")).isFalse(); assertThat(ctx.containsBean("configurationClassWithConditionTests.BeanTwoConfiguration")).isFalse(); }配套的两个配置类与条件类:
@Configuration static class BeanOneConfiguration { @Bean public ExampleBean bean1() { return new ExampleBean(); } } @Configuration @Conditional(NoBeanOneCondition.class) static class BeanTwoConfiguration { @Bean public ExampleBean bean2() { return new ExampleBean(); } } static class NoBeanOneCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return !context.getBeanFactory().containsBeanDefinition("bean1"); } }用例断言揭示的结论:
BeanOneConfiguration无条件注册,bean1存在(isTrue);BeanTwoConfiguration标注了@Conditional(NoBeanOneCondition.class),其条件逻辑是"容器中不存在名为bean1的 BeanDefinition 时才匹配";- 由于
bean1已存在,NoBeanOneCondition.matches返回false,shouldSkip返回true,于是bean2不存在、连BeanTwoConfiguration这个配置类本身都没有被注册为 Bean(isFalse)。
这组断言从"最终 Bean 状态"层面验证了:实例化BeanTwoConfiguration时,Spring 会去执行NoBeanOneCondition.matches方法,返回false即整体跳过。
实战:自定义 Condition 实现条件化配置
基于上面的源码机制,自定义一个条件配置只需三步:
- 实现
Condition接口,编写matches逻辑; - 在配置类或
@Bean方法上标注@Conditional(你的条件类.class); - 交给
AnnotationConfigApplicationContext加载并refresh()。
一个可运行的完整示例(基于官方测试类改写):
public class ConditionalDemo { public static class ExampleBean { } // 条件:只有配置了 jdbc.url 属性时才生效 public static class OnJdbcUrlCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return context.getEnvironment().containsProperty("jdbc.url"); } } @Configuration @Conditional(OnJdbcUrlCondition.class) public static class JdbcConfiguration { @Bean public ExampleBean exampleBean() { return new ExampleBean(); } } public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); // 不设置 jdbc.url:JdbcConfiguration 被跳过 ctx.register(JdbcConfiguration.class); ctx.refresh(); System.out.println(ctx.containsBean("exampleBean")); // false } }条件类中可以自由组合ConditionContext提供的五类资源:
- 按环境属性:
context.getEnvironment().getProperty("xxx"); - 按 Bean 是否存在:
context.getBeanFactory().containsBeanDefinition("beanName"); - 按类是否在 classpath:
context.getClassLoader().loadClass("com.xxx.Yyy"); - 按资源是否存在:
context.getResourceLoader().getResource("classpath:xxx.xml")。
如需控制条件执行阶段,可让条件类实现ConfigurationCondition并覆写getConfigurationPhase(),指定在PARSE_CONFIGURATION或REGISTER_BEAN阶段生效。
延伸:Spring Boot 对 @Conditional 的体系化扩展
理解了 Spring 框架层的@Conditional与Condition之后,再看 Spring Boot 的条件化自动装配就一目了然了。仓库中的 SpringBoot-ConditionalOnBean.md 一文专门剖析了 Spring Boot 在这套机制之上的完整扩展,核心要点如下。
一系列 ConditionalOnXxx 注解
Spring Boot 在@Conditional基础上衍生出一整套开箱即用的条件注解:
ConditionalOnBean、ConditionalOnClass、ConditionalOnCloudPlatform、ConditionalOnExpression、ConditionalOnJava、ConditionalOnJndi、ConditionalOnMissingBean、ConditionalOnMissingClass、ConditionalOnNotWebApplication、ConditionalOnProperty、ConditionalOnResource、ConditionalOnSingleCandidate、ConditionalOnWebApplication
它们本质都是元注解式的@Conditional。以@ConditionalOnBean为例:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnBeanCondition.class) public @interface ConditionalOnBean { Class<?>[] value() default {}; // 需要匹配的 bean 类型 String[] type() default {}; // 需要匹配的 bean 类型(字符串形式) Class<? extends Annotation>[] annotation() default {}; // 匹配的 bean 注解 String[] name() default {}; // 需要匹配的 beanName SearchStrategy search() default SearchStrategy.ALL; // 搜索策略 Class<?>[] parameterizedContainer() default {}; // 泛型容器 }其中SearchStrategy枚举决定了 Bean 搜索范围:
public enum SearchStrategy { CURRENT, // 当前上下文 ANCESTORS, // 找所有的父容器 ALL // 当前上下文 + 父容器 }SpringBootCondition:模板方法模式的骨架
OnBeanCondition、OnClassCondition、OnWebApplicationCondition等条件类都继承自org.springframework.boot.autoconfigure.condition.SpringBootCondition,它把Condition.matches固化成了模板方法:
@Override public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String classOrMethodName = getClassOrMethodName(metadata); try { // 比较类,子类实现 ConditionOutcome outcome = getMatchOutcome(context, metadata); // 日志输出 logOutcome(classOrMethodName, outcome); // 报告记录(供 ConditionEvaluationReport / debug 使用) recordEvaluation(context, classOrMethodName, outcome); // 返回匹配结果 return outcome.isMatch(); } catch (NoClassDefFoundError ex) { /* 类缺失时的兜底处理 */ } catch (RuntimeException ex) { /* 统一异常包装 */ } }子类只需实现抽象方法getMatchOutcome(context, metadata),返回封装了match布尔值与ConditionMessage说明信息的ConditionOutcome。这样既统一了日志输出、评估报告记录等横切逻辑,又保证了条件判断的可调试性(Spring Boot 的ConditionEvaluationReport正是依赖recordEvaluation把每个自动配置类的命中/未命中原因记录在案,供启动时--debug查看)。
自动装配阶段的三级过滤
在 Spring Boot 启动阶段,AutoConfigurationImportSelector#filter(详见 SpringBoot-自动装配.md)会从spring.factories中加载AutoConfigurationImportFilter实现:
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ org.springframework.boot.autoconfigure.condition.OnBeanCondition,\ org.springframework.boot.autoconfigure.condition.OnClassCondition,\ org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition这组过滤器会在候选自动配置类批量导入前先行筛掉不满足条件的类,避免无意义的类加载;随后,真正注册每个配置类时,框架层的ConditionEvaluator.shouldSkip仍会再次执行@Conditional判断,形成"批量预过滤 + 逐个精细判断"的两级防线。
组合条件实战示例
在条件类上还可以叠加@Order控制判断顺序,例如:
@Component public class Beans { @Bean public A a() { return new A(); } @Bean @ConditionalOnBean(value = A.class) // 容器中存在 A 类型 Bean 才注册 B public B b() { return new B(); } }再如MessageSourceAutoConfiguration中同时使用@ConditionalOnMissingBean(name = "messageSource", search = SearchStrategy.CURRENT)、@Conditional(ResourceBundleCondition.class)等多重条件组合,充分展示了这套条件机制的灵活度。
关联阅读
- Spring-Conditional.md(本文原始笔记)
- SpringBoot-ConditionalOnBean.md:Spring Boot 条件注解全剖析
- SpringBoot-自动装配.md:自动配置类的候选、过滤与导入全流程
- Spring-BeanFactoryPostProcessor.md:BeanDefinition 注册后的定制扩展点
- Spring-scan.md:组件扫描与 BeanDefinition 生成的另一条注册路径
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考