条件注解 @ConditionalOnBean 的正确使用姿势

本文转载于公众号“吉姆餐厅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 {

    @Bean
    public 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

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

上述代码开始解析配置类。如果配置类中有@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

继续跟进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是被定义的配置类中的,所以此时配置类的解析无法保证先后顺序,就会出现不生效的情况。

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


解决

以下两种方式:

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

     

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

     

    这里推荐第一种。


总结

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

 

 

### 更多阅读

[史上最简单的 SpringCloud 教程汇总](https://blog.csdn.net/forezp/article/details/70148833)

[SpringBoot教程汇总](https://blog.csdn.net/forezp/article/details/70341818)

[Java面试题系列汇总](https://blog.csdn.net/forezp/article/details/85163411)

 

 

                                          
                                                扫一扫,支持博主吧

                                

 

  • 37
    点赞
  • 92
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: @ConditionalOnBean 是 Spring 框架中的一个条件注解,用于指定在特定的 Bean 存在时才会生效。 通常情况下,我们使用 @ConditionalOnBean 注解来限制某个 Bean 的加载条件。当被注解的类或方法放置在 Bean 的定义上时,Spring 容器会根据指定的条件判断是否加载该 Bean。只有当条件满足时,才会将该 Bean 加入到 Spring 容器中。 @ConditionalOnBean 注解可以用于任何 Spring Bean 上,如 @Service、@Component、@Configuration 等等。它的参数是一个或多个 Class 类型的数组,指定了需要判断是否存在的 Bean 的类型。只有当指定的所有 Bean 都存在于 Spring 容器中,条件才会满足。 使用 @ConditionalOnBean 注解时,可以将多个条件连接起来以构成复杂的条件判断。通过在多个条件注解之间添加逻辑运算符,如 @ConditionalOnBean({BeanA.class, BeanB.class}) 表示只有当 BeanA 和 BeanB 同时存在时,条件才会满足。 这个注解的主要作用是根据特定的 Bean 的存在与否来决定特定的功能是否可用。比如,当我们的应用依赖于某个外部库的存在时,可以使用 @ConditionalOnBean 注解来限制某个组件的加载,在外部库存在时才加载该组件。 总之,@ConditionalOnBean 是 Spring 框架中用于条件化加载 Bean 的注解之一,可以根据特定 Bean 的存在来决定是否加载某个 Bean,从而实现更加灵活和可配置的应用开发。 ### 回答2: @ConditionalOnBean是Spring框架中的一个注解,用于指定一个组件的条件,只有当指定的Bean存在于Spring容器中时,才会创建被注解的组件。 使用@ConditionalOnBean注解可以在某些特定的条件下,动态地决定是否创建一个特定的组件。它的作用是根据指定的Bean是否存在来决定某个组件是否应该被创建。 @ConditionalOnBean的使用方法是将它放在一个类上面,这个类可以是任意可以被Spring容器扫描到的类,比如配置类、bean类等。注解中需要指定的参数是一个或多个被依赖的Bean的类名或类的全限定名。 当被注解的类被加载到Spring容器中时,会先检查指定的Bean是否存在于容器中。如果存在,则创建被注解的组件;如果不存在,则不创建该组件。 通过@ConditionalOnBean注解,我们可以根据应用条件来动态地控制组件的创建。比如,我们可以根据不同的数据源配置,优先选择某个特定的数据源作为默认数据源。 总结起来,@ConditionalOnBean是一个根据指定的Bean的存在与否来决定是否创建某个组件的注解。它提供了一种简单而灵活的方式来根据特定条件动态地选择组件的创建。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值