Spring中的@Conditional注解使用和原理详解

 更新时间:2024年01月03日 10:34:15   作者:落魄的小开发  
这篇文章主要介绍了Spring中的@Conditional注解使用和原理详解,@Conditional在Spring4.0中被引入,用于开发"If-Then-Else"类型的bean注册条件检查,在@Conditional之前,也有一个注解@Porfile起到类似的作用,需要的朋友可以参考下

前言

熟悉 SpringBoot 的小伙伴们肯定不会对 @Conditional 注解感到陌生,它在 SpringBoot 的自动化配置特性中起到了非常重要的作用。

许多配置类在加载 Bean 时都使用到了 @ConditionalOnClass、@ConditionalOnBean,@ConditionalOnProperty 等 @Conditional 的衍生注解。

那么,在单纯的 Spring 项目中,我们是否也可以使用 @Conditional 来实现一些自动化配置的特性呢?

我们该怎么样去使用@Conditional? 它又是如何生效的?

别着急,本篇文章会一一解答。

概述

@Conditional 在 Spring 4.0 中被引入,用于开发 “If-Then-Else” 类型的 bean 注册条件检查。在 @Conditional 之前,也有一个注解 @Porfile 起到类似的作用,它们两个的区别在于:

  • @Profile 仅用于基于环境变量的条件检查,使用范围比较窄。
  • @Conditional 更加通用,开发人员可以自定义条件检查策略。可用于 bean 注册时的条件检查。
  • 4.3.8后,@Profile 也基于 @Conditional 来实现。

用法

首先来看一下源码中 @Conditional 的定义

package org.springframework.context.annotation;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

根据定义, @Conditional 可以使用在类或方法上,具体的用法有:

  • 作为类注解,标注在直接或间接使用了 @Component 的类上,包括 @Configuration 类
  • 作为元注解,直接标注在其他的注解上面,用于编写自定义注解
  • 作为任何 @Bean 方法的方法级注解

@Conditional 有一个属性 value,其类型是 Condition 数组。组件必须匹配数组中所有的 Condition,才可以被注册。

package org.springframework.context.annotation;
@FunctionalInterface
public interface Condition {
    /**
	 * 判断条件是否匹配
	 * @param context 上下文信息,可以从中获取 BeanDefinitionRegistry,BeanFactory,Environment,ResourceLoader,ClassLoader 等一些用于资源加载的信息
	 * @param metadata 注解的元信息,可以从中获取注解的属性
	 * @return {@code true} 条件匹配,组件可以注册
	 * or {@code false} 否决组件的注册
	 */
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

Condition 是一个函数式接口,只有一个 matches 方法,返回 true 则表示条件匹配。matches 方法的两个参数分别是上下文信息和注解的元信息,从这两个参数中可以获取到 IOC 容器和当前组件的信息,从而判断条件是否匹配。 由于 ConditionContext 和 AnnotatedTypeMetadata 的方法都比较简单,这里就不贴出源码了,有兴趣的小伙伴可自行翻看源码。 Condition 必须遵循与 BeanFactoryPostProcessor 相同的限制,并注意永远不要与 bean 实例交互。如果要对与 @Configuration bean 交互的条件进行更细粒度的控制,可以考虑 ConfigurationCondition 接口。

public interface ConfigurationCondition extends Condition {
    /**
	 * 返回条件生效的阶段
	 */
    ConfigurationPhase getConfigurationPhase();
    enum ConfigurationPhase {
        /**
		 * 在 @Configuration 类解析时生效
		 */
        PARSE_CONFIGURATION,
        /**
		 * 在 bean 注册时生效。此时所有的 @Configuration 都解析完成了。
		 */
        REGISTER_BEAN
        }
}

接下来我们在 Spring 下实现一个简单的 ConditionalOnBean 注解,实现一个 bean 只有在另一个 bean 存在时,才进行注册。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// Conditional 作为元注解,主要的判断逻辑在 OnBeanCondition 类中
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
    // bean 的名称
    String[] name() default {};
}
// OnBeanCondition 主要的判断逻辑在 matches 方法中
class OnBeanCondition implements ConfigurationCondition {
    @Override
	public ConfigurationPhase getConfigurationPhase() {
		return ConfigurationPhase.REGISTER_BEAN;
	}
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
            MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ConditionalOnShardingProps.class.getName());
            if (attrs != null) {
                ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
                for (Object beanName : attrs.get("name")) {
                    if(!beanFactory.containsBean((String) beanName)) {
                        return false;
                    }
                }
                return true;
            }
        }
        return true;
    }
}
// 使用 ConditionalOnBean 注解
@Configuration
@Conditional(ConditionalOnBean.class)
public static class OnBeanConfig {
    @Bean
    @ConditionalOnBean(name = "a")
    public B b() {
        return new B();
    }
}

这样,一个自定义的 Conditional 注解就写好了,使用时只要把它加到类或方法上即可生效。

原理

首先,通过调用链路的分析可知,Conditional 的调用方是 ConditionEvaluator,而 ConditionEvaluator 在 ConfigurationClassParser、ConfigurationClassBeanDefinitionReader 和 AnnotatedBeanDefinitionReader 中均有所使用。先来看下这三个类在 Spring 的流程中扮演什么角色。 ConfigurationClassParser ConfigurationClassParser 在 ConfigurationClassPostProcessor 中被使用到,而 ConfigurationClassPostProcessor 是一个 BeanDefinitionRegistryPostProcessor,顾名思义,就是在 bean 扫描完成后,对 bean 的定义进行修改的一个后置处理器,主要的功能在于解析 bean 中的所有配置类。

// ConfigurationClassParser 的核心逻辑
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    // 调用 shouldSkip 方法,对应的阶段是 PARSE_CONFIGURATION
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
			return;
	}
    ... 省略
}

ConfigurationClassBeanDefinitionReader

ConfigurationClassBeanDefinitionReader 也是在 ConfigurationClassPostProcessor 中被使用到。在配置类解析完成后,对其中包含的 bean 进行注册。

private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    // 调用的还是 conditionEvaluator.shouldSkip,在其基础上做了个缓存。
    // 对应的阶段是 REGISTER_BEAN
    if (trackedConditionEvaluator.shouldSkip(configClass)) {
        String beanName = configClass.getBeanName();
        if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
            this.registry.removeBeanDefinition(beanName);
        }
        this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
        return;
    }
    ... 省略
}

AnnotatedBeanDefinitionReader

AnnotatedBeanDefinitionReader 主要在 AnnotationConfigApplicationContext 中被使用到。AnnotationConfigApplicationContext 是 Spring 中的一个高级容器,与 ClassPathXmlApplicationContext 不同的是,它主要通过解析 Java 配置文件中的配置,来进行 bean 的注册。

<T> void doRegisterBean(Class<T> beanClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) {
    AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
    // 调用 shouldSkip 方法,对应的阶段为 null
    if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
        return;
    }
    ... 省略
}

综上,我们已经了解了 ConditionEvaluator 在 Spring 的流程中是如何发挥作用的,接下来看看核心方法 shouldSkip 的具体实现逻辑。

public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 不存在 Conditional 注解,则不处理
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }
    // 阶段为空时的处理逻辑
    if (phase == null) {
        // 有 Configuration、Component、ComponentScan、Import、ImportResource 等注解,则任务是配置解析阶段
        if (metadata instanceof AnnotationMetadata &&
            ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
            return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
        }
        return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    }
    // 获取所有 Conditional 注解,并提取出 Condition 类
    List<Condition> conditions = new ArrayList<>();
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }
    // 对 Condition 进行排序
    AnnotationAwareOrderComparator.sort(conditions);
    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 调用 Condition 的 matches 方法,不符合条件的则跳过
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }
    // 所有的 Condition 都符合,则不跳过,进行后续处理
    return false;
}

看了源代码,相信小伙伴们对 Conditional 的理解又深入了一层。

  • Conditional 可以用作元注解加在自定义注解之上。
  • Spring 在解析配置类或者注册 bean 时,都会调用 ConditionEvaluator#shouldSkip 方法,判断是否符合注册条件。
  • shouldSkip 会获取到组件上的所有 Conditional 注解,并拿到注解上的所有 Condition 类,调用 Condition#matches 进行判断。
  • Condition 默认按照定义的顺序来执行,一般通过 @Order 对Condition 进行排序。
  • 只有所有条件都符合,Spring 才会进行后续的处理流程。

总结

  • @Conditional 注解用于开发 “If-Then-Else” 类型的 bean 注册条件检查。
  • @Conditional 可以使用在类或方法上,具体的用法有三种:
    • 作为类注解,标注在直接或间接使用了 @Component 的类上,包括 @Configuration 类
    • 作为元注解,直接标注在其他的注解上面,用于编写自定义注解
    • 作为任何 @Bean 方法的方法级注解
  • @Conditional 在解析配置类和注册 bean 这两个阶段生效。可以通过 ConfigurationCondition 指定阶段。
  • Condition 默认按照定义的顺序来执行,一般通过 @Order 对Condition 进行排序。
  • 组件必须匹配所有的 Condition,才可以被注册。

到此这篇关于Spring中的@Conditional注解使用和原理详解的文章就介绍到这了,更多相关Spring中的@Conditional注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java实现的生成二维码和解析二维码URL操作示例

    Java实现的生成二维码和解析二维码URL操作示例

    这篇文章主要介绍了Java实现的生成二维码和解析二维码URL操作,结合实例形式分析了Java创建与解析二维码,以及文件读写等相关操作技巧,需要的朋友可以参考下
    2018-07-07
  • Maven打包没有指定主类问题(xxx.jar中没有主清单属性)

    Maven打包没有指定主类问题(xxx.jar中没有主清单属性)

    这篇文章主要介绍了Maven打包没有指定主类问题(xxx.jar中没有主清单属性),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • 深入探究SpringBoot可以同时处理多少请求

    深入探究SpringBoot可以同时处理多少请求

    SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序,但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在,在本篇文章中,我们将深入探讨这个问题,需要的朋友可以参考下
    2023-07-07
  • Java中的类初始化解析

    Java中的类初始化解析

    这篇文章主要介绍了Java中的类初始化解析,类的初始化是一个Java类生命周期中的其中一个阶段,初始化是类加载的最后一个阶段,也正是在初始化阶段,才会真正开始执行类中所写的Java代码,需要的朋友可以参考下
    2023-08-08
  • java 对ArrayList进行分页实例代码

    java 对ArrayList进行分页实例代码

    这篇文章主要介绍了java 对ArrayList进行分页实例代码的相关资料,需要的朋友可以参考下
    2017-02-02
  • Java8的Optional如何干掉空指针(示例详解)

    Java8的Optional如何干掉空指针(示例详解)

    这篇文章主要介绍了Java8的Optional如何干掉空指针,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • MyBatis-Plus自动填充功能失效导致的原因及解决

    MyBatis-Plus自动填充功能失效导致的原因及解决

    这篇文章主要介绍了MyBatis-Plus自动填充功能失效导致的原因及解决,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • 详解Java中自定义注解的使用

    详解Java中自定义注解的使用

    Annontation是Java5开始引入的新特征,中文名称叫注解,它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。本文主要介绍了自定义注解的使用,希望对大家有所帮助
    2023-03-03
  • 浅谈springBoot注解大全

    浅谈springBoot注解大全

    本篇文章主要介绍了浅谈springBoot注解大全,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03
  • Java读取其下所有文件夹与文件路径的方法

    Java读取其下所有文件夹与文件路径的方法

    这篇文章主要为大家详细介绍了Java读取其下所有文件夹与文件路径的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03

最新评论