Recuerde un ImportBeanDefinitionRegistrar usado junto con ImportSelector, lo que resulta en ideas de solución de fallas de @ConditionalOnBean

antecedentes

Recientemente, estoy escribiendo algunos componentes para la empresa y, naturalmente, usaré más clases de extensión de Spring. Me encontré con un problema extraño. Combinado con el proceso de creación de Spring Bean, registraré la solución de problemas y las ideas de solución. El fenómeno:

  1. Úselo para ImportBeanDefinitionRegistrarescanear el paquete de componentes (el motivo es que el proyecto de componentes no es coherente con el paquete de servicios web, por lo que debe escribir manualmente BeanDefinitionScannerpara escanear el paquete)
  2. Usar para ImportSelectorimportar una clase de configuración

Finalmente, se encuentra que cuando los dos se usan juntos, ImportSelectorlas siguientes dos anotaciones condicionales en la clase de configuración importada se ImportBeanDefinitionRegistrarinvalidarán y los beans escaneados por el escáner personalizado no serán válidos.

@ConditionalOnBean
@ConditionalOnMissingBean

adivinar:

  1. @ConditionalOnMissingBeanImportBeanDefinitionRegistrar¿Por qué falla?La alta probabilidad es que el bean exportado no se haya inicializado en el contenedor Spring cuando se ejecuta el juicio condicional.
  2. Se supone que la prioridad de inicialización de los beans importados usando @Import es mayor

Recurrencia del problema

Primero crea una demostración para reproducir el problema

/**
 * 一个老师的接口
 * @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 {
}

Primero defina la clase del maestro. El efecto que queremos lograr es que si no hay un maestro senior en Spring, se usará el maestro predeterminado. Para tener cuidado con:

  1. Profesor senior, usando @Component, el paquete se inyecta automáticamente en Spring
  2. El maestro predeterminado, no usa @Component, es importado por la configuración posterior TestAutoConfiguration y hace restricciones condicionales
/**
 * 配置类,用于判断是否存在高级老师,若无,则使用默认老师
 *
 * @author yejunxi 2022/06/29
 */
public class TestAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(Teacher.class)
    public Teacher teacherDefault() {
        return new TeacherDefault();
    }
}

Otra anotación para abrir el componente, importar ImportSelector e ImportBeanDefinitionRegistrar respectivamente

@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;
    }
}

Se escribe el código del componente y luego se usa Spring Boot para iniciar la prueba de la clase. Para simular el escenario real (el paquete del componente es diferente del paquete de la clase de inicio), agrego una clase de inicio al directorio de prueba.

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

Escribir una clase de prueba, citando al profesor.

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

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

La estructura de directorios final se muestra en la figura.

image.png

Ejecutar, si no hay accidente, se informará de un error

image.png

La excepción es muy simple, porque se encontraron 2 maestros en el contenedor Spring, por lo que se informó un error.

Solucionar problemas de ideas

Para verificar la conjetura anterior

Primero hacemos clic en @ConditionalOnMissingBean y encontramos que la clase para su lógica de juicio esorg.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);

Supongo que te gusta

Origin juejin.im/post/7116728662224814111
Recomendado
Clasificación