本文转载于公众号“吉姆餐厅ak”

条件注解是Spring4提供的一种bean加载特性,主要用于控制配置类和bean初始化条件。在springBoot,springCloud一系列框架底层源码中,条件注解的使用到处可见。

不少人在使用 @ConditionalOnBean 注解时会遇到不生效的情况,依赖的 bean 明明已经配置了,但就是不生效。是不是@ConditionalOnBean和 Bean加载的顺序有没有关系呢?

本篇文章就针对这个问题,跟着源码,一探究竟。


问题演示:

@Configuration
public class Configuration1 {@Bean@ConditionalOnBean(Bean2.class)public Bean1 bean1() {return new Bean1();}
}
@Configuration
public class Configuration2 {@Beanpublic Bean2 bean2(){return new Bean2();}
}

运行结果:
@ConditionalOnBean(Bean2.class)返回false。明明定义的有bean2,bean1却未加载。


源码分析

首先要明确一点,条件注解的解析一定发生在spring ioc的bean definition阶段,因为 spring bean初始化的前提条件就是有对应的bean definition,条件注解正是通过判断bean definition来控制bean能否被解析。

对上述示例进行源码调试。

从 bean definition解析的入口开始:ConfigurationClassPostProcessor

    @Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {int registryId = System.identityHashCode(registry);if (this.registriesPostProcessed.contains(registryId)) {throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);}if (this.factoriesPostProcessed.contains(registryId)) {throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry);}this.registriesPostProcessed.add(registryId);// 解析bean definition入口processConfigBeanDefinitions(registry);}

跟进processConfigBeanDefinitions方法:

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {//省略不必要的代码...//解析候选bean,先获取所有的配置类,也就是@Configuration标注的类parser.parse(candidates);parser.validate();//配置类存入集合Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());configClasses.removeAll(alreadyParsed);// Read the model and create bean definitions based on its contentif (this.reader == null) {this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment,this.importBeanNameGenerator, parser.getImportRegistry());}//开始解析配置类,也就是条件注解解析的入口this.reader.loadBeanDefinitions(configClasses);alreadyParsed.addAll(configClasses);//...}

跟进条件注解解析入口loadBeanDefinitions,开始循环解析所有的配置类。这里是所有自定义的配置类和自动装配的配置类,如下:

上述代码开始解析配置类。如果配置类中有@Bean标注的方法,则会调用loadBeanDefinitionsForBeanMethod()来获得所有方法。然后循环解析,解析时会执行如下校验方法,也正是条件注解的入口:

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);}//获取当前定义bean的方法上,所有的条件注解List<Condition> conditions = new ArrayList<>();for (String[] conditionClasses : getConditionClasses(metadata)) {for (String conditionClass : conditionClasses) {Condition condition = getCondition(conditionClass, this.context.getClassLoader());conditions.add(condition);}}//根据Order来进行排序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;}}return false;}

继续跟进条件注解的匹配方法,开始解析示例代码中bean1的配置:

   @Bean@ConditionalOnBean(Bean2.class)public Bean1 bean1() {return new Bean1();}

在getMatchOutcome方法中,参数metadata是要解析的目标bean,也就是bean1。条件注解依赖的bean被封装成了BeanSearchSpec,从名字可以看出是要寻找的对象,这是一个静态内部类,构造方法如下:

BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata,Class<?> annotationType) {this.annotationType = annotationType;//读取 metadata中的设置的valueMultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(annotationType.getName(), true);//设置各参数,根据这些参数进行寻找目标类collect(attributes, "name", this.names);collect(attributes, "value", this.types);collect(attributes, "type", this.types);collect(attributes, "annotation", this.annotations);collect(attributes, "ignored", this.ignoredTypes);collect(attributes, "ignoredType", this.ignoredTypes);this.strategy = (SearchStrategy) metadata.getAnnotationAttributes(annotationType.getName()).get("search");BeanTypeDeductionException deductionException = null;try {if (this.types.isEmpty() && this.names.isEmpty()) {addDeducedBeanType(context, metadata, this.types);}}catch (BeanTypeDeductionException ex) {deductionException = ex;}validate(deductionException);}

继续跟进搜索bean的方法:

MatchResult matchResult = getMatchingBeans(context, spec);
private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) {ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();if (beans.getStrategy() == SearchStrategy.ANCESTORS) {BeanFactory parent = beanFactory.getParentBeanFactory();Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,"Unable to use SearchStrategy.PARENTS");beanFactory = (ConfigurableListableBeanFactory) parent;}MatchResult matchResult = new MatchResult();boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(beans.getIgnoredTypes(), beanFactory, context, considerHierarchy);//因为实例代码中设置的是类型,所以这里会遍历类型,根据type获取目标bean是否存在for (String type : beans.getTypes()) {Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,context.getClassLoader(), considerHierarchy);typeMatches.removeAll(beansIgnoredByType);if (typeMatches.isEmpty()) {matchResult.recordUnmatchedType(type);}else {matchResult.recordMatchedType(type, typeMatches);}}//根据注解寻找for (String annotation : beans.getAnnotations()) {List<String> annotationMatches = Arrays.asList(getBeanNamesForAnnotation(beanFactory, annotation,context.getClassLoader(), considerHierarchy));annotationMatches.removeAll(beansIgnoredByType);if (annotationMatches.isEmpty()) {matchResult.recordUnmatchedAnnotation(annotation);}else {matchResult.recordMatchedAnnotation(annotation, annotationMatches);}}//根据设置的name进行寻找for (String beanName : beans.getNames()) {if (!beansIgnoredByType.contains(beanName)&& containsBean(beanFactory, beanName, considerHierarchy)) {matchResult.recordMatchedName(beanName);}else {matchResult.recordUnmatchedName(beanName);}}return matchResult;}

getBeanNamesForType()方法最终会委托给BeanTypeRegistry类的getNamesForType方法来获取对应的指定类型的bean name:

    Set<String> getNamesForType(Class<?> type) {//同步spring容器中的beanupdateTypesIfNecessary();//返回指定类型的beanreturn this.beanTypes.entrySet().stream().filter((entry) -> entry.getValue() != null&& type.isAssignableFrom(entry.getValue())).map(Map.Entry::getKey).collect(Collectors.toCollection(LinkedHashSet::new));}

重点来了。
上述方法中的第一步便是同步bean,也就是获取此时 spring 容器中的所有 beanDifinition。只有这样,条件注解的判断才有意义。

我们跟进updateTypesIfNecessary()

    private void updateTypesIfNecessary() {//这里lastBeanDefinitionCount 代表已经同步的数量,如果和容器中的数量不相等,才开始同步。//否则,获取beanFactory迭代器,开始同步。if (this.lastBeanDefinitionCount != this.beanFactory.getBeanDefinitionCount()) {Iterator<String> names = this.beanFactory.getBeanNamesIterator();while (names.hasNext()) {String name = names.next();if (!this.beanTypes.containsKey(name)) {addBeanType(name);}}//同步完之后,更新已同步的beanDefinition数量。this.lastBeanDefinitionCount = this.beanFactory.getBeanDefinitionCount();}}

离答案只差一步了,就是看一下从beanFactory中迭代的是哪些beanDefinition

继续跟进getBeanNamesIterator()

@Overridepublic Iterator<String> getBeanNamesIterator() {CompositeIterator<String> iterator = new CompositeIterator<>();iterator.add(this.beanDefinitionNames.iterator());iterator.add(this.manualSingletonNames.iterator());return iterator;}

分别来看:

  • beanDefinitionNames就是存储一些自动解析和装配的bean,我们的启动类、配置类、controller、service等。

  • manualSingletonNames,从名字可以看出,手工单例名称。什么意思呢?在 spring ioc的过程中,会手动触发一些bean的注册。比如在springboot启动过程中,会显示的注册一些配置 bean,如:
    springBootBanner,systemEnvironment,systemProperties等。

我们来分析一下上面示例bean1为何没有实例化?

spring ioc的过程中,优先解析@Component,@Service,@Controller注解的类。其次解析配置类,也就是@Configuration标注的类。最后开始解析配置类中定义的bean。 

示例代码中bean1是定义在配置类中的,当执行到配置类解析的时候,@Component,@Service,@Controller ,@Configuration标注的类已经全部扫描,所以这些BeanDifinition已经被同步。 但是bean1的条件注解依赖的是bean2bean2是被定义的配置类中的,所以此时配置类的解析无法保证先后顺序,就会出现不生效的情况。

同样的道理,如果依赖的是FeignClient,可以设想一下结果?FeignClient最终还是由配置类触发的,解析的先后顺序同样也不能保证。


解决

以下两种方式:

  • 项目中条件注解依赖的类,大多会交给spring容器管理,所以如果要在配置中Bean通过@ConditionalOnBean依赖配置中的Bean时,完全可以用@ConditionalOnClass(Bean2.class)来代替。

  • 如果一定要区分两个配置类的先后顺序,可以将这两个类交与EnableAutoConfiguration管理和触发。也就是定义在META-INF\spring.factories中声明是配置类,然后通过@AutoConfigureBefore、AutoConfigureAfter  AutoConfigureOrder控制先后顺序。之所以这么做是因为这三个注解只对自动配置类的先后顺序生效。

    这里推荐第一种。


总结

在配置类中定义Bean,如果使用@ConditionalOnBean注解依赖的Bean是通过配置类触发解析的,则执行结果依赖配置类加载顺序。

-更多文章-

Docker入门与实践

分布式之消息队列复习精讲

那些年让你迷惑的阻塞、非阻塞、异步、同步

拨开云雾见天日:剖析单机事务原理

Spring Cloud GateWay初体验

Kubernetes基础与架构

对业务系统的监控 No.118

Kubernetes对象模型

拜托!面试请不要再问我Spring Cloud底层原理

【性能优化之道】每秒上万并发下的Spring Cloud参数优化实战

Docker 核心技术与实现原理

学习别跟我谈兴趣 No.88

分布式事务的实现原理

-关注我-

条件注解 @ConditionalOnBean 的正确使用姿势相关推荐

  1. 难以想象SpringBoot中的条件注解底层居然是这样实现的

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源 | https://urlify.cn/bm2qqi Spr ...

  2. 面试:SpringBoot中的条件注解底层是如何实现的?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源 | https://urlify.cn/bm2qqi Spr ...

  3. 面试:Spring Boot 中的条件注解底层是如何实现的?

    SpringBoot内部提供了特有的注解:条件注解(Conditional Annotation).比如@ConditionalOnBean.@ConditionalOnClass.@Conditio ...

  4. @data注解不生效_面试官:你经常在SpringBoot中使用的条件注解底层是如何实现的?你了解过吗?...

    SpringBoot内部提供了特有的注解:条件注解(Conditional Annotation).比如@ConditionalOnBean.@ConditionalOnClass.@Conditio ...

  5. 什么叫取反_转载:CodeReview正确的姿势是什么?

    作者:微博是阿里孤尽 链接:https://www.zhihu.com/question/383079175/answer/1109655276 来源:知乎 著作权归作者所有.商业转载请联系作者获得授 ...

  6. Spring Boot 自动配置之条件注解

    2019独角兽企业重金招聘Python工程师标准>>> Spring Boot 神奇的自动配置,主要依靠大量的条件注解来使用配置自动化. 根据满足某一个特定条件创建一个特定的Bean ...

  7. java同步锁如何使用_java 同步锁(synchronized)的正确使用姿势

    关于线程安全,线程锁我们经常会用到,但你的使用姿势正确不,反正我用错了好长一段时间而不自知.所以有了这篇博客总结下线程锁的正确打开姿势 废话不说看例子 一,对整个方法进行加锁 1,对整个方法进行加锁, ...

  8. java 日志使用_Java日志正确使用姿势

    前言 关于日志,在大家的印象中都是比较简单的,只须引入了相关依赖包,剩下的事情就是在项目中"尽情"的打印我们需要的信息了.但是往往越简单的东西越容易让我们忽视,从而导致一些不该有的 ...

  9. 索引的正确“打开姿势”

    本文分享自华为云社区<DWS 索引的正确"打开姿势">,原文作者:hoholy . 索引能干什么呢,一言以蔽之:查询加速.常见的索引有下面几种: 1. 常用索引介绍 1 ...

最新文章

  1. 视觉与机械手标定系统技术解决方案
  2. BCI比赛数据集简介-BCI competition IV 2b
  3. 华农软件工程实验报告_华南农业大学15年软件工程复习提纲
  4. 如何在基于Bytom开发过程中集成IPFS
  5. mongodb 持久化 mysql_scrapy数据持久化存储(MySQL、MongoDB)
  6. Python练习:tkinter(1)
  7. .NET错误:未找到类型或命名空间名称
  8. 【大会】技术决策背后的商业逻辑
  9. 莫烦Pytorch神经网络第三章代码修改
  10. minhash算法检索相似文本_基于向量的深层语义相似文本召回?你需要bert和faiss...
  11. 不懂Python装饰器?教程双手奉上!
  12. 数字的补数——力扣476
  13. 三星Galaxy Note20新旗舰发布会官宣:8月5日线上见
  14. 第二十七篇 导航栏和内容块
  15. mysql定制化_【MySQL技巧】定制你的MySQL命令行
  16. html制作幸运抽奖,基于canvas的jQuery幸运抽奖大轮盘插件
  17. Qt实现音视频播放器
  18. 六爻预测,前沿科学?伪科学?
  19. MIT线性代数笔记二十八讲 相似矩阵和若尔当标准型
  20. const T 与T const(const T vs.T const的翻译 Dan Saks)

热门文章

  1. 视觉稿与H5页面之间的终端适配
  2. Mui.ajax请求服务器正确返回json数据格式
  3. UITableView
  4. 关于IOS的蓝牙(转)
  5. OA项目12:系统管理之用户管理
  6. Umbra 3:次世代的遮挡裁剪
  7. baidu mp3竟然还加密,太扯了
  8. 让html:error只显示第一条错误信息
  9. [导入]源代码版本控制(一)
  10. 利用BP神经网络教计算机进行非线函数拟合(代码部分单层)