news 2026/9/13 20:12:49

Spring @Conditional 注解源码深度解析:从 ConditionEvaluator 到条件化 Bean 注册

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring @Conditional 注解源码深度解析:从 ConditionEvaluator 到条件化 Bean 注册

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)推导;environmentresourceLoader在显式传入时直接使用,否则分别通过deduceEnvironment(registry)deduceResourceLoader(registry)从注册表推导;classLoader则由resourceLoaderbeanFactory共同推导。也就是说,即使调用方只传入一个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)继承关系。isAnnotatedgetAnnotationAttributes均为默认方法,底层都委托给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的第一步是快速失败判断:如果metadatanull,或目标上没有标注@Conditional注解,直接返回false(不跳过,正常注册)。

第二阶段:phase 为 null 时自动推导阶段

ConfigurationPhaseConfigurationCondition接口中定义的枚举,标记条件在哪个阶段生效:

  • PARSE_CONFIGURATION(配置类解析阶段):在ConfigurationClassParser解析@Configuration类时执行条件判断,此时@Bean方法尚未处理,适用于"决定某个配置类整体是否加载";
  • REGISTER_BEAN(Bean 注册阶段):在配置类解析完成后、Bean 注册时执行,适用于"决定某个@Bean方法产生的 Bean 是否注册"。

当调用方没有显式传入phase时,Spring 会根据metadata的类型自动推导:

  • metadataAnnotationMetadataConfigurationClassUtils.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

  1. 若条件实现了ConfigurationCondition,则取出其声明的requiredPhase
  2. 只有当requiredPhasenull(即普通Condition,任何阶段都参与)或与当前phase相等时,才执行matches
  3. 一旦某个条件matches返回falseshouldSkip立即返回true(跳过注册);
  4. 所有条件都通过,才返回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())(此时phasenull,由 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返回falseshouldSkip返回true,于是bean2不存在、连BeanTwoConfiguration这个配置类本身都没有被注册为 Bean(isFalse)。

这组断言从"最终 Bean 状态"层面验证了:实例化BeanTwoConfiguration时,Spring 会去执行NoBeanOneCondition.matches方法,返回false即整体跳过

实战:自定义 Condition 实现条件化配置

基于上面的源码机制,自定义一个条件配置只需三步:

  1. 实现Condition接口,编写matches逻辑;
  2. 在配置类或@Bean方法上标注@Conditional(你的条件类.class)
  3. 交给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")
  • 按类是否在 classpathcontext.getClassLoader().loadClass("com.xxx.Yyy")
  • 按资源是否存在context.getResourceLoader().getResource("classpath:xxx.xml")

如需控制条件执行阶段,可让条件类实现ConfigurationCondition并覆写getConfigurationPhase(),指定在PARSE_CONFIGURATIONREGISTER_BEAN阶段生效。

延伸:Spring Boot 对 @Conditional 的体系化扩展

理解了 Spring 框架层的@ConditionalCondition之后,再看 Spring Boot 的条件化自动装配就一目了然了。仓库中的 SpringBoot-ConditionalOnBean.md 一文专门剖析了 Spring Boot 在这套机制之上的完整扩展,核心要点如下。

一系列 ConditionalOnXxx 注解

Spring Boot 在@Conditional基础上衍生出一整套开箱即用的条件注解:

ConditionalOnBeanConditionalOnClassConditionalOnCloudPlatformConditionalOnExpressionConditionalOnJavaConditionalOnJndiConditionalOnMissingBeanConditionalOnMissingClassConditionalOnNotWebApplicationConditionalOnPropertyConditionalOnResourceConditionalOnSingleCandidateConditionalOnWebApplication

它们本质都是元注解式的@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:模板方法模式的骨架

OnBeanConditionOnClassConditionOnWebApplicationCondition等条件类都继承自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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 20:12:14

SteamOS深度适配指南:硬件兼容性与Linux游戏生态实战

1. SteamOS不是“装个系统”那么简单&#xff1a;它本质是一套游戏终端操作系统重构方案最近刷到不少标题党文章&#xff0c;说什么“SteamOS全面开放&#xff0c;普通电脑秒变Steam游戏机”&#xff0c;点进去一看全是复制粘贴的安装步骤&#xff0c;连BIOS里Secure Boot关不关…

作者头像 李华
网站建设 2026/9/13 20:12:09

数组清零底层原理与多语言实现:从memset到fill

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 20:10:42

豆瓣电影爬虫与Spark数据分析可视化实战

简介&#xff1a;一份基于Python和Spark的豆瓣电影爬虫与数据分析可视化系统&#xff0c;适合毕业设计、期末大作业和课程设计场景。项目完整覆盖从网页爬虫、数据清洗、Spark批量统计到前端可视化展示的整个流程&#xff0c;面向想快速搭建大数据分析应用的Python和Spark初学者…

作者头像 李华
网站建设 2026/9/13 20:07:11

零基础学PLC多久能独立调试产线设备?87天实证路径

1. 这个问题我被问了至少237次——0基础学PLC到底要多久&#xff1f;不是“看教程”那种虚的&#xff0c;是真能上手改程序、查故障、调变频器的时间“0基础学PLC要多久&#xff1f;”——这句话背后藏着三类人&#xff1a;刚毕业想转行的机械/电气大专生&#xff0c;干了十年电…

作者头像 李华
网站建设 2026/9/13 20:06:35

AI市场调研:从数据到决策的智能分析实践

1. 从问卷汇总到决策洞察的进化之路市场调研领域长期存在一个痛点&#xff1a;90%的企业调研报告停留在原始数据堆砌阶段。我见过太多这样的案例——市场部同事花费两周时间回收500份问卷&#xff0c;最终呈现的却是30页Excel表格截图配以简单的百分比计算。这种"问卷汇总…

作者头像 李华