本文正在參加「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 及相關源碼學習須要的能夠看個人柯基小冊四件套哦(對,是四件套了),學習起來 ~ 奧利給】