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

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

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

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


问题演示:

 
  1. @Configuration

  2. public class Configuration1 {

  3.  
  4.     @Bean

  5.     @ConditionalOnBean(Bean2.class)

  6.     public Bean1 bean1() {

  7.         return new Bean1();

  8.     }

  9. }

  10.  
 
  1. @Configuration

  2. public class Configuration2 {

  3.  
  4.     @Bean

  5.     public Bean2 bean2(){

  6.         return new Bean2();

  7.     }

  8. }

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


源码分析

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

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

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

 
  1.     @Override

  2.     public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

  3.         int registryId = System.identityHashCode(registry);

  4.         if (this.registriesPostProcessed.contains(registryId)) {

  5.             throw new IllegalStateException(

  6.                     "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);

  7.         }

  8.         if (this.factoriesPostProcessed.contains(registryId)) {

  9.             throw new IllegalStateException(

  10.                     "postProcessBeanFactory already called on this post-processor against " + registry);

  11.         }

  12.         this.registriesPostProcessed.add(registryId);

  13.  
  14.         // 解析bean definition入口

  15.         processConfigBeanDefinitions(registry);

  16.     }

跟进processConfigBeanDefinitions方法:

 
  1. public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {

  2.  
  3.             //省略不必要的代码...

  4.             //解析候选bean,先获取所有的配置类,也就是@Configuration标注的类

  5.             parser.parse(candidates);

  6.             parser.validate();

  7.  
  8.             //配置类存入集合

  9.             Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());

  10.             configClasses.removeAll(alreadyParsed);

  11.  
  12.             // Read the model and create bean definitions based on its content

  13.             if (this.reader == null) {

  14.                 this.reader = new ConfigurationClassBeanDefinitionReader(

  15.                         registry, this.sourceExtractor, this.resourceLoader, this.environment,

  16.                         this.importBeanNameGenerator, parser.getImportRegistry());

  17.             }

  18.             //开始解析配置类,也就是条件注解解析的入口

  19.             this.reader.loadBeanDefinitions(configClasses);

  20.             alreadyParsed.addAll(configClasses);

  21.             //...

  22.  
  23. }

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

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

 
  1. public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {

  2.  
  3.  
  4.         //判断是否有条件注解,否则直接返回

  5.         if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {

  6.             return false;

  7.         }

  8.  
  9.         if (phase == null) {

  10.             if (metadata instanceof AnnotationMetadata &&

  11.                     ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {

  12.                 return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);

  13.             }

  14.             return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);

  15.         }

  16.  
  17.         //获取当前定义bean的方法上,所有的条件注解

  18.         List<Condition> conditions = new ArrayList<>();

  19.         for (String[] conditionClasses : getConditionClasses(metadata)) {

  20.             for (String conditionClass : conditionClasses) {

  21.                 Condition condition = getCondition(conditionClass, this.context.getClassLoader());

  22.                 conditions.add(condition);

  23.             }

  24.         }

  25.  
  26.         //根据Order来进行排序

  27.         AnnotationAwareOrderComparator.sort(conditions);

  28.  
  29.         //遍历条件注解,开始执行条件注解的流程

  30.         for (Condition condition : conditions) {

  31.             ConfigurationPhase requiredPhase = null;

  32.             if (condition instanceof ConfigurationCondition) {

  33.                 requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();

  34.             }

  35.             //这里执行条件注解的 condition.matches 方法来进行匹配,返回布尔值

  36.             if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {

  37.                 return true;

  38.             }

  39.         }

  40.  
  41.         return false;

  42.     }

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

 
  1.    @Bean

  2.     @ConditionalOnBean(Bean2.class)

  3.     public Bean1 bean1() {

  4.         return new Bean1();

  5.     }

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

 
  1. BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata,

  2.                 Class<?> annotationType) {

  3.             this.annotationType = annotationType;

  4.             //读取 metadata中的设置的value

  5.             MultiValueMap<String, Object> attributes = metadata

  6.                     .getAllAnnotationAttributes(annotationType.getName(), true);

  7.             //设置各参数,根据这些参数进行寻找目标类

  8.             collect(attributes, "name", this.names);

  9.             collect(attributes, "value", this.types);

  10.             collect(attributes, "type", this.types);

  11.             collect(attributes, "annotation", this.annotations);

  12.             collect(attributes, "ignored", this.ignoredTypes);

  13.             collect(attributes, "ignoredType", this.ignoredTypes);

  14.             this.strategy = (SearchStrategy) metadata

  15.                     .getAnnotationAttributes(annotationType.getName()).get("search");

  16.             BeanTypeDeductionException deductionException = null;

  17.             try {

  18.                 if (this.types.isEmpty() && this.names.isEmpty()) {

  19.                     addDeducedBeanType(context, metadata, this.types);

  20.                 }

  21.             }

  22.             catch (BeanTypeDeductionException ex) {

  23.                 deductionException = ex;

  24.             }

  25.             validate(deductionException);

  26.         }

继续跟进搜索bean的方法:

MatchResult matchResult = getMatchingBeans(context, spec);
 
  1. private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) {

  2.         ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();

  3.         if (beans.getStrategy() == SearchStrategy.ANCESTORS) {

  4.             BeanFactory parent = beanFactory.getParentBeanFactory();

  5.             Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,

  6.                     "Unable to use SearchStrategy.PARENTS");

  7.             beanFactory = (ConfigurableListableBeanFactory) parent;

  8.         }

  9.         MatchResult matchResult = new MatchResult();

  10.         boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;

  11.         List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(

  12.                 beans.getIgnoredTypes(), beanFactory, context, considerHierarchy);

  13.         //因为实例代码中设置的是类型,所以这里会遍历类型,根据type获取目标bean是否存在

  14.         for (String type : beans.getTypes()) {

  15.             Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,

  16.                     context.getClassLoader(), considerHierarchy);

  17.             typeMatches.removeAll(beansIgnoredByType);

  18.             if (typeMatches.isEmpty()) {

  19.                 matchResult.recordUnmatchedType(type);

  20.             }

  21.             else {

  22.                 matchResult.recordMatchedType(type, typeMatches);

  23.             }

  24.         }

  25.         //根据注解寻找

  26.         for (String annotation : beans.getAnnotations()) {

  27.             List<String> annotationMatches = Arrays

  28.                     .asList(getBeanNamesForAnnotation(beanFactory, annotation,

  29.                             context.getClassLoader(), considerHierarchy));

  30.             annotationMatches.removeAll(beansIgnoredByType);

  31.             if (annotationMatches.isEmpty()) {

  32.                 matchResult.recordUnmatchedAnnotation(annotation);

  33.             }

  34.             else {

  35.                 matchResult.recordMatchedAnnotation(annotation, annotationMatches);

  36.             }

  37.         }

  38.         //根据设置的name进行寻找

  39.         for (String beanName : beans.getNames()) {

  40.             if (!beansIgnoredByType.contains(beanName)

  41.                     && containsBean(beanFactory, beanName, considerHierarchy)) {

  42.                 matchResult.recordMatchedName(beanName);

  43.             }

  44.             else {

  45.                 matchResult.recordUnmatchedName(beanName);

  46.             }

  47.         }

  48.         return matchResult;

  49.     }

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

 
  1.     Set<String> getNamesForType(Class<?> type) {

  2.         //同步spring容器中的bean

  3.         updateTypesIfNecessary();

  4.         //返回指定类型的bean

  5.         return this.beanTypes.entrySet().stream()

  6.                 .filter((entry) -> entry.getValue() != null

  7.                         && type.isAssignableFrom(entry.getValue()))

  8.                 .map(Map.Entry::getKey)

  9.                 .collect(Collectors.toCollection(LinkedHashSet::new));

  10.     }

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

我们跟进updateTypesIfNecessary()

 
  1.     private void updateTypesIfNecessary() {

  2.         //这里lastBeanDefinitionCount 代表已经同步的数量,如果和容器中的数量不相等,才开始同步。

  3.         //否则,获取beanFactory迭代器,开始同步。

  4.         if (this.lastBeanDefinitionCount != this.beanFactory.getBeanDefinitionCount()) {

  5.             Iterator<String> names = this.beanFactory.getBeanNamesIterator();

  6.             while (names.hasNext()) {

  7.                 String name = names.next();

  8.                 if (!this.beanTypes.containsKey(name)) {

  9.                     addBeanType(name);

  10.                 }

  11.             }

  12.             //同步完之后,更新已同步的beanDefinition数量。

  13.             this.lastBeanDefinitionCount = this.beanFactory.getBeanDefinitionCount();

  14.         }

  15.     }

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

继续跟进getBeanNamesIterator()

 
  1. @Override

  2.     public Iterator<String> getBeanNamesIterator() {

  3.         CompositeIterator<String> iterator = new CompositeIterator<>();

  4.         iterator.add(this.beanDefinitionNames.iterator());

  5.         iterator.add(this.manualSingletonNames.iterator());

  6.         return iterator;

  7.     }

分别来看:

  • 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是通过配置类触发解析的,则执行结果依赖配置类加载顺序

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值