Spring中的@Conditional注解实现分析

 更新时间:2023年12月29日 09:21:14   作者:it_lihongmin  
这篇文章主要介绍了Spring中的@Conditional注解实现分析,  @Conditional是Spring 4出现的注解,但是真正露出价值的是Spring Boot的扩展@ConditionalOnBean等,需要的朋友可以参考下

@Conditional注解实现分析

@Conditional是Spring 4出现的注解,但是真正露出价值的是Spring Boot的扩展@ConditionalOnBean等。但是任然使用的是Spring框架进行处理,并没有做太多定制的东西,所以还是先看看@Conditional注解的实现。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
 
	/**
	 * All {@link Condition Conditions} that must {@linkplain Condition#matches match}
	 * in order for the component to be registered.
	 */
	Class<? extends Condition>[] value();
 
}

先看看@Conditional注解的结构比较简单,只需要定义一个Condition子类即可,并且说明只有满足了当前Condition的matches方法时才会将当前@Component注册成Bean。那么再看看Condition接口和体系。

/**
 * @since 4.0
 * @see ConfigurationCondition
 * @see Conditional
 * @see ConditionContext
 */
@FunctionalInterface
public interface Condition {
 
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

当前会传入ConditionContext和AnnotatedTypeMetadata进行回调,返回是否匹配,如果不匹配则不会注册成Bean。但是这是在哪里进行回调的呢?

ConfigurationClassParser # processConfigurationClass (ConfigurationClassParser # doProcessConfigurationClass等)

ConditionEvaluator # shouldSkip

比较清楚了,又是在处理@Import、@ComponentScan、ImportSelector等的处理类ConfigurationClassParser执行时机比较清楚了

再看看Condition的结构体系:

大致有四类

1)、ProfileCondition,项目启动后Profile信息存放在ApplicationContext的Environment中,能拿到两者之一就可以判断。

2)、ConfigurationCondition

public interface ConfigurationCondition extends Condition {
 
    // 定义了子类必须实现,返回下面枚举的一种
    ConfigurationPhase getConfigurationPhase();
 
    // 判断阶段
	enum ConfigurationPhase {
            
        // Conponent阶段,即将@Component加入到BeanFactory
        PARSE_CONFIGURATION,
 
        // 只有通过getBean才能正在将Bean注册到Ioc容器中。前提是要将BeanDefinition添加到 
        // BeanFactory中
        REGISTER_BEAN
    }
}

3)、ConditionEvalutionReport,Spring Boot报表相关

4)、SpringBootCondition,直接是Spring Boot中扩展的。下一篇博客,具体分析 @ConditionalOnBean等再具体分析。

几个的角色比较清楚了,只要一个@Conditional注解,注解的属性为@Condition或其子类。根据回调@Condition的matches方法,即可判断是否将注册成Bean。

先看看回调时机,都是在处理@Component、@ComponentSacn、ImportSelector等情况注册Bean时。

由于情况比较复杂,可能@Component上有@ComponentScan,则会递归进行处理,总之都会先调用ConfigurationClassParser的ConditionEvaluator conditionEvaluator的shouldSkip方法判断是否跳过。

比如:

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(),  
           ConfigurationPhase.PARSE_CONFIGURATION)) {
 
        return;
    }
 
    // 省略其余代码
}

而conditionEvaluator在ConfigurationClassParser的构造器中被初始化,如下:

this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);

先看看ConditionEvaluator的类结构

class ConditionEvaluator {
 
    private final ConditionContextImpl context;
 
    public ConditionEvaluator(@Nullable BeanDefinitionRegistry registry,
			@Nullable Environment environment, @Nullable ResourceLoader resourceLoader) {
 
		this.context = new ConditionContextImpl(registry, environment, resourceLoader);
	}
    
    // 省略其他方法
 
    private static class ConditionContextImpl implements ConditionContext {
 
        private final BeanDefinitionRegistry registry;
 
        private final ConfigurableListableBeanFactory beanFactory;
 
        private final Environment environment;
 
        private final ResourceLoader resourceLoader;
 
        private final ClassLoader classLoader;
 
        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);
        }
    }
}

也就是说,在ConfigurationClassParser构造器中初始化ConditionEvaluator时候,就初始化了内部类ConditionContextImpl,并且传入了BeanFactory(Bean是否存在可以通过工厂进行判断)、Environment(环境配置、Profile等存放在其中)、ResourceLoader(Spring的Resource加载器)、ClassLoader(类加载器)等。

到这里就比较清楚了,回调Condition的matches接口时传入这些组件的类ConditionContextImpl,要实现@ConditionalOnBean、@OnPropertyCondition、@Profile、@ConditionalOnClass等都比较简单了。

在调用Condition的matches时,不仅传入了ConditionContextImpl,还出入了AnnotatedTypeMetadata,这是当前注解结合被Spring封装的注解元信息。理解比较抽象,比如自动装配EmbeddedTomcat时需要同时存在Servlet、Tomcat、upgradeProtocol类;并且没有将ServletWebServerFactory注册成Bean,此时Component才能真正生效,如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {
    // 省略其他代码
}

具体再看看ConditionEvaluator的shouldSkip方法的实现:

public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 注解信息不能为空, 并且必须有Conditional或者其子类
    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<>();
    // 获取多有的Condition类型,如上面的EmbeddedTomcat同时需要多个条件成立
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }
    // 条件排序(根据Spring的那三个排序方式定义)
    AnnotationAwareOrderComparator.sort(conditions);
 
    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 判断阶段为空(非ConfigurationCondition的子类)、不需要判断阶段,则直接返回true
        // 否则才调用matches接口判断
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }
    return false;
}

1)、注解信息不能为空, 并且必须有Conditional或者其子类

2)、阶段为null,则根据情况设置阶段后再递归调用该方法

3)、获取所有的Condition列表

4)、进行排序

5)、遍历Condition,是否在该阶段进行判断。需要则再调用该Condition的matches方法

总结:添加了@Conditional或者@ConditionXXX注解,其value值会对应一个Condition或者子类的Class。在处理@ComponentScan、ImportSelector等时会根据判断阶段,调用Condition的matches方法判断是否进行注册成Bean。从而实现各种复杂的动态判断注册成Bean的情况。

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

相关文章

  • Spring Boot 4 与 Spring Framework 7新特性、升级要点与实战指南

    Spring Boot 4 与 Spring Framework&nb

    SpringBoot4和SpringFramework7带来了大量新特性,包括升级到JakartaEE11、支持Java21、增强模块化、优化测试、增加弹性功能等,本文介绍Spring Boot 4 与 Spring Framework 7新特性、升级要点与实战指南,感兴趣的朋友跟随小编一起看看吧
    2026-03-03
  • java+mysql实现登录和注册功能

    java+mysql实现登录和注册功能

    这篇文章主要为大家详细介绍了java+mysql实现登录和注册功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • java实现时钟表盘

    java实现时钟表盘

    这篇文章主要为大家详细介绍了java实现时钟表盘,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09
  • springboot部署linux访问服务器资源的方法

    springboot部署linux访问服务器资源的方法

    这篇文章主要介绍了springboot部署linux访问服务器资源,部署springboot项目至服务器用了几种不同方法,文中给大家详细介绍,需要的朋友可以参考下
    2019-12-12
  • Java实现时间戳转代码运行时长

    Java实现时间戳转代码运行时长

    这篇文章主要为大家详细介绍了如何使用Java实现时间戳转代码运行时长功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-06-06
  • Java限流方法常见实现方案(单机限流和分布式限流)

    Java限流方法常见实现方案(单机限流和分布式限流)

    Java限流用于保护系统资源,分为单机(Guava/滑动窗口)和分布式(Redis+Lua)方案,核心算法包括固定窗口、令牌桶、漏桶等,推荐使用Sentinel等成熟框架实现动态流量控制,本文介绍Java限流方法常见实现方案(单机限流和分布式限流),感兴趣的朋友一起看看吧
    2025-08-08
  • Java Stream流以及常用方法操作实例

    Java Stream流以及常用方法操作实例

    Stream是对Java中集合的一种增强方式,使用它可以将集合的处理过程变得更加简洁、高效和易读,这篇文章主要介绍了Java Stream流以及常用方法的相关资料,需要的朋友可以参考下
    2025-08-08
  • SpringBoot文件上传接口并发性能调优

    SpringBoot文件上传接口并发性能调优

    在一个项目现场,文件上传接口(文件500K)QPS只有30,这个并发性能确实堪忧,此文记录出坑过程,文中通过代码示例讲解的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2024-06-06
  • Java Set 中的常用方法示例总结

    Java Set 中的常用方法示例总结

    Set是Java集合框架(Collection Framework)中最常用的接口之一,具有元素唯一、不允许重复 的特性,这篇文章将全面总结Java Set的常用方法并通过示例进行讲解,主要用于日常查询使用,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • Java Math.round(),Math.ceil(),Math.floor()的区别详解

    Java Math.round(),Math.ceil(),Math.floor()的区别详解

    这篇文章主要介绍了Java Math.round(),Math.ceil(),Math.floor()的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08

最新评论