记一篇 ImportBeanDefinitionRegistrar与ImportSelector一起使用,导致@ConditionalOnBean 失效解决思路

1,323 阅读6分钟

背景

最近在给公司写一些组件,自然会用到比较多的Spring拓展类,遇到一个奇异问题,结合Spring Bean创建过程,记录一下排查解决思路,现象:

  1. 使用到ImportBeanDefinitionRegistrar 对组件包扫包(原因是组件项目与web服务包不一致,所以需要手动写BeanDefinitionScanner进行扫包)
  2. 使用到ImportSelector导入某配置类

最后发现两者一起使用后,会导致ImportSelector导入的配置类中下面2个条件注解,对ImportBeanDefinitionRegistrar自定义扫描器扫描的bean失效

@ConditionalOnBean
@ConditionalOnMissingBean

猜想:

  1. @ConditionalOnMissingBean为何失效,大概率是执行条件判断的时候Spring容器里面还没有初始化ImportBeanDefinitionRegistrar所导出的Bean
  2. 那猜想使用@Import导入的Bean的初始化优先级比较高

问题复现

先搭一个Demo复现问题

/**
 * 一个老师的接口
 * @author yejunxi 2022/06/29
 */
public interface Teacher {
}
/**
 * 默认老师 
 *
 * @author yejunxi 2022/06/29
 */
public class TeacherDefault implements Teacher {
}
/**
 * 高级老师
 *
 * @author yejunxi 2022/06/29
 */
@Component
public class TeacherAdvanced implements Teacher {
}

先定义老师的类,我们想到达到的效果就是若Spring中没有高级老师任教,那就使用默认老师。要注意的是:

  1. 高级老师,使用了@Component,扫包自动注入Spring
  2. 默认老师,不使用@Component,由后面的配置TestAutoConfiguration导入,并作出条件限制
/**
 * 配置类,用于判断是否存在高级老师,若无,则使用默认老师
 *
 * @author yejunxi 2022/06/29
 */
public class TestAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(Teacher.class)
    public Teacher teacherDefault() {
        return new TeacherDefault();
    }
}

再来一个开启组件的注解,分别导入ImportSelector与ImportBeanDefinitionRegistrar

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({TestImportSelector.class, TestImportBeanDefinitionRegistrar.class})
public @interface TestEnable {

}
public class TestImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{TestAutoConfiguration.class.getName()};
    }
}
public class TestImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    private Environment environment;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //扫描组件的包
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, true, environment);
        scanner.scan("com.heys1.test");
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

组件的代码编写完毕,接下来使用Spring boot启动类测试,为模拟实际场景(组件包与启动类包不一样),我在test目录新增启动类

@SpringBootApplication
@TestEnable
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication sa = new SpringApplication(TestApplication.class);
        sa.run(args);
    }
}

写一个测试类,引用老师

/**
 * @author yejunxi 2022/06/29
 */
@Component
public class Tester {
    @Autowired
    Teacher teacher;

    @PostConstruct
    public void init(){
        System.out.println(teacher);
    }
}

最终的目录结构如图

image.png

运行,如无意外,将会报错

image.png

异常很简单,就是因为Spring容器中找到了2个Teacher,所以报错了

排查思路

为验证上面的猜想

我们先点进@ConditionalOnMissingBean,发现它判断逻辑的类是org.springframework.boot.autoconfigure.condition.OnBeanCondition

那我们先在org.springframework.boot.autoconfigure.condition.OnBeanCondition#getOutcomes打断点,查看它的调用链路

image.png

了解过SpringBean声明周期都知道,Spring必然是在org.springframework.context.support.AbstractApplicationContext#refresh对Bean进行初始化,我们先从此处入手

一条一条点一下,看是在哪儿创建配置类的BeanDefinition

image.png 然后发现有一个 processConfigBeanDefinitions 方法,看这方法名,似乎就是我想找的。再往上点org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions 中的parser.parse(candidates) 就是问题所在

原因是经过多次断点观察,在本方法内的this.reader.loadBeanDefinitions(configClasses);会对生成配置类的definition,而决定需要加载哪些配置类的代码是parser.parse(candidates) ,这个行代码是用来筛出哪些是ConfigurationClass,再对这些配置类进行初始化

企业微信截图_386cc999-46a5-4ca1-afa9-b8ec8855eebb.png

我们取消运行刚刚的断点,重新在parser.parse(candidates)打断点,看一下执行完这个方法后有什么变化

image.png

哦吼,在这个configurationClasses里面居然找不到我们的高级老师类,我们放行掉这个断点,因为这是一个do循环,发现第二次循环就出现我们的高级老师类了

先说结论:

  1. ImportBeanDefinitionRegistrar 所引入的类不算configurationClasses,而且是比configurationClasses更晚加载
  2. ImportBeanDefinitionRegistrarImportSelector 本身均不会进入单例池

接下来我们只需搞清楚parser.parse(candidates); 搞了啥就行了

public void parse(Set<BeanDefinitionHolder> configCandidates) {
   for (BeanDefinitionHolder holder : configCandidates) {
      BeanDefinition bd = holder.getBeanDefinition();
      try {
         if (bd instanceof AnnotatedBeanDefinition) {
            //p1 我们的配置类都是通过注解注入的,所以会走此方法
            parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
         }
         else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
            parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
         }
         else {
            parse(bd.getBeanClassName(), holder.getBeanName());
         }
      }
      catch (BeanDefinitionStoreException ex) {
         throw ex;
      }
      catch (Throwable ex) {
         throw new BeanDefinitionStoreException(
               "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
      }
   }

   this.deferredImportSelectorHandler.process();
}

继续进去p1的方法,doProcessConfigurationClass应该就是核心逻辑

image.png

protected final SourceClass doProcessConfigurationClass(
      ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
      throws IOException {
    略... 

   // Process any @ComponentScan annotations
   // 在Spring Boot下,第一个加载的配置类是启动类,此处会找到 @ComponentScan,并注入包里面的Bean

   Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
         sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
   if (!componentScans.isEmpty() &&
         !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
      for (AnnotationAttributes componentScan : componentScans) {
         // The config class is annotated with @ComponentScan -> perform the scan immediately
         // 被扫描的Bean
         Set<BeanDefinitionHolder> scannedBeanDefinitions =
               this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
         // Check the set of scanned definitions for any further config classes and parse recursively if needed
         for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
            BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
            if (bdCand == null) {
               bdCand = holder.getBeanDefinition();
            }
            if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
               parse(bdCand.getBeanClassName(), holder.getBeanName());
            }
         }
      }
   }

   // Process any @Import annotations
   // 处理@Import的配置,我们启动类是通过
   // @Import({TestImportSelector.class, TestImportBeanDefinitionRegistrar.class}) 
   // 导入的这两个配置类,所以进去看看
   processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
    
   略... 
}
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
      Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
      boolean checkForCircularImports) {

   if (importCandidates.isEmpty()) {
      return;
   }

   if (checkForCircularImports && isChainedImportOnStack(configClass)) {
      this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
   }
   else {
      //会走这里
      this.importStack.push(configClass);
      try {
         for (SourceClass candidate : importCandidates) {
            // 处理 ImportSelector
            if (candidate.isAssignable(ImportSelector.class)) {
               // Candidate class is an ImportSelector -> delegate to it to determine imports
               Class<?> candidateClass = candidate.loadClass();
               ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
                     this.environment, this.resourceLoader, this.registry);
               Predicate<String> selectorFilter = selector.getExclusionFilter();
               if (selectorFilter != null) {
                  exclusionFilter = exclusionFilter.or(selectorFilter);
               }
               if (selector instanceof DeferredImportSelector) {
                  this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
               }
               else {
                  // 递归操作,导入Bean,走到外层if判断的else分支上
                  String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                  Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
                  processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
               }
            }
            //p2
            //处理ImportBeanDefinitionRegistrar
            //可以看出,此处并没有立即将里面的自定义BeanDefinitionScanner 所扫描的Bean作为配置类
            else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
               // Candidate class is an ImportBeanDefinitionRegistrar ->
               // delegate to it to register additional bean definitions
               Class<?> candidateClass = candidate.loadClass();
               ImportBeanDefinitionRegistrar registrar =
                     ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                           this.environment, this.resourceLoader, this.registry);
               configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
            }
            else {
               // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
               // process it as an @Configuration class
               this.importStack.registerImport(
                     currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
               processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
            }
         }
      }
      catch (BeanDefinitionStoreException ex) {
         throw ex;
      }
      catch (Throwable ex) {
         throw new BeanDefinitionStoreException(
               "Failed to process import candidates for configuration class [" +
               configClass.getMetadata().getClassName() + "]", ex);
      }
      finally {
         this.importStack.pop();
      }
   }
}

根据p2处得知,处理ImportBeanDefinitionRegistrar时

configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());

只是使用一个Map将其保存起来,并没有执行 ImportBeanDefinitionRegistrar#registerBeanDefinitions(),自然也不会扫到我们自定义包下的Bean

那我们看看这个Map在何处用到

image.png

IDEA 查看哪里调用到getImportBeanDefinitionRegistrars(),发现是在org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass

image.png

那又是在哪儿调用这个方法呢,一直找上去,发现是

image.png

这行代码上面提到过,是用于实例化BeanDefinition,那到这流程就清晰了

  1. 第一步 启动类(xxApplication) 作为入口配置; (parser.parse(candidates))
  2. 第二步 找到启动类中@ComponentScan 扫描的bean; (parser.parse(candidates))
  3. 第三步 找到启动类中@Import 导入的bean; (parser.parse(candidates))
  4. 按顺序加载上面配置类 第一次循环的this.reader.loadBeanDefinitions(configClasses);
  5. 加载完后再处理ImportBeanDefinitionRegistrar,若里面有导入的Bean则在 第二次循环的this.reader.loadBeanDefinitions(configClasses); 才加载

核心方法均在org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions 方法中下面的两行代码,多观察断点即可

  • parser.parse(candidates);
  • this.reader.loadBeanDefinitions(configClasses);