《SpringBoot系列十三》:图文精讲@Conditional条件装配实现原理

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 《SpringBoot系列十三》:图文精讲@Conditional条件装配实现原理

@[TOC]

一、前言

在前一篇博文:《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理(两万字图文源码分析)(含@ConditionalOnClass原理),聊了Spring自动装配时做的条件装配,其中@ConditionalOnBean实现的条件装配:居然不是根据Bean是否存在于Spring容器中来判断,而是和@ConditionalOnClass一样依靠类是否能被加载来判断。

本文着重讨论@Conditional各类衍生注解实现条件装配的原理。

注:Spring Boot版本:2.3.7.RELEASE(博主写博客时最新Spring-boot版本 -- 2.6.X代码逻辑几乎一样)

二、@Conditional简介和使用

@Conditional注解是从spring4.0版本才有的,其是一个条件装配注解,可以用在任何类型或者方法上面,以指定的条件形式限制bean的创建;即当所有条件都满足的时候,被@Conditional标注的目标才会被spring容器处理。

  1. @Conditional本身也是一个父注解,从SpringBoot1.0版本开始派生出了大量的子注解;用于Bean的按需加载。
  2. @Conditional注解和其所有子注解必须依托于被@Component衍生注解标注的类,即Spring要能扫描到@Conditional衍生注解所在的类,才能做进一步判断。
  3. @Conditional衍生注解可以加在类 或 类的方法上;加在类上表示类的所有方法都做条件装配、加在方法上则表示只有当前方法做条件装配。

使用方式参考博文《SpringBoot系列十一》:精讲如何使用@Conditional系列注解做条件装配

自定义条件装配参考博文《SpringBoot系列十二》:如何自定义条件装配

三、条件装配什么时候执行?

Spring Boot对ConfigurationClass配置类的处理分为2个阶段:配置类解析阶段、配置类注册为BeanDefinition阶段。
在这里插入图片描述

1、什么是ConfigurationClass配置类?

当一个类符合下列条件时:

  1. 类上有@Component注解(或者说间接被@Component标注);
  2. 类上有@CompontentScan注解;
  3. 类上有@Import注解;
  4. 类上有@ImportResource注解;
  5. 类中有@Bean标注的方法。

1)如何判断一个类是不是配置类?

org.springframework.context.annotation.ConfigurationClassUtils类提供了一个isConfigurationCandidate(AnnotationMetadata)方法用于判断一个类是不是配置类。

abstract class ConfigurationClassUtils {
    private static final Set<String> candidateIndicators = new HashSet<>(8);

    static {
        candidateIndicators.add(Component.class.getName());
        candidateIndicators.add(ComponentScan.class.getName());
        candidateIndicators.add(Import.class.getName());
        candidateIndicators.add(ImportResource.class.getName());
    }
    ...

    public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
        // Do not consider an interface or an annotation...
        if (metadata.isInterface()) {
            return false;
        }

        // Any of the typical annotations found?
        for (String indicator : candidateIndicators) {
            if (metadata.isAnnotated(indicator)) {
                return true;
            }
        }

        // Finally, let's look for @Bean methods...
        try {
            return metadata.hasAnnotatedMethods(Bean.class.getName());
        }
        catch (Throwable ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
            }
            return false;
        }
    }

    ....
}

2、配置类解析阶段发生的条件装配

1> 第一次条件装配

在这里插入图片描述
结合上面的流程图 和 debug流程中可以看出,在ConfigurationClassParser#processConfigurationClass(ConfigurationClass, Predicate<String>)方法的最上层通过调用ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata,ConfigurationPhase)方法进行第一次条件装配,由于方法第二参数传的是ConfigurationPhase.PARSE_CONFIGURATION,表明此时在做的条件装配是解析配置类 类型的。

2> 第二次条件装配

在这里插入图片描述
紧接着进入到doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)方法;方法的入参configClass、sourceClass是我们的启动类(被@SpringBootApplication注解标注的类 或者说 是main函数所在的类)。

方法中会依次解析@PropertySource、@ComponentScan、@Import、 @ImportResource、@Bean注解。一个最干净的Spring Boot 应用程序中,不会涉及到@PropertySource、@ImportResource、@Bean三个注解的解析。

在这里插入图片描述

1)解析@ComponentScan注解时

会通过调用ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata,ConfigurationPhase)方法进行第二次条件装配,由于方法第二参数传的是ConfigurationPhase.REGISTER_BEAN,表明此时在做的条件装配是注册Bean 类型的。然后:

  1. 首先@ComponentScan注解在 解析配置类阶段扫描到的所有 被@Component衍生注解 标注的配置类都会放入到ConfigurationClassParser类的configurationClasses映射中,并且放入到BeanFactory的beanDefinitionNames集合中。

    这里的configurationClasses映射(Map<ConfigurationClass, ConfigurationClass>)可以看做是一个缓存,其中放了所有符合配置类解析阶段条件装配的bean对象信息。
    而BeanFactory的beanDefinitionNames集合(List<String>)则用于存放所有符合注册Bean阶段条件装配的bean对象信息。

    • 那这里直接把@ComponentScan扫描到的所有被@Component衍生注解标注的类都直接放到了BeanFactory的beanDefinitionNames集合中是不是有问题啊?它还没做注册Bean阶段的条件装配吧?
    • 并且我们也知道,则配置类解析阶段@ConditionalOnBean、@ConditionalOnMissingBean注解大概率可能不会生效(因为此时很多类还没有注册到BeanFactory的beanDefinitionNames集合中)。我想Spring boot肯定不会不考虑这一层。看后面注册Bean阶段做的条件装配即可解答疑惑。
  2. 接着遍历所有扫描出来的类,通过递归的方式每一个扫描出来的类都去执行ConfigurationClassParser#parse()方法去解析自身的配置类信息。

2)解析@Import注解时

ImportSelector类型的类会被添加到ConfigurationClassParser类的deferredImportSelectors集合中,并且走完逻辑之后,集合中也只会有一个成员:AutoConfigurationImportSelector(在获取所有的自动装配类是需要使用它的process()方法)。对于其他类(ImportSelector类型、ImportBeanDefinitionRegistrar类型除外的)采用递归的方式走processConfigurationClass()方法将自身看做是一个ConfigurationClass做配置类的解析操作。
在这里插入图片描述

3> 第三次条件装配

第三次条件装配仅针对自动装配类,上面提到AutoConfigurationImportSelector#process()方法将所有自动装配类全路径名放入到configurationClasses集合中,其中也牵扯到条件装配。那么多的自动装配类,我并不是每一个都需要用到,对于不需要用到的就要过滤掉。
在这里插入图片描述
针对这里我们在博文:《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理(万字图文源码分析)详细聊过。

其中有一点需要格外注意,在过滤自动装配类时,就OnBeanCondition过滤器而言(即@ConditionalOnBean、@ConditionalOnMissingBean),功能和OnClassConditionCondition类似, 装配条件为Class是否存在;因为此时ConfigClass几乎都还没有注册到BeanFactory的临时容器beanDefinitionNames中,而正常情况下Bean Conditions条件注解的使用需要开发人员特别小心BeanDefinition的添加顺序,所以
SpringBoot官网的JavaDoc强烈建议开发人员仅在自动装配中使用Bean Conditions条件注解。
在这里插入图片描述
Bean Conditions条件注解的使用参考博文:《SpringBoot系列十一》:精讲如何使用@Conditional系列注解做条件装配

3、配置类注册为BeanDefinition阶段发生的条件装配(第四次)

在这里插入图片描述
在解析配置类阶段,所有符合条件装配的配置类都会放到ConfigurationClassParser对象的configurationClasses映射(Map<ConfigurationClass, ConfigurationClass>)中,在配置类注册为BeanDefinition阶段要做的就是把映射里所有的ConfigurationClass转为BeanDefinition注册到BeanFactory的String集合类型的beanDefinitionNames成员中,供后续注册到Spring IOC容器中使用。

具体代码执行流程如下:
在这里插入图片描述

除了对获取到的自动装配类做第一遍条件装配时,其余条件装配的执行入口均为:ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata,ConfigurationPhase)

下面沿着这个入口,讨论一下条件装配是怎么执行的?

四、条件装配怎么执行?

ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata,ConfigurationPhase)方法的返回值为boolean类型,方法返回true表示当前类应该被过滤掉(即不符合条件装配的规则)、否则表示当前类应该被留下(即符合条件装配的规则)。

/**
 * Determine if an item should be skipped based on {@code @Conditional} annotations.
 * @param metadata the meta data
 * @param phase the phase of the call
 * @return if the item should be skipped
 */
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 如果类没被@Conditional衍生注解标注,则直接返回FALSE,表示当前类不应该被过滤掉
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }

    // 如果没设置条件装配的阶段,当类是配置类时,为 PARSE_CONFIGURATION 阶段,否则默认为 REGISTER_BEAN 阶段
    if (phase == null) {
        if (metadata instanceof AnnotationMetadata &&
                ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
            return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
        }
        return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    }

    // 获取类上所有的@Conditional 子注解,返回@Conditional注解中的value值
    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);

    // 遍历所有的Condition,进行match
    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            // 获取当前Condition的执行阶段
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 如果入参传入的阶段和Condition的阶段不同,直接返回FALSE。
        // 如果阶段相同 或 Condition的阶段为null,再使用Condition#matches(this.context, metadata)做真正的条件装配逻辑,不符合则返回TRUE。
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }

    return false;
}

shouldSkip()方法执行的逻辑概括如下:

  1. 首先,如果入参类 为 null 或者 没有被@Conditional衍生注解标注,则直接返回FALSE,表示当前类不应该被过滤掉;
  2. 如果入参没有传条件装配的阶段;当类是配置类时,为 PARSE_CONFIGURATION 阶段,否则默认为 REGISTER_BEAN 阶段;
  3. 接着,获取类上所有的@Conditional 子注解,返回@Conditional注解中的value值。并对获取到的所有Condition接口的实现类进行排序。
  4. 遍历所有的Condition,进行匹配;如果入参传入的阶段和Condition的阶段不同,直接返回FALSE。如果阶段相同 或 Condition的阶段为null,再使用Condition#matches(this.context, metadata)做真正的条件装配逻辑,不符合则返回TRUE。

下面我们接着一次讨论条件装配的阶段、Condition#matches()方法的具体匹配逻辑;

1、条件装配的阶段

public interface ConfigurationCondition extends Condition {

    /**
     * 返回当前Condition应该被评估的阶段
     */
    ConfigurationPhase getConfigurationPhase();


    /**
     * 可以评估装配条件的几个阶段.
     */
    enum ConfigurationPhase {

        /**
         * 类解析阶段
         */
        PARSE_CONFIGURATION,

        /**
         * 类注册阶段
         */
        REGISTER_BEAN
    }

}

PARSE_CONFIGURATION表示解析配置类的时候执行、REGISTER_BEAN表示注册Bean的时候执行;这里和上面讨论的条件装配的入口(Spring Boot 配置类加载的两阶段)相呼应。

getConfigurationPhase()表示每个Condition执行器,都可以指定一个阶段去执行,并且只有在此阶段才会评估装配条件。
在这里插入图片描述
就OnClassCondition、OnBeanCondition、OnWebApplicationCondition三个常用的Condition执行器来看,仅有OnBeanCondition指定了评估装配条件的阶段(具体阶段为REGISTER_BEAN)。即:OnBeanCondition仅在注册Bean阶段才会评估装配条件,而OnClassCondition 和 OnWebApplicationCondition在任意阶段都会评估装配条件。

2、Condition#matches()匹配逻辑

这里讨论OnClassCondition 和 OnBeanCondition两种Condition执行器。

1)OnClassCondition

代码执行流程如下:
在这里插入图片描述
最后进入到FilteringSpringBootCondition#matches()方法(根据上层逻辑,此处的matches()方法可能命名为notMatches更易理解):
在这里插入图片描述
代码流程解析:

  1. 根据当前ClassLoader使用反射Class.forNam()加载类:

    • 如果MISSING枚举能加载到Class,matches()方法则向上返回false,表示当前类符合条件装配;
    • 如果PRESENT枚举加载Class时报错(即加载不到Class),matches()方法则向上返回false,表示当前类符合条件装配。
  2. 如果Class Conditions中存在多个Class,则for循环判断,不符合条件装配的类先添加到一个List<String>集合missing/present,上层判断如果集合不为空,则返回一个ConditionOutcome对象,内容为:@ConditionalOnClass did not find required classes 'xxxxx'@ConditionalOnMissingClass found unwanted classes 'xxxxx'。并给到Logger打印。
  3. 否则表示符合条件装配。

2)OnBeanCondition

在这里插入图片描述
WebMvcAutoConfiguration的WebMvcAutoConfigurationAdapter静态内部类的@Bean方法viewResolver()为例,其上标注了@ConditionalOnMissingBean注解,需要满足条件:类型为ViewResolver的Bean存在,且beanName名称为”viewResolver“、类型为org.springframework.web.servlet.view.ContentNegotiatingViewResolver的bean不存在时,才会将ContentNegotiatingViewResolver注册到Spring容器中。

1> 按bean type匹配

整体代码执行流程如下:
在这里插入图片描述
上述代码流程为 根据条件装配注解中设置的Xxx.class(即类的类型),最终进入到DefaultListableBeanFactory#doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit)方法;
在这里插入图片描述
从beanDefinitionNames和manualSingletonNames中查找到相应类型的所有bean。

  • beanDefinitionNames中存储通过配置类解析阶段和Bean注册阶段的一些Bean;比如:启动类、自动装配类、@Bean方法注入的Bean、Controller、Service等等。
  • manualSingletonNames,从名字(手工单例名称)来看:在 spring Bean注册的过程中,会手动触发一些bean的注册。比如在springboot启动过程中,会显示的注册一些配置 bean:springBootBanner,springApplicationArguments,systemEnvironment,systemProperties等等。

在这里插入图片描述
org.springframework.web.servlet.ViewResolver类型的类而言,找到三个bean,如下:
在这里插入图片描述
往上返回,回到OnBeanCondition#getMatchingBeans()方法,将获取到的三个bean添加到MatchResult的matchedTypes属性(private final Map<String, Collection<String>> matchedTypes = new HashMap<>();)中。
在这里插入图片描述
后续会继续判断bean的名称,由于@ConditionalOnBean(ViewResolver.class)中只有type,没有name,所以跳过。

再往上返回,@ConditionalOnBean条件装配通过;
在这里插入图片描述

2> 按bean name匹配

再看@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)其中会判断:名称为viewResolver、类型为ContentNegotiatingViewResolver的Bean不存在;

代码执行逻辑如下:
在这里插入图片描述
这里最后会判断Spring的最终容器singletonObjects和临时容器beanDefinitionMap是否包含name为viewResolver的Bean,最终返回FALSE,表示不存在。
在这里插入图片描述

临时容器beanDefinitionMap是什么时候赋值?临时容器不是beanDefinitionNames吗?
  • beanDefinitionNames中存放的只是类的全路径名,而beanDefinitionMap中存放的是类的全路径名和BeanDefinition信息的键值对。
  • registerBeanDefinition(String beanName, BeanDefinition beanDefinition)方法中会同时对beanDefinitionNames 和 beanDefinitionMap赋值;所以它俩的元素个数是一样的。

在这里插入图片描述

最终WebMvcAutoConfiguration的WebMvcAutoConfigurationAdapter静态内部类的@Bean方法viewResolver()符合条件装配的条件。可以注册到Spring的IOC容器中。
在这里插入图片描述

五、总结

  1. @Conditional注解可以标注在ConfigurationClass配置类、@Bean方法上,相当于加了个条件判断,通过判断的结果最终决定是否将Bean注册到spring容器中。
  2. Spring在处理配置类时有两个阶段:解析配置类、注册bean;这两个阶段中都会使用@Conditional注解来做条件装配;
  3. 另外,@Conditional 的这套机制很大程度上是用于 自动配置 上,尤其是针对Bean Conditions类的注解,否则需要考虑Bean 的加载顺序,不然容易出现@ConditionalOnBean 或 @ConditionalOnMissingBean注解失效的问题。

下一篇文章,我们继续讨论:条件装配时各个Condition执行的顺序问题

相关文章
|
2月前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
37 0
|
10天前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
17天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
67 14
|
2月前
|
Java Spring
SpringBoot自动装配的原理
在Spring Boot项目中,启动引导类通常使用`@SpringBootApplication`注解。该注解集成了`@SpringBootConfiguration`、`@ComponentScan`和`@EnableAutoConfiguration`三个注解,分别用于标记配置类、开启组件扫描和启用自动配置。
62 17
|
2月前
|
消息中间件 Java 数据库
解密Spring Boot:深入理解条件装配与条件注解
Spring Boot中的条件装配与条件注解提供了强大的工具,使得应用程序可以根据不同的条件动态装配Bean,从而实现灵活的配置和管理。通过合理使用这些条件注解,开发者可以根据实际需求动态调整应用的行为,提升代码的可维护性和可扩展性。希望本文能够帮助你深入理解Spring Boot中的条件装配与条件注解,在实际开发中更好地应用这些功能。
40 2
|
2月前
|
Java 容器
springboot自动配置原理
启动类@SpringbootApplication注解下,有三个关键注解 (1)@springbootConfiguration:表示启动类是一个自动配置类 (2)@CompontScan:扫描启动类所在包外的组件到容器中 (3)@EnableConfigutarion:最关键的一个注解,他拥有两个子注解,其中@AutoConfigurationpackageu会将启动类所在包下的所有组件到容器中,@Import会导入一个自动配置文件选择器,他会去加载META_INF目录下的spring.factories文件,这个文件中存放很大自动配置类的全类名,这些类会根据元注解的装配条件生效,生效
|
6月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
130 0
|
3月前
|
Java Spring 容器
springboot @RequiredArgsConstructor @Lazy解决循环依赖的原理
【10月更文挑战第15天】在Spring Boot应用中,循环依赖是一个常见问题,当两个或多个Bean相互依赖时,会导致Spring容器陷入死循环。本文通过比较@RequiredArgsConstructor和@Lazy注解,探讨它们解决循环依赖的原理和优缺点。@RequiredArgsConstructor通过构造函数注入依赖,使代码更简洁;@Lazy则通过延迟Bean的初始化,打破创建顺序依赖。两者各有优势,需根据具体场景选择合适的方法。
130 4
|
4月前
|
Java 应用服务中间件 API
Vertx高并发理论原理以及对比SpringBoot
Vertx 是一个基于 Netty 的响应式工具包,不同于传统框架如 Spring,它的侵入性较小,甚至可在 Spring Boot 中使用。响应式编程(Reactive Programming)基于事件模式,通过事件流触发任务执行,其核心在于事件流 Stream。相比多线程异步,响应式编程能以更少线程完成更多任务,减少内存消耗与上下文切换开销,提高 CPU 利用率。Vertx 适用于高并发系统,如 IM 系统、高性能中间件及需要较少服务器支持大规模 WEB 应用的场景。随着 JDK 21 引入协程,未来 Tomcat 也将优化支持更高并发,降低响应式框架的必要性。
Vertx高并发理论原理以及对比SpringBoot
|
3月前
|
设计模式 Java Spring
Spring Boot监听器的底层实现原理
Spring Boot监听器的底层实现原理主要基于观察者模式(也称为发布-订阅模式),这是设计模式中用于实现对象之间一对多依赖的一种常见方式。在Spring Boot中,监听器的实现依赖于Spring框架提供的事件监听机制。
40 1