1.前言

由于所在公司的的现有的开发框架要改造成基于SpringBoot构建,以此来兼容SringBoot的宽泛的技术积累,在此结合过程中,需要根据配置来动态生成Bean,但发现一些方式生成的bean会导致如@ConditionalOnBean等注解的失效,这里记录一下过程。

2.如何动态创建bean

Spring在进行bean的实例化之前都会在BeanFactory中注册成一个个的BeanDefinition,这些BeanDefinition包含了bean创建的属性,比如单例或者多例,以及设置Bean创建的过程的bean创建工厂方法。也就是说,只要把这些BeanDefinition注册到BeanFactory中,就能通过getBean的方式获取到bean的实例。刚开始我的方式简写后是这样的:

2.1第一种

准备一个要被动态创建的Bean类

package org.test;

/**
 * @author LHL
 * @version 1.0
 * @Description
 * @date 2021/9/20 12:08
 * @since
 */
public class Mybean {
    private String des ="this is a Bean";
    
    @Override
    public String toString() {
        return "Mybean{" + "des='" + des + '\'' + '}';
    }
}

准备用来注册和获取bean的类

package org.test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

/**
 * @author LHL
 * @version 1.0
 * @Description
 * @date 2021/9/20 12:07
 * @since
 */
@Configuration
public class RegisterAndGetBean {
    @Autowired
    private ApplicationContext applicationContext;
    
    public void registerBean() {
        System.out.println("registerBean");
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Mybean.class);
        BeanDefinition beanDefinition = null;
        beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        ((DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory())
            .registerBeanDefinition("myBean", beanDefinition);
        
    }
    
    public void getBean() {
        System.out.println("getBean");
        applicationContext.getBean(Mybean.class).toString();
    }
}

然后动态注册和获取

package org.test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;

/**
 * @author LHL
 * @version 1.0
 * @Description
 * @date 2021/9/19 22:02
 * @since
 */
@SpringBootApplication
@Configuration()
public class AppMain {
    @Autowired
    private RegisterAndGetBean registerAndGetBean;
    public static void main(String[] args) {
        new SpringApplication(AppMain.class).run(args);
    }
    
    @EventListener
     public void started(ApplicationStartedEvent startedEvent) {
        registerAndGetBean.registerBean();
        registerAndGetBean.getBean();
    }
}

运行的结果

registerBean
getBean
Mybean{des='this is a Bean'}

显然这种方式是正常的使用ioc的,但是注册Bean的时机太靠后了,也就会导致如连@Autowired这种注解注入bean的方式也会失效:
如下:

@SpringBootApplication
@Configuration()
@SpringBootApplication
@Configuration()
public class AppMain {
    @Autowired
    private RegisterAndGetBean registerAndGetBean;
    public static void main(String[] args) {
        new SpringApplication(AppMain.class).run(args);
    }
    
    
    @Autowired
    private Mybean mybean;
    @EventListener
     public void started(ApplicationStartedEvent startedEvent) {
     
        registerAndGetBean.registerBean();
        registerAndGetBean.getBean();
        mybean.toString();
    }
}

输出的会是


Action:

Consider defining a bean of type 'org.test.Mybean' in your configuration.

2.1第二种

所以上面的方式显然是不行的,时候我尝试着提前创建Bean定义的时机,根据Spring的生命周期,我们可以在实例化之前,通过Spring的扩展接口:Bean定义后置处理器,即BeanDefinitionRegistryPostProcessor来注册Bean定义。这个时候bean都没有实例化,在即将实例化之前会调用这个接口。代码如下:

package org.test;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.annotation.Configuration;

/**
 * @author LHL
 * @version 1.0
 * @Description
 * @date 2021/9/20 12:48
 * @since
 */
@Configuration
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(Mybean.class);
        BeanDefinition beanDefinition = null;
        beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition("Mybean", beanDefinition);
    }
    
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)
        throws BeansException {
        
    }
}

然后获取的方式改成

@SpringBootApplication
@Configuration()
public class AppMain {
    @Autowired
    private RegisterAndGetBean registerAndGetBean;
    public static void main(String[] args) {
        new SpringApplication(AppMain.class).run(args);
    }
    
    
    @Autowired
    private Mybean mybean;
    @EventListener
     public void started(ApplicationStartedEvent startedEvent) {
        System.out.println(mybean.toString());
    }
}

运行结果

Mybean{des='this is a Bean'}

现在,可以正常使用@Autowired的功能了,但是这时我们试试其他的注解能不能正常使用,比如说@ConditionalOnBean
我尝试改成如下

@SpringBootApplication
@Configuration()
public class AppMain {
    @Autowired
    private RegisterAndGetBean registerAndGetBean;
    public static void main(String[] args) {
        new SpringApplication(AppMain.class).run(args);
    }
    
    @ConditionalOnBean(Mybean.class)
    @Bean
    public String hello(){
        System.out.println("hello ConditionalOnBean");
        return "";
    }
    
    @Autowired
    private Mybean mybean;
//    @EventListener
//     public void started(ApplicationStartedEvent startedEvent) {
//        System.out.println(mybean.toString());
//    }
}

如果在我预想的情况下,会成功调用hello()方法,但事实是没有,那也就是说spring在进行@ConditionalOnBean是判断到没有这个bean,那么,是不是我的时机还不够靠前,导致做此判断是还没有运行到我的后置处理器,我打在 @ConditionalOnBean的处理方法处打了个断点:
在这里插入图片描述
发现正是我想的那样,他更靠前,我查询源码,发现最终的根源出现在@ConditionalOnBean做判断时,起源调用点是从ConfigurationClassPostProcessor开始的,它也是一个后置处理器,但此处理器比我定义的更靠前,那现在解决方式就有了,只要我在它之前就可以注册好我的bean定义就可以了,于是我进一步翻看源码,想寻找一个后置处理器是什么时候加入处理器链的,追踪找到了如下代码
在这里插入图片描述
调用栈如下:
在这里插入图片描述
这样的话,这个后置处理器是spring在容器中默认扫描加入的也就是说:我现在的方式是无法正常能做到比他靠前的,@Order这种也不行,那开源的一些著名框架是怎么做到的?我参考了mybatis的自动配置启动器。
发现了这样一段代码:

  public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

    private BeanFactory beanFactory;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      if (!AutoConfigurationPackages.has(this.beanFactory)) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
        return;
      }

      logger.debug("Searching for mappers annotated with @Mapper");

      List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
      if (logger.isDebugEnabled()) {
        packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
      }

      BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
      builder.addPropertyValue("processPropertyPlaceHolders", true);
      builder.addPropertyValue("annotationClass", Mapper.class);
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
      BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
      Stream.of(beanWrapper.getPropertyDescriptors())
          // Need to mybatis-spring 2.0.2+
          .filter(x -> x.getName().equals("lazyInitialization")).findAny()
          .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
      registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
      this.beanFactory = beanFactory;
    }

  }

是的,这里也动态创建了bean定义,那这个后置处理器是如何被注册的?找到注册点发现代码如下:

 @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
    }

  }

2.1第二种

这也就引出了第三种,这里在@Configuration注解配置使用了@Import(),同时它的后置处理器实现的是ImportBeanDefinitionRegistrar ,这种方式的时机要更靠前么?我将自己动态定义bean的代码改造成了如下:

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author LHL
 * @version 1.0
 * @Description
 * @date 2021/9/20 13:10
 * @since
 */
public class MyBeanImportDefinitionRegistryPostProcessor implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //构造bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(Mybean.class);
        BeanDefinition beanDefinition = null;
        beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        registry.registerBeanDefinition("Mybean", beanDefinition);
    }
}

@Configuration
//这里引入
@Import(MyBeanImportDefinitionRegistryPostProcessor.class)
public class RegisterAndGetBean {
}
@SpringBootApplication
@Configuration
public class AppMain {
    @Autowired
    private RegisterAndGetBean registerAndGetBean;
    public static void main(String[] args) {
        new SpringApplication(AppMain.class).run(args);
    }
    
    @Bean
    @ConditionalOnBean(Mybean.class)
    public String hello(){
        System.out.println("hello ConditionalOnBean");
        return "";
    }
    
    @Autowired
    private Mybean mybean;
//    @EventListener
//     public void started(ApplicationStartedEvent startedEvent) {
//        System.out.println(mybean.toString());
//    }
}

然后调用输出:

hello ConditionalOnBean

发现可以正常使用了,虽然这里可以正常使用了,但是有给新的疑问点,当我把hello()移动到@Configuration 配置类RegisterAndGetBean 时,会发现它失效了。

@Configuration
@Import(MyBeanImportDefinitionRegistryPostProcessor.class)
public class RegisterAndGetBean {
    @Bean
    @ConditionalOnBean(Mybean.class)
    public String hello(){
        System.out.println("hello ConditionalOnBean");
        return "";
    }
}

这里分两个,第一步,为什么这种方法能成功,第二步,为什么在hello()放到里面会失效.

第一步

探究源码发现,之所以能成功是在ConfigurationClassPostProcessor中针对ImportBeanDefinitionRegistrar 做了扩展处理,源码如下:
在这里插入图片描述
这里会再次收集bean定义,也就是能在ConfigurationClassPostProcessor过程动态注册好bean的原因。

第二步

那为什么放在内部就无法成功了?通过源码探究如下:
首先找到具体匹配的逻辑源码
在这里插入图片描述
发现当发现失效的情况下,是先进行判断逻辑再去动态生成bean的,那么表面原因就找到了,那根本原因时什么,最后找到了如下代码:
在这里插入图片描述
上面那个箭头会获取内部方法bean定义并进行bean存在判断,第二个方法会import动态bean后置处理器,会发现,如果你@import所在的Confuguration在你的 @ConditionalOnBean后的话,那么将会因为先后顺序导致判断失败,也就是这个是否我们只要想办法调整下@Confuguration类的顺序就可以了,把@import所在的@Confuguration类调的更靠前,进一步找到了处理链的存放容器,如下:
在这里插入图片描述
再往上查找:
在这里插入图片描述
最后一直查找发现并没有什么能决定顺序,是通过扫描的包名和类名类决定的,这样的不可控因素太多,显然是不行的。那就只能从@Configuration的创建方式进行解决了,经了解一般有如下创建方法:

  • 手动:构建ApplicationContext时由构建者手动传入,可手动控制顺序。高优先级
  • 自动1:被@ComponentScan自动扫描进去,无法控制顺序。高优先级
  • 自动2:通过SPI的方式,可以控制顺序,低优先级。

那也就是最后的解决方案如下:

  • 如果是EnableAutoConfiguration可以通过@AutoConfigureBefore的方式
  • 第二种就是通过手动指定的方式:
ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class, Config2.class);

也就是只能在局限的范围内决定先后顺序…

总结

spring的bean的注册时机对bean的管理影响很大,如果是如@ConditionalOnBean这些注解,还是不要用在@Component及其子类的判断上,因为先后顺序的不确定性会导致系统的不稳定,同时如果这个bean是在SPI的方式bean里创建的,一定是判断不通过的。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐