转载

跟我学Spring之运行时动态注册bean

上文 跟我学Spring之Bean生命周期-BeanDefinition元信息解析 中,我们了解了Spring中Bean生命周期的BeanDefinition元信息解析原理。

本文就利用该部分的原理,实战一把运行时动态注册Bean的黑科技玩法。

需求场景

首先我们要明确,什么情况会使用到运行期动态注册Bean。

通常情况下,我们对Bean的操作都是在容器初始化完成,bean装载之后发生的。一般也用不到运行时动态注册。

但是在某些特殊场景下,就不得不使用了。

比如,有个遗留项目中依赖了一个三方类库,其中有一个Spring Bean,比如叫QueryService是用来做数据库操作的,它依赖了一个数据源,这里就假设它用的是JdbcTemplate,当然其他的JPA,MyBatis也都可以。

在项目启动时候会默认加载这个QueryService,然后QueryService会依赖JdbcTemplate。

此时,产品提出一个需求,要我们整合多数据源,但是不能对三方库做修改。

抽象一下就是,我们要在容器中针对多个数据源,加载多个QueryService,并且每个QueryService都需要依赖对应的数据源,也就是特定的JdbcTemplate实例。

需求不复杂,但难就难在我们如何才能动态的为QueryService设置特定的JdbcTemplate,因为在Spring初始化之后,JdbcTemplate实例已经注入完成了,就是默认的数据源。

我们编码的核心就是要在运行时初始化特定的JdbcTemplate替换掉默认JdbcTemplate,并且将bean重新注册到Spring容器中。

提到Bean注册,你想到了什么?

没错,就是我们在上文中分析的DefaultListableBeanFactory,而本文的核心操作,也是围绕DefaultListableBeanFactory展开的。话不多说,我们进入实际操作。

代码实操

为了方便理解,我们定义一个模拟的DBTemplate替代JdbcTemplate。实际开发中,根据具体依赖的Bean灵活替换即可。

DbTemplate

DbTemplate是一个模拟JdbcTemplate的实体

public class DbTemplate {

    private String dbName;
    private String userName;

    public DbTemplate(String dbName, String userName) {
        this.dbName = dbName;
        this.userName = userName;
    }

    public DbTemplate() {
    }

    public String getDbName() {
        return dbName;
    }

    public DbTemplate setDbName(String dbName) {
        this.dbName = dbName;
        return this;
    }

    public String getUserName() {
        return userName;
    }

    public DbTemplate setUserName(String userName) {
        this.userName = userName;
        return this;
    }

    @Override
    public String toString() {
        return "DbTemplate{" +
                "dbName='" + dbName + '/'' +
                ", userName='" + userName + '/'' +
                '}';
    }
}

就是一个POJO,实现了toString方法便于观察日志。

QueryService

QueryService是我们在需求阶段提到的三方库中的一个类,它被声明为一个Spring Bean注入容器中,实现InitializingBean接口,便于传递引用。

注意我们要注意的是,这里的QueryService在实际编码中是不可修改的,这里的代码可以认为是反编译Jar中的class得到的,便于我们观察类定义。

public class QueryService implements InitializingBean {

    @Autowired(required = false)
    DbTemplate defaultDbTemplate;

    private String name;

    public QueryService(String name) {
        this.name = name;
    }

    private static QueryService instance;

    public static QueryService instance() {
        return instance;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        instance = this;
        System.out.println("QueryService 初始化完成, name=" + name + ",dbTemplate: " + defaultDbTemplate.toString());
    }

    public QueryService() {
    }

    public String getName() {
        return name;
    }

    public QueryService setName(String name) {
        this.name = name;
        return this;
    }

    public DbTemplate getDefaultDbTemplate() {
        return defaultDbTemplate;
    }

    public QueryService setDefaultDbTemplate(DbTemplate defaultDbTemplate) {
        this.defaultDbTemplate = defaultDbTemplate;
        return this;
    }
}

可以看到,在QueryService中注入了DbTemplate,它的beanName=defaultDbTemplate

BeanConfig 注册类

我们编写一个BeanConfig注册类,声明要注入的Bean。使用XML配置文件能够达到同样的效果。

@Configuration
public class BeanConfig {

    /**
    * 用户库QueryService
    * @return
    */
    @Bean
    public QueryService userQueryService() {
        QueryService userQueryService = new QueryService("userQueryService");
        return userQueryService;
    }

    /**
    * 订单库QueryService
    * @return
    */
    @Bean
    public QueryService orderQueryService() {
        QueryService orderQueryService = new QueryService("orderQueryService");
        return orderQueryService;
    }


    /**
    * 默认的DbTemplate, 也是初始化注入到QueryService里的
    * @return
    */
    @Primary
    @Bean
    public DbTemplate defaultDbTemplate() {
        DbTemplate dbTemplate = new DbTemplate();
        dbTemplate.setDbName("default-db").setUserName("admin");
        return dbTemplate;
    }

    /**
    * DynamicQueryServiceHandler  更换QueryService中的DbTemplate引用
    * @return
    */
    @Bean
    public DynamicQueryServiceHandler dynamicQueryServiceHandler() {
        DynamicQueryServiceHandler dynamicQueryServiceHandler = new DynamicQueryServiceHandler();
        return dynamicQueryServiceHandler;
    }
}

BeanConfig是一个Bean的配置类,声明了QueryService的两个实例,

  • userQueryService-表示用户库QueryService实例
  • orderQueryService-表示订单库QueryService实例

声明了DbTemplate的默认实现,也就是QueryService依赖的DbTemplate实例;

我们还声明了一个dynamicQueryServiceHandler的bean,它就是本次文章说明的核心,主要作用为在运行期替换具体QueryService依赖的DbTemplate实例;我们在后面会详细分析。

客户端类Client

编写一个Client类用于验证我们编写的代码逻辑。

public class Client {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);
        // 获取DynamicQueryServiceHandler
        DynamicQueryServiceHandler dynamicQueryServiceHandler = applicationContext.getBean("dynamicQueryServiceHandler", DynamicQueryServiceHandler.class);

        // 初始化要替换的dbTemplate实例
        DbTemplate userDbTemplate = new DbTemplate("user-db", "userAdmin");
        DbTemplate orderDbTemplate = new DbTemplate("order-db", "orderAdmin");

        // 进行替换
        dynamicQueryServiceHandler.changeDbTemplate("userQueryService", "userDbTemplate", userDbTemplate);
        dynamicQueryServiceHandler.changeDbTemplate("orderQueryService", "orderDbTemplate", orderDbTemplate);

        // 打印更新之后的bean
        QueryService updatedUserQueryService = applicationContext.getBean("userQueryService", QueryService.class);
        QueryService updateOrderQueryService = applicationContext.getBean("orderQueryService", QueryService.class);
        System.out.println("updatedUserQueryService 更新完成, name=" + updatedUserQueryService.getName() + ",dbTemplate:" +
                updatedUserQueryService.getDefaultDbTemplate().toString());
        System.out.println("updateOrderQueryService 更新完成, name=" + updateOrderQueryService.getName() + ",dbTemplate:" +
                updateOrderQueryService.getDefaultDbTemplate().toString());
    }
}

这里先卖个关子,我们先不看DynamicQueryServiceHandler具体的代码实现,只需要知道定义了DynamicQueryServiceHandler这个bean,注入到Spring容器中的beanName是dynamicQueryServiceHandler。

main方法主要做了如下几件事

  1. 定义并初始化了AnnotationConfigApplicationContext,通过构造方法注入BeanConfig配置类,用于加载并初始化我们声明的bean;同时返回ApplicationContext上下文
  2. 从ApplicationContext中根据beanName获取DynamicQueryServiceHandler实例
  3. 此时容器初始化完成,如果bean实现了InitializingBean接口,在容器加载过程中,会以此回调afterPropertiesSet()方法,有日志则打印日志
  4. 由于QueryService实现了InitializingBean接口,因此我们能在控制台看到QueryService打印出初始化日志
  5. 我们接着构造了两个具体的DbTemplate对象,类比到实际开发中,就是我们根据具体数据源的配置,创建出对应的数据源,并初始化对应的JdbcTemplate对象
  6. 接着调用 dynamicQueryServiceHandler.changeDbTemplate方法,传入要替换DBTemplate的具体QueryService实例的beanName,以及我们创建的DBTemplate实例引用,以及对应的beanName(根据业务灵活指定即可,不要同名);dynamicQueryServiceHandler.changeDbTemplate方法会将替换好的QueryService实例重新注册到Spring容器上下文中
  7. 替换完成之后,我们重新获取一下beanName为userQueryService,orderQueryService的两个bean,并打印一下其中的属性(包含依赖的DBTemplate)是否已经变更。

到此就是Client类的完整逻辑。我们先运行一下看看效果

控制台打印

...省略部分debug日志...
QueryService 初始化完成, name=userQueryService,dbTemplate: DbTemplate{dbName='default-db', userName='admin'}
QueryService 初始化完成, name=orderQueryService,dbTemplate: DbTemplate{dbName='default-db', userName='admin'}
finished  class com.dynamic.bean.DbTemplate
finished  class java.lang.String
finished  class com.dynamic.bean.QueryService
13:45:22.995 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - 
        Overriding bean definition for bean 'userQueryService' with a different definition: 
finished  class com.dynamic.bean.DbTemplate
finished  class java.lang.String
finished  class com.dynamic.bean.QueryService
13:45:22.995 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - 
        Overriding bean definition for bean 'orderQueryService' with a different definition:

这部分是Spring容器加载阶段的日志,可以看到在Spring容器初始化过程中,注入了userQueryService,orderQueryService两个QueryService实例,并分别注入了默认的DbTemplate实例。

13:45:22.995 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'userQueryService'
13:45:23.002 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'userDbTemplate'
QueryService 初始化完成, name=userQueryService,dbTemplate: DbTemplate{dbName='user-db', userName='user-db'}

这里就是执行dynamicQueryServiceHandler.changeDbTemplate替换了DBTemplate之后重新注册userQueryService的日志打印

13:45:23.016 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderQueryService'
13:45:23.016 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderDbTemplate'
QueryService 初始化完成, name=orderQueryService,dbTemplate: DbTemplate{dbName='order-db', userName='order-db'}

这里逻辑同上,是执行dynamicQueryServiceHandler.changeDbTemplate替换了DBTemplate之后重新注册orderQueryService的日志打印

updatedUserQueryService 更新完成, name=userQueryService,dbTemplate:DbTemplate{dbName='user-db', userName='user-db'}
updateOrderQueryService 更新完成, name=orderQueryService,dbTemplate:DbTemplate{dbName='order-db', userName='order-db'}

这里是我们在main方法中打印的日志,输出表明我们已经将默认的DBTemplate成功替换为对应的userDbTemplate和orderDbTemplate。

之后我们就可以使用userQueryService操作user数据源,使用orderQueryService操作order数据源了。

分析DynamicQueryServiceHandler实现

到此,流程就梳理完成了。我们还有一个悬念没有解开,就是DynamicQueryServiceHandler具体是如何实现的?

接下来就详细分析一下DynamicQueryServiceHandler的代码逻辑。

首先声明DynamicQueryServiceHandler为一个Spring的Component,将其注册到Spring上下文中。

@Component
public class DynamicQueryServiceHandler implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

通过实现ApplicationContextAware接口,使DynamicQueryServiceHandler能够获取到ApplicationContext上下文的引用,便于操作。

changeDbTemplate方法是核心替换逻辑,它接受三个参数

  • queryServiceName, 要进行替换操作的QueryService引用
  • dbTemplateBeanName,实际替换的DbTemplate的BeanName
  • dbTemplate,实际替换的DbTemplate实例引用。(实例化之后传入即可)

    public void changeDbTemplate(String queryServiceName, String dbTemplateBeanName, DbTemplate dbTemplate) {
        QueryService queryService = applicationContext.getBean(queryServiceName, QueryService.class);
        if (queryService == null) {
            return;
        }

step0:首先通过queryServiceName获取到容器中已经注册的具体的QueryService实例

// 更新QueryService中的dbTemplate引用然后重新注册回去
Class<?> beanType = applicationContext.getType(queryServiceName);
if (beanType == null) {
    return;
}

Field[] declaredFields = beanType.getDeclaredFields();
for (Field field : declaredFields) {
    // 从spring容器中拿到这个具体的bean对象
    Object bean = queryService;
    // 当前字段设置新的值
    try {
        field.setAccessible(true);
        Class<?> type = field.getType();
        if (type == DbTemplate.class) {
            field.set(bean, dbTemplate);
        }
        System.out.println("finished  " + type);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这段代码逻辑用到了反射,概括起来解释就是,我们取得了queryServiceName对应的Class,也就是QueryService.class。

然后获得QueryService.class的属性,并进行遍历,通过反射设置属性为可访问的,重点在于if逻辑:

如果判断属性的类型为DbTemplate.class,则将我们传入的dbTemplate实例设置给queryService实例。

这段逻辑完成之后,我们就获得了一个具备特定DBTemplate引用的QueryService实例。只不过它还是游离于Spring容器的,需要我们再将其注册回Spring上下文。

// 刷新容器中的bean,获取bean工厂并转换为DefaultListableBeanFactory
defaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

重头戏来了,通过applicationContext,我们获取到了DefaultListableBeanFactory实例,也就是BeanDefinitionRegistry实例。这部分不理解的一定要回过头去看上一篇文章 !

// 刷新DbTemplate的bean定义
BeanDefinitionBuilder dbTemplatebeanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DbTemplate.class);
dbTemplatebeanDefinitionBuilder.addPropertyValue("dbName", dbTemplate.getDbName());
dbTemplatebeanDefinitionBuilder.addPropertyValue("userName", dbTemplate.getDbName());

这里的核心是通过BeanDefinitionBuilder为传入的DbTemplate引用,创建Bean定义,设置BeanDefinition的属性为传入的DbTemplate引用的具体属性值。

// 通过BeanDefinitionBuilder创建bean定义
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(QueryService.class);
// 设置属性defaultDbTemplate,此属性引用已经定义的bean,这里defaultDbTemplate已经被spring容器管理了.
beanDefinitionBuilder.addPropertyReference("defaultDbTemplate", dbTemplateBeanName);
// 刷新QueryService的DbTemplate引用
beanDefinitionBuilder.addPropertyValue("name", queryServiceName);

这里就和上面大同小异,我们还需要刷新QueryService实例的BeanDefinition,因此通过BeanDefinitionBuilder为QueryService创建Bean定义,并将defaultDbTemplate引用指向我们传入的待替换的dbTemplateBeanName,(举个例子,比如给userQueryService的defaultDbTemplate引用设置成userDbTemplate)。

最后通过beanDefinitionBuilder.addPropertyValue(“name”, queryServiceName);刷新其他属性,这里的name属性是为了打印日志方便增加的一个名称属性。可以根据需要灵活添加。

// 重新注册bean
defaultListableBeanFactory.registerBeanDefinition(dbTemplateBeanName, dbTemplatebeanDefinitionBuilder.getRawBeanDefinition());
defaultListableBeanFactory.registerBeanDefinition(queryServiceName, beanDefinitionBuilder.getRawBeanDefinition());

最后,调用 defaultListableBeanFactory.registerBeanDefinition(String beanName, BeanDefinition beanDefinition) 方法,将更新之后的dbTemplate,queryService的beanDefinition注册回Spring容器中。

之后我们就可以使用刷新后的QueryService引用操作具体的DbTemplate对应的数据源了。

小结

到此,我们就通过一个完整的实战案例,从实操到分析,全方位的实践了 “运行时动态注册bean” 的黑科技操作。

Spring框架中这类特性还有很多,他们无一例外都以IOC、AOP为核心构建。

我们一直说IOC、AOP,但是真正能够灵活运用的却少之又少,这给我的启示就是一定不能空谈,要以实践结合理论。

追根溯源,唯有掌握Spring框架的核心机理,对于重点代码和原理熟练掌握,才能在错综复杂的需求中提炼出解决方案,并且优雅的解决问题。

希望本文能够对聪明的你有所启发。

更多Spring源码解析,请拭目以待。

版权声明:

原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。

原文  http://wuwenliang.net/2020/03/11/跟我学Spring之运行时动态注册bean/
正文到此结束
Loading...