跟我学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方法主要做了如下几件事
- 定义并初始化了AnnotationConfigApplicationContext,通过构造方法注入BeanConfig配置类,用于加载并初始化我们声明的bean;同时返回ApplicationContext上下文
- 从ApplicationContext中根据beanName获取DynamicQueryServiceHandler实例
- 此时容器初始化完成,如果bean实现了InitializingBean接口,在容器加载过程中,会以此回调afterPropertiesSet()方法,有日志则打印日志
- 由于QueryService实现了InitializingBean接口,因此我们能在控制台看到QueryService打印出初始化日志
- 我们接着构造了两个具体的DbTemplate对象,类比到实际开发中,就是我们根据具体数据源的配置,创建出对应的数据源,并初始化对应的JdbcTemplate对象
- 接着调用 dynamicQueryServiceHandler.changeDbTemplate方法,传入要替换DBTemplate的具体QueryService实例的beanName,以及我们创建的DBTemplate实例引用,以及对应的beanName(根据业务灵活指定即可,不要同名);dynamicQueryServiceHandler.changeDbTemplate方法会将替换好的QueryService实例重新注册到Spring容器上下文中
- 替换完成之后,我们重新获取一下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源码解析,请拭目以待。
版权声明:
原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。