文章目录
在前一篇博文:《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理(两万字图文源码分析)(含@ConditionalOnClass原理),聊了Spring自动装配时做的条件装配,其中@ConditionalOnBean实现的条件装配:居然不是根据Bean是否存在于Spring容器中来判断,而是和@ConditionalOnClass一样依靠类是否能被加载来判断。
本文着重讨论@Conditional各类衍生注解实现条件装配的原理。
注:Spring Boot版本:2.3.7.RELEASE(博主写博客时最新Spring-boot版本 – 2.6.X代码逻辑几乎一样)
@Conditional注解是从spring4.0版本才有的,其是一个条件装配注解,可以用在任何类型或者方法上面,以指定的条件形式限制bean的创建;即当所有条件都满足的时候,被@Conditional标注的目标才会被spring容器处理。
- @Conditional本身也是一个父注解,从SpringBoot1.0版本开始派生出了大量的子注解;用于Bean的按需加载。
- @Conditional注解和其所有子注解必须依托于被
@Component衍生注解标注的类,即Spring要能扫描到@Conditional衍生注解所在的类,才能做进一步判断。- @Conditional衍生注解可以加在类 或 类的方法上;加在类上表示类的所有方法都做条件装配、加在方法上则表示只有当前方法做条件装配。
使用方式参考博文:《SpringBoot系列十一》:精讲如何使用@Conditional系列注解做条件装配。
自定义条件装配参考博文:《SpringBoot系列十二》:如何自定义条件装配。
Spring Boot对ConfigurationClass配置类的处理分为2个阶段:配置类解析阶段、配置类注册为BeanDefinition阶段。

当一个类符合下列条件时:
- 类上有@Component注解(或者说间接被@Component标注);
- 类上有@CompontentScan注解;
- 类上有@Import注解;
- 类上有@ImportResource注解;
- 类中有@Bean标注的方法。
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;
}
}
....
}

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

紧接着进入到doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)方法;方法的入参configClass、sourceClass是我们的启动类(被@SpringBootApplication注解标注的类 或者说 是main函数所在的类)。
方法中会依次解析@PropertySource、@ComponentScan、@Import、 @ImportResource、@Bean注解。一个最干净的Spring Boot 应用程序中,不会涉及到@PropertySource、@ImportResource、@Bean三个注解的解析。

会通过调用ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata,ConfigurationPhase)方法进行第二次条件装配,由于方法第二参数传的是ConfigurationPhase.REGISTER_BEAN,表明此时在做的条件装配是注册Bean 类型的。然后:
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阶段做的条件装配即可解答疑惑。
ConfigurationClassParser#parse()方法去解析自身的配置类信息。ImportSelector类型的类会被添加到ConfigurationClassParser类的deferredImportSelectors集合中,并且走完逻辑之后,集合中也只会有一个成员:AutoConfigurationImportSelector(在获取所有的自动装配类是需要使用它的process()方法)。对于其他类(ImportSelector类型、ImportBeanDefinitionRegistrar类型除外的)采用递归的方式走processConfigurationClass()方法将自身看做是一个ConfigurationClass做配置类的解析操作。

第三次条件装配仅针对自动装配类,上面提到AutoConfigurationImportSelector#process()方法将所有自动装配类全路径名放入到configurationClasses集合中,其中也牵扯到条件装配。那么多的自动装配类,我并不是每一个都需要用到,对于不需要用到的就要过滤掉。

针对这里我们在博文:《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理(万字图文源码分析)详细聊过。
其中有一点需要格外注意,在过滤自动装配类时,就OnBeanCondition过滤器而言(即@ConditionalOnBean、@ConditionalOnMissingBean),功能和OnClassConditionCondition类似, 装配条件为Class是否存在;因为此时ConfigClass几乎都还没有注册到BeanFactory的临时容器beanDefinitionNames中,而正常情况下Bean Conditions条件注解的使用需要开发人员特别小心BeanDefinition的添加顺序,所以
SpringBoot官网的JavaDoc强烈建议开发人员仅在自动装配中使用Bean Conditions条件注解。

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

在解析配置类阶段,所有符合条件装配的配置类都会放到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()方法执行的逻辑概括如下:
- 首先,如果入参类 为 null 或者 没有被@Conditional衍生注解标注,则直接返回FALSE,表示当前类不应该被过滤掉;
- 如果入参没有传条件装配的阶段;当类是配置类时,为 PARSE_CONFIGURATION 阶段,否则默认为 REGISTER_BEAN 阶段;
- 接着,获取类上所有的@Conditional 子注解,返回@Conditional注解中的value值。并对获取到的所有Condition接口的实现类进行排序。
- 遍历所有的Condition,进行匹配;如果入参传入的阶段和Condition的阶段不同,直接返回FALSE。如果阶段相同 或 Condition的阶段为null,再使用Condition#matches(this.context, metadata)做真正的条件装配逻辑,不符合则返回TRUE。
下面我们接着一次讨论条件装配的阶段、Condition#matches()方法的具体匹配逻辑;
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在任意阶段都会评估装配条件。
这里讨论OnClassCondition 和 OnBeanCondition两种Condition执行器。
代码执行流程如下:

最后进入到FilteringSpringBootCondition#matches()方法(根据上层逻辑,此处的matches()方法可能命名为notMatches更易理解):

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

以WebMvcAutoConfiguration的WebMvcAutoConfigurationAdapter静态内部类的@Bean方法viewResolver()为例,其上标注了@ConditionalOnMissingBean注解,需要满足条件:类型为ViewResolver的Bean存在,且beanName名称为”viewResolver“、类型为org.springframework.web.servlet.view.ContentNegotiatingViewResolver的bean不存在时,才会将ContentNegotiatingViewResolver注册到Spring容器中。
整体代码执行流程如下:

上述代码流程为 根据条件装配注解中设置的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条件装配通过;

再看@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容器中。

- @Conditional注解可以标注在ConfigurationClass配置类、@Bean方法上,相当于加了个条件判断,通过判断的结果最终决定是否将Bean注册到spring容器中。
- Spring在处理配置类时有两个阶段:解析配置类、注册bean;这两个阶段中都会使用@Conditional注解来做条件装配;
- 另外,@Conditional 的这套机制很大程度上是用于 自动配置 上,尤其是针对Bean Conditions类的注解,否则需要考虑Bean 的加载顺序,不然容易出现@ConditionalOnBean 或 @ConditionalOnMissingBean注解失效的问题。
下一篇文章,我们继续讨论:条件装配时各个Condition执行的顺序问题
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin
我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于