前言

有时候我们需要在应用启动的时候根据某些规则动态创建一个Bean对象,我们应该要考虑一些事情。

  • 对象是否在创建后才能使用,这样避免有些对象在定义的时候就由于Bean创建的顺序问题导致Bean都没有初始化给容器就被使用了,这样启动的时候出现一些null异常。
  • 对象的属性或描述从第三方配置中心获取,这些配置是否能在Bean定义中就能获取到。

大概流程如图:
image

动态创建Bean的两种方式

1、ImportBeanDefinitionRegistrar

第一步:创建实现ImportBeanDefinitionRegistrar接口的类, 并实现registerBeanDefinitions方法DeanDefintion的注册即可交给Spring去初始化

@Configuration
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        GenericBeanDefinition beandefinition=new GenericBeanDefinition();
        beandefinition.setBeanClassName("你的类全路径");
        beandefinition.getPropertyValues().add("id",1);
        beandefinition.getPropertyValues().add("name","cdg");

        registry.registerBeanDefinition("testComponent",beandefinition);
    }
}

第二步:结合@Import让它生效,两者搭配使用。

@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)

2、BeanDefinitionRegistryPostProcessor

此方式有两种定义方式可以玩,一种是构造函数,一种是不用构造函数。

@Configuration
public class MyBeanDefinitionRegistryPostProcessor implements EnvironmentAware, BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {

        // 构造函数使用方式
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClass(你需要创建的类.class);
        ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
        constructorArgumentValues.addIndexedArgumentValue(0, tenants);
        beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
        beanDefinitionRegistry.registerBeanDefinition("dynamicRedisTemplate", beanDefinition);
        
        // 非构造函数使用方式
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                        .genericBeanDefinition(你需要创建的类.class);
        beanDefinitionBuilder.setInitMethodName("init");
        beanDefinitionBuilder.setDestroyMethodName("close");
        beanDefinitionBuilder.addPropertyValue("url", config.getUrl());
        beanDefinitionRegistry.registerBeanDefinition(config.getDataName(), beanDefinition);
    }
}    

以上的代码相信大家琢磨一下也能看的懂,我假设要实现一个DruidDataSource数据源Bean,该类里面有init和close方法需要我去指定,所以里面就写了这个例子。

配置文件加载

这点其实应该要提前说明的,这里有个坑要淌很久时间才能熟悉其门道。

我在做这个POC的时候用的配置文件是一个yml文件在classpath下,我用Spring的@Configuration定义了解析Bean,但是在这里就是得不到值,都是空的。后来我看了Spring的启动流程才发现,BeanDefinition是最早去创建的,@Configuration定义的配置类是在他之后才会被加载,用@Condition注解也没作用。
再后来发现只能去实现EnvironmentAware从环境变量中拿,然后又担心第三方Apollo是否比他还要滞后,但查看源码后发现Apollo实现的配置加载是在这个之前并且提交给了Spring的环境变量容器,所以我用 String json = environment.getProperty("dynamic.datasource.json");是可以拿到第三方配置中心里面的配置。

所以我就定义了一个统一初始化配置文件转对象的类,但是要注意只转换一次,避免多线程使用的不安全行为,请看我的实现

public final class JsonInit {
    private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);

    private static List<Tenants> tenants;

    public static List<Tenants> doInit(String json) {
        if (!INITIALIZED.compareAndSet(false, true)) {
            return tenants;
        }
        tenants = JSON.parseArray(json, Tenants.class);
        if (tenants == null) {
            log.warn("dynamic.datasource.json is null!");
            tenants = new ArrayList<>();
        }

        return tenants;
    }
}

到此就大概能完整了。

上一篇 下一篇