本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看掘力计划 Java 主题月 - Java Debug笔记java
2021 年已通过去 1/3 了,阿熊怎么还不写文章呢?mysql
哎,不是不写,是一直在更新着新的 MyBatis 小册嘛,正好这段时间处在换工做的阶段,白天除了找找招聘岗位就是写写小册和文章,相对也悠闲一点(可是快要饿死了呀 ~ )。最近在翻底稿的时候找到了一个以前跟小册交流群的群友讨论的话题,感受这个主题还不错,因此本篇文章,咱们就来研究一下本文标题所述的这个话题:SpringFramework 如何在运行期动态注册新的数据源?git
这个需求的起源是来自一个 SpringBoot 自动装配的数据源注册,由于一个项目中须要注册的数据源不肯定,因此须要在启动时根据配置文件的内容动态注册多个数据源。后来聊着聊着,就演变成运行时动态注册新的数据源了。虽然看上去这两个事情好像差很少,但实际上两件事差了不少哈。github
前者的处理方式相对比较简单,经过声明一个标注了 @ConfigurationProperties
的类,并用 Map 接收数据源的参数就能够把数据源的定义信息都获取到了:web
@ConfigurationProperties(prefix = "spring.datasource.dynamic")
public class DynamicDataSourceProperties {
private Map<String, DataSource> dataSourceMap = new HashMap<>();
// ......
}
复制代码
而后,再编写一个 ImportBeanDefinitionRegistrar
,读取这个 DynamicDataSourceProperties
的内容,就能够把这些数据源都注册到 IOC 容器中。spring
可是后者就麻烦了,运行期动态注册新的数据源应该如何实现才行呢?下面咱们来经过几个方案,讲解该需求的实现。sql
首先,咱们先来搭建一下编码环境。数据库
首先,咱们先来建立 3 个不一样的数据库(固然也能够只建立一个数据库,这里咱们搞的更真实一点吧):apache
CREATE DATABASE db1;
CREATE DATABASE db2;
CREATE DATABASE db3;
复制代码
接下来给每个数据库中都初始化一张相同的表:编程
CREATE TABLE tbl_user (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(32) NOT NULL,
tel varchar(16) NULL,
PRIMARY KEY (id)
);
复制代码
OK 就这么简单的准备一下就能够了。
为了快速编码,咱们仍然采用 SpringBoot 构建项目,直接使用 SpringInitializer 就挺好,固然也能够经过 Maven 构建项目,这里咱们就省去那些麻烦的构建步骤了,只把代码贴一下哈。
项目名称:dynamic-register-datasource
。
pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.8.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.linkedbear.spring</groupId>
<artifactId>dynamic-register-datasource</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
复制代码
application.yml
:
spring:
datasource:
db1:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8 # 注意这里是jdbc-url而不是url
username: root
password: 123456
db2:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8
username: root
password: 123456
复制代码
DataSourceConfiguration
:
@Configuration
public class DataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.db1")
public DataSource db1() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.db2")
public DataSource db2() {
return DataSourceBuilder.create().build();
}
}
复制代码
以上的代码,是咱们最多见到的 SpringBoot 中定义多个数据源的方法了是吧。
最后编写 SpringBoot 主启动类,在这里咱们将启动完成后的 IOC 容器拿到,并从中取出全部的 DataSource
,取一下它们其中的数据库链接 Connection
:
@SpringBootApplication
public class DynamicRegisterDataSourceApplication {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext ctx = SpringApplication.run(DynamicRegisterDataSourceApplication.class, args);
Map<String, DataSource> dataSourceMap = ctx.getBeansOfType(DataSource.class);
for (Map.Entry<String, DataSource> entry : dataSourceMap.entrySet()) {
String name = entry.getKey();
DataSource dataSource = entry.getValue();
System.out.println(name);
System.out.println(dataSource.getConnection()); // 这里会抛出异常,直接throws走了
}
}
}
复制代码
运行主启动类,能够在控制台中发现咱们已经注册好的两个 DataSource
,以及它们对应的 Connection
:
db1
2021-01-15 20:43:14.299 INFO 7624 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-01-15 20:43:14.412 INFO 7624 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
HikariProxyConnection@65982709 wrapping com.mysql.jdbc.JDBC4Connection@64030b91
db2
2021-01-15 20:43:14.414 INFO 7624 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Starting...
2021-01-15 20:43:14.418 INFO 7624 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Start completed.
HikariProxyConnection@652007616 wrapping com.mysql.jdbc.JDBC4Connection@66e889df
复制代码
到这里,基本的环境和代码都就准备好了。
下面,咱们来说解两种程序运行期动态注册数据源的解决方案。
若是各位小伙伴有学习过我 Spring 小册的 IOC 高级部分,应该都知道 bean 的建立来源是 BeanDefinition
吧!一般状况下,咱们经过 <bean>
标签、@Bean
注解,或者 @Component
配合 @ComponentScan
注解完成的 bean 注册,都是先封装为一个个的 BeanDefinition
,而后才是根据 BeanDefinition
建立 bean 对象!
使用 SpringFramework 的 BeanDefinition
元编程,咱们能够手动构造一个 BeanDefinition
,并注册到 DefaultListableBeanFactory
( BeanDefinitionRegistry
)中:
@RestController
public class RegisterDataSourceController implements BeanFactoryAware, ApplicationContextAware {
private DefaultListableBeanFactory beanFactory;
@GetMapping("/register1")
public String register1() {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HikariDataSource.class);
builder.addPropertyReference("driverClassName", "com.mysql.jdbc.Driver");
builder.addPropertyReference("jdbcUrl", "jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
builder.addPropertyReference("username", "root");
builder.addPropertyReference("password", "123456");
// builder.setScope(ConfigurableListableBeanFactory.SCOPE_SINGLETON);
beanFactory.registerBeanDefinition("db3", builder.getBeanDefinition());
return "success";
}
@GetMapping("/getDataSources")
public String getDataSources() {
Map<String, DataSource> dataSourceMap = beanFactory.getBeansOfType(DataSource.class);
dataSourceMap.forEach((s, dataSource) -> {
System.out.println(s + " ======== " + dataSource);
});
return "success";
}
// ......
}
复制代码
若是构造的 DataSource
须要指定做用域等额外的配置,能够操纵 BeanDefinitionBuilder
的 API 进行设置。
以此法编写好以后,咱们能够重启项目测试一下。重启以后先访问 /getDataSources
,能够发现控制台只有两个 DataSource
的打印:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
复制代码
而后访问 /register1
路径,以后再访问 /getDataSources
,控制台就能够打印三个 DataSource
了:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 ======== HikariDataSource (HikariPool-3)
复制代码
这种方法比较简单,比较具备通用性,关键的点是抓住核心知识点:BeanFactory
中的 bean 绝大多数都是经过 BeanDefinition
建立而来。
若是须要注册的 bean 都是单实例 bean ,并且不须要通过 AOP 处理的话,则也可使用接下来要讲的这种方式,相较于上一种而言,采用这种方法相对会更友好。
若是小伙伴有看过个人 Spring 小册第 14 章 BeanFactory
章节,应该不会忘记 BeanFactory
在 ApplicationContext
中惟一现役的最终实现是 DefaultListableBeanFactory
吧。那这个实现类,最终是继承了 AbstractBeanFactory
,而它又继承了一个叫 DefaultSingletonBeanRegistry
的类,这个类咱们在 Spring 小册的正篇中没有说起,现已经补充到小册的加餐内容中了,小伙伴们能够戳连接去学习呀。
咱们简单的来讲哈,DefaultSingletonBeanRegistry
这个类实现了一个 SingletonBeanRegistry
接口,这个接口中定义了一个方法:registerSingleton
,它能够直接向 IOC 容器注册一个已经完彻底全存在的对象,使其成为 IOC 容器中的一个 bean 。
void registerSingleton(String beanName, Object singletonObject);
复制代码
又由于 DefaultListableBeanFactory
继承自 DefaultSingletonBeanRegistry
,因此借助这个原理以后,实现这个需求就简单的很了。咱们只须要拿到 DefaultListableBeanFactory
,以后调用它的 registerSingleton
方法便可:
@RestController
public class RegisterDataSourceController implements BeanFactoryAware {
private DefaultListableBeanFactory beanFactory;
@GetMapping("/getDataSources")
public String getDataSources() {
Map<String, DataSource> dataSourceMap = beanFactory.getBeansOfType(DataSource.class);
dataSourceMap.forEach((s, dataSource) -> {
System.out.println(s + " ======== " + dataSource);
});
return "success";
}
@GetMapping("/register2")
public String register2() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.getConnection();
System.out.println("db3 建立完成!");
beanFactory.registerSingleton("db3", dataSource);
return "success";
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (DefaultListableBeanFactory) beanFactory;
}
}
复制代码
这样注册好了,IOC 容器中就有这个 db3 的数据源了,咱们能够再测试一下。
重启工程,先访问 /getDataSources
,控制台依然是只有两个 DataSource
的打印:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
复制代码
而后访问 /register2
路径,控制台能够打印成功 db3 建立完成!
,此时再访问 /getDataSources
路径,控制台也能够打印三个 DataSource
了:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 ======== HikariDataSource (HikariPool-3)
复制代码
上面咱们看到的生效,那仅仅是咱们拿到 BeanFactory
,或者 ApplicationContext
后主动调用 getBean
系列方法,去获取 IOC 容器的 bean 。但对于那些依赖了 DataSource
的 bean ,这种状况就很差办了:由于依赖注入的时机是 bean 的初始化阶段,当 bean 建立完成后,没有其余代码的干涉,bean 依赖的那些 bean 就不会变化。
听起来有点绕,咱们来写一个 Service 类来解释一下。
@Service
public class DataSourceService {
@Autowired
Map<String, DataSource> dataSourceMap;
public void printDataSources() {
dataSourceMap.forEach((s, dataSource) -> {
System.out.println(s + " ======== " + dataSource);
});
}
}
复制代码
这里咱们造了一个 DataSourceService
,并经过注入一整个 Map
的方式,将 IOC 容器中的 DataSource
连带着 bean 的 name 都注入进来。
而后咱们修改一下 Controller ,让它取容器中的 DataSourceService
,打印它里面的 DataSource
:
@GetMapping("/getDataSources")
public String getDataSources() {
DataSourceService dataSourceService = beanFactory.getBean(DataSourceService.class);
dataSourceService.printDataSources();
return "success";
}
复制代码
重启工程,并重复上面的测试效果,此次发现两次打印的结果是同样的:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 建立完成!
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
复制代码
这个现象就是上面提到的:bean 中依赖注入的属性没有被主动干预,则不会发生变化。
怎么解决这个问题呢?哎,仍是靠 BeanFactory
。
在 Spring 小册第 14 章 BeanFactory
的 1.4.2 节中咱们讲到了有关 AutowireCapableBeanFactory
的一个做用是框架集成,它提供了一个 autowireBean
方法,用于给现有的对象进行依赖注入:
void autowireBean(Object existingBean) throws BeansException;
复制代码
因此咱们能够借助这个特性,在动态注册完 DataSource
后,把 IOC 容器中的 DataSourceService
取出来,让它从新执行一次依赖注入便可:
@GetMapping("/register2")
public String register2() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.getConnection();
System.out.println("db3 建立完成!");
beanFactory.registerSingleton("db3", dataSource);
// 从新执行依赖注入
beanFactory.autowireBean(beanFactory.getBean(DataSourceService.class));
return "success";
}
复制代码
就这么简单,添加这样一行代码便可。
OK ,从新测试一下效果怎样,重启工程,按照上面的测试过程,先访问 /getDataSources
,再访问 /register2
,而后从新访问 /getDataSources
,此次控制台打印了 DataSourceService
中的 3 个 DataSource
了:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 建立完成!
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 ======== HikariDataSource (HikariPool-3)
复制代码
这样依赖注入的问题也就解决了。
虽然上面这样的写法没啥问题,但若是依赖 DataSource
的 bean 太多,那咱们一个一个的从新依赖注入,那岂不是太费劲了?有没有更好的方案,能针对某一种特定的 bean 的类型,当 BeanFactory
动态注册该类型的 bean 时,自动刷新 IOC 容器中依赖了该类型 bean 的 bean 。这个想法是否能实现呢?
比较惋惜,使用普通的套路咱们没法比较容易的获取到 IOC 容器中哪些 bean 依赖这些 DataSource
,因此咱们能够换一个思路:既然依赖这些 DataSource
的 bean 一般都是咱们本身编写的(咱们本身的业务场景须要呀),因此咱们彻底能够给这些 bean 上面添加一个自定义的注解。
譬如说,咱们给上面的代码中,DataSourceService
的上面添加一个 @RefreshDependency
注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RefreshDependency {
}
复制代码
@Service
@RefreshDependency
public class DataSourceService {
@Autowired
Map<String, DataSource> dataSourceMap;
// ......
}
复制代码
这个注解的做用,就是标识那些须要 BeanFactory
去执行依赖重注入动做的 bean 。
接下来,就是每次动态注册完 bean 后,让 BeanFactory
去寻找这些标有 @RefreshDependency
注解的 bean ,并执行依赖重注入:
@GetMapping("/register3")
public String register3() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.getConnection();
System.out.println("db3 建立完成!");
beanFactory.registerSingleton("db3", dataSource);
Map<String, Object> beansMap = beanFactory.getBeansWithAnnotation(RefreshDependency.class);
beansMap.values().forEach(bean -> beanFactory.autowireBean(bean));
return "success";
}
复制代码
固然,这两行代码虽然不长,但它毕竟是一个能够抽取的逻辑。若是后续咱们的代码中还有别的地方也须要动态注册新的 bean 后通知其它 bean 完成依赖重注入,则相同的代码又要再写一次。
针对这个问题,咱们能够继续使用事件驱动的特性来优化。
既然要用事件驱动,而咱们又知道 ApplicationContext
自己也是一个 ApplicationEventPublisher
,它具有发布事件的能力,因此咱们此次就没必要在 Controller 中注入 BeanFactory
了,而是换用 ApplicationContext
:
@RestController
public class RegisterDataSourceController implements BeanFactoryAware, ApplicationContextAware {
private ConfigurableApplicationContext ctx;
// ......
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = (ConfigurableApplicationContext) ctx;
}
}
复制代码
注意这里要用
ConfigurableApplicationContext
去接收,由于ApplicationContext
接口并无继承SingletonBeanRegistry
接口,ConfigurableApplicationContext
才继承了它。
而后,在注册完 bean 以后,就能够发布一个事件,经过事件机制来触发 bean 的依赖重注入了。咱们先来把事件和监听器造出来:
// 继承自ApplicationContextEvent,则能够直接从事件中获取ApplicationContext
public class DynamicRegisterEvent extends ApplicationContextEvent {
public DynamicRegisterEvent(ApplicationContext source) {
super(source);
}
}
复制代码
@Component
public class DynamicRegisterListener implements ApplicationListener<DynamicRegisterEvent> {
@Override
public void onApplicationEvent(DynamicRegisterEvent event) {
ApplicationContext ctx = event.getApplicationContext();
AutowireCapableBeanFactory beanFactory = ctx.getAutowireCapableBeanFactory();
Map<String, Object> beansMap = ctx.getBeansWithAnnotation(RefreshDependency.class);
beansMap.values().forEach(beanFactory::autowireBean);
}
}
复制代码
OK ,把监听器注册到 IOC 容器周,接下来再修改 Controller 中的动态注册 bean 的逻辑,让它注册完 bean 后发布 DynamicRegisterEvent
事件:
@GetMapping("/register3")
public String register3() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.getConnection();
System.out.println("db3 建立完成!");
ctx.getBeanFactory().registerSingleton("db3", dataSource);
ctx.publishEvent(new DynamicRegisterEvent(ctx));
return "success";
}
复制代码
这样一切就大功告成了,注册 bean 的逻辑,和依赖重注入的逻辑也都经过事件驱动解耦了。
从新测试一下,浏览器前后访问 /getDataSources
、/register3
、/getDataSources
,控制台依然能够打印 DataSourceService
中的 3 个 DataSource
:
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 建立完成!
db1 ======== HikariDataSource (HikariPool-1)
db2 ======== HikariDataSource (HikariPool-2)
db3 ======== HikariDataSource (HikariPool-3)
复制代码
说明咱们的优化方案是没有问题的。
本文涉及到的全部源码能够从 GitHub 中找到:github.com/LinkedBear/…
【都看到这里了,小伙伴们要不要关注点赞一下呀,有 Spring 、SpringBoot 、MyBatis 及相关源码学习须要的能够看个人柯基小册四件套哦(对,是四件套了),学习起来 ~ 奥利给】