上文 跟我学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是一个模拟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是我们在需求阶段提到的三方库中的一个类,它被声明为一个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注册类,声明要注入的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的两个实例,
声明了DbTemplate的默认实现,也就是QueryService依赖的DbTemplate实例;
我们还声明了一个dynamicQueryServiceHandler的bean,它就是本次文章说明的核心,主要作用为在运行期替换具体QueryService依赖的DbTemplate实例;我们在后面会详细分析。
编写一个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方法主要做了如下几件事
到此就是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为一个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方法是核心替换逻辑,它接受三个参数
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源码解析,请拭目以待。
版权声明:
原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。