SpringBoot2 | 条件注解 @ConditionalOnBean 原理源码分析(七)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
简介: SpringBoot2 | 条件注解 @ConditionalOnBean 原理源码分析(七)


条件注解是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 {
    @Bean
    public Bean2 bean2(){
        return new Bean2();
    }
}
复制代码

结果: @ConditionalOnBean(Bean2.class)返回false。 命名定义了bean2bean1却未加载。

源码分析

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

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

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

@Override
    public 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 content
            if (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,开始循环解析配置类。这里是所有自定义的配置类和自动装配的配置类,如下:

在解析方法loadBeanDefinitionsForConfigurationClass()中,会获得配置类中定义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中的设置的value
      MultiValueMap<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容器中的bean
    updateTypesIfNecessary();
    //返回指定类型的bean
    return 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

跟进beanFactory.getBeanNamesIterator();方法:

@Override
  public 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是被定义的配置类中的,因为两个Bean都是配置类中Bean,所以此时配置类的解析无法保证先后顺序,就会出现不生效的情况。

同样的道理,如果依赖的是FeignClient,也有可能会出现不生效的情况。因为FeignClient最终还是由配置类触发,解析的先后顺序也不能保证。

解决

有两种方式:

  • 项目中条件注解依赖的类,大多会交给spring容器管理,所以如果要在配置中Bean通过@ConditionalOnBean依赖配置中的Bean时,完全可以用@ConditionalOnClass(Bean2.class)来代替
  • 如果一定要区分两个配置类的先后顺序,可以将这两个类交与EnableAutoConfiguration管理和触发。也就是定义在META-INF\spring.factories中声明是配置类,然后通过@AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder控制先后顺序。因为这三个注解只对自动配置类生效

总结

在配置类中定义Bean,如果使用@ConditionalOnBean依赖的也是配置类中Bean,则执行结果不可控,和配置类加载顺序有关。



目录
相关文章
|
6天前
|
Java 开发者 Spring
【SpringBoot 异步魔法】@Async 注解:揭秘 SpringBoot 中异步方法的终极奥秘!
【8月更文挑战第25天】异步编程对于提升软件应用的性能至关重要,尤其是在高并发环境下。Spring Boot 通过 `@Async` 注解简化了异步方法的实现。本文详细介绍了 `@Async` 的基本用法及配置步骤,并提供了示例代码展示如何在 Spring Boot 项目中创建与管理异步任务,包括自定义线程池、使用 `CompletableFuture` 处理结果及异常情况,帮助开发者更好地理解和运用这一关键特性。
51 1
|
15天前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
|
3天前
|
缓存 Java 数据库连接
Spring Boot奇迹时刻:@PostConstruct注解如何成为应用初始化的关键先生?
【8月更文挑战第29天】作为一名Java开发工程师,我一直对Spring Boot的便捷性和灵活性着迷。本文将深入探讨@PostConstruct注解在Spring Boot中的应用场景,展示其在资源加载、数据初始化及第三方库初始化等方面的作用。
13 0
|
15天前
|
Java 数据安全/隐私保护 Spring
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
|
15天前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
|
15天前
|
XML 数据库 数据格式
Spring5入门到实战------14、完全注解开发形式 ----JdbcTemplate操作数据库(增删改查、批量增删改)。具体代码+讲解 【终结篇】
这篇文章是Spring5框架的实战教程的终结篇,介绍了如何使用注解而非XML配置文件来实现JdbcTemplate的数据库操作,包括增删改查和批量操作,通过创建配置类来注入数据库连接池和JdbcTemplate对象,并展示了完全注解开发形式的项目结构和代码实现。
Spring5入门到实战------14、完全注解开发形式 ----JdbcTemplate操作数据库(增删改查、批量增删改)。具体代码+讲解 【终结篇】
|
15天前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式
|
16天前
|
XML JSON Java
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
本文介绍了如何使用IntelliJ IDEA和Maven搭建一个整合了Struts2、Spring4、Hibernate4的J2EE项目,并配置了项目目录结构、web.xml、welcome.jsp以及多个JSP页面,用于刷新和学习传统的SSH框架。
27 0
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
|
2天前
|
监控 安全 Java
【开发者必备】Spring Boot中自定义注解与处理器的神奇魔力:一键解锁代码新高度!
【8月更文挑战第29天】本文介绍如何在Spring Boot中利用自定义注解与处理器增强应用功能。通过定义如`@CustomProcessor`注解并结合`BeanPostProcessor`实现特定逻辑处理,如业务逻辑封装、配置管理及元数据分析等,从而提升代码整洁度与可维护性。文章详细展示了从注解定义、处理器编写到实际应用的具体步骤,并提供了实战案例,帮助开发者更好地理解和运用这一强大特性,以实现代码的高效组织与优化。
|
15天前
|
设计模式 Java 测试技术
公司为何禁止在SpringBoot中使用@Autowired注解?
【8月更文挑战第15天】在Spring Boot的广泛应用中,@Autowired注解作为依赖注入的核心机制之一,极大地简化了Bean之间的装配过程。然而,在某些企业环境下,我们可能会遇到公司政策明确禁止或限制使用@Autowired注解的情况。这一决策背后,往往蕴含着对代码质量、可维护性、测试便利性以及团队开发效率等多方面的考量。以下将从几个方面深入探讨这一决定的合理性及替代方案。
24 0
下一篇
云函数