文章目录
  1. 1. 需求场景
  2. 2. 代码实操
    1. 2.1. DbTemplate
    2. 2.2. QueryService
    3. 2.3. BeanConfig 注册类
    4. 2.4. 客户端类Client
      1. 2.4.1. 控制台打印
    5. 2.5. 分析DynamicQueryServiceHandler实现
  3. 3. 小结

上文 跟我学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源码解析,请拭目以待。



版权声明:

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

文章目录
  1. 1. 需求场景
  2. 2. 代码实操
    1. 2.1. DbTemplate
    2. 2.2. QueryService
    3. 2.3. BeanConfig 注册类
    4. 2.4. 客户端类Client
      1. 2.4.1. 控制台打印
    5. 2.5. 分析DynamicQueryServiceHandler实现
  3. 3. 小结
Fork me on GitHub