前言
數(shù)據(jù)源,實(shí)際就是數(shù)據(jù)庫(kù)連接池,負(fù)責(zé)管理數(shù)據(jù)庫(kù)連接,在Springboot中,數(shù)據(jù)源通常以一個(gè)bean的形式存在于IOC容器中,也就是我們可以通過(guò)依賴注入的方式拿到數(shù)據(jù)源,然后再?gòu)臄?shù)據(jù)源中獲取數(shù)據(jù)庫(kù)連接。
那么什么是多數(shù)據(jù)源呢,其實(shí)就是IOC容器中有多個(gè)數(shù)據(jù)源的bean,這些數(shù)據(jù)源可以是不同的數(shù)據(jù)源類型,也可以連接不同的數(shù)據(jù)庫(kù)。
本文將對(duì)多數(shù)據(jù)如何加載,如何結(jié)合MyBatis使用進(jìn)行說(shuō)明,知識(shí)點(diǎn)腦圖如下所示。
正文
一. 數(shù)據(jù)源概念和常見(jiàn)數(shù)據(jù)源介紹
數(shù)據(jù)源,其實(shí)就是數(shù)據(jù)庫(kù)連接池,負(fù)責(zé)數(shù)據(jù)庫(kù)連接的管理和借出。目前使用較多也是性能較優(yōu)的有如下幾款數(shù)據(jù)源。
- TomcatJdbc。TomcatJdbc是Apache提供的一種數(shù)據(jù)庫(kù)連接池解決方案,各方面都還行,各方面也都不突出;
- Druid。Druid是阿里開源的數(shù)據(jù)庫(kù)連接池,是阿里監(jiān)控系統(tǒng)Dragoon的副產(chǎn)品,提供了強(qiáng)大的可監(jiān)控性和基于Filter-Chain的可擴(kuò)展性;
- HikariCP。HikariCP是基于BoneCP進(jìn)行了大量改進(jìn)和優(yōu)化的數(shù)據(jù)庫(kù)連接池,是Springboot 2.x版本默認(rèn)的數(shù)據(jù)庫(kù)連接池,也是速度最快的數(shù)據(jù)庫(kù)連接池。
二. Springboot加載數(shù)據(jù)源原理分析
首先搭建一個(gè)極簡(jiǎn)的示例工程,POM文件引入依賴如下所示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>MySQL</groupId>
<artifactId>mysql-connector-JAVA</artifactId>
</dependency>
復(fù)制代碼
編寫一個(gè)Springboot的啟動(dòng)類,如下所示。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
復(fù)制代碼
再編寫一個(gè)從數(shù)據(jù)源拿連接的DAO類,如下所示。
@Repository
public class MyDao implements InitializingBean {
@Autowired
private DataSource dataSource;
@Override
public void afterPropertiesSet() throws Exception {
Connection connection = dataSource.getConnection();
System.out.println("獲取到數(shù)據(jù)庫(kù)連接:" + connection);
}
}
復(fù)制代碼
在application.yml文件中加入數(shù)據(jù)源的參數(shù)配置。
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
hikari:
max-lifetime: 1600000
keep-alive-time: 90000
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: root
復(fù)制代碼
其中url,username和password是必須配置的,其它的僅僅是為了演示。
整體的工程目錄如下。
負(fù)責(zé)完成數(shù)據(jù)源加載的類叫做
DataSourceAutoConfiguration,由spring-boot-autoconfigure包提供,
DataSourceAutoConfiguration的加載是基于Springboot的自動(dòng)裝配機(jī)制,不過(guò)這里說(shuō)明一下,由于本篇文章是基于Springboot的2.7.6版本,所以沒(méi)有辦法在spring-boot-autoconfigure包的spring.factories文件中找到
DataSourceAutoConfiguration,在Springboot的2.7.x版本中,是通過(guò)加載META-INF/spring/xxx.xxx.xxx.imports文件來(lái)實(shí)現(xiàn)自動(dòng)裝配的,但這不是本文重點(diǎn),故先在這里略做說(shuō)明。
下面先看一下
DataSourceAutoConfiguration的部分代碼實(shí)現(xiàn)。
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {
......
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
}
......
}
復(fù)制代碼
上述展示出來(lái)的代碼,做了兩件和加載數(shù)據(jù)源有關(guān)的事情。
- 將數(shù)據(jù)源的配置類DataSourceProperties注冊(cè)到了容器中;
- 將DataSourceConfiguration的靜態(tài)內(nèi)部類Hikari注冊(cè)到了容器中。
先看一下DataSourceProperties的實(shí)現(xiàn),如下所示。
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private boolean generateUniqueName = true;
private String name;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
......
}
復(fù)制代碼
DataSourceProperties中加載了配置在application.yml文件中的spring.datasource.xxx等配置,像我們配置的type,driver-class-name,url,username和password都會(huì)加載在DataSourceProperties中。
再看一下DataSourceConfiguration的靜態(tài)內(nèi)部類Hikari的實(shí)現(xiàn),如下所示。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
復(fù)制代碼
可知Hikari會(huì)向容器注冊(cè)一個(gè)HikariCP的數(shù)據(jù)源HikariDataSource,同時(shí)HikariDataSource也是一個(gè)配置類,其會(huì)加載application.yml文件中的
spring.datasource.hikari.xxx等和HikariCP相關(guān)的數(shù)據(jù)源配置,像我們配置的max-lifetime和keep-alive-time都會(huì)加載在HikariDataSource中。
然后還能發(fā)現(xiàn),創(chuàng)建HikariDataSource的createDataSource方法的第一個(gè)參數(shù)是容器中的DataSourceProperties的bean,所以在創(chuàng)建HikariDataSource時(shí),肯定是需要使用到DataSourceProperties里面保存的相關(guān)配置的,下面看一下DataSourceConfiguration的createDataSource() 方法的實(shí)現(xiàn)。
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
復(fù)制代碼
DataSourceProperties的
initializeDataSourceBuilder() 方法會(huì)返回一個(gè)DataSourceBuilder,具體實(shí)現(xiàn)如下。
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
.url(determineUrl()).username(determineUsername()).password(determinePassword());
}
復(fù)制代碼
也就是在創(chuàng)建DataSourceBuilder時(shí),會(huì)一并設(shè)置type,driverClassName,url,username和password等屬性,其中type和driverClassName不用設(shè)置也沒(méi)關(guān)系,Springboot會(huì)做自動(dòng)判斷,只需要引用了相應(yīng)的依賴即可。
那么至此,Springboot加載數(shù)據(jù)源原理已經(jīng)分析完畢,小結(jié)如下。
- 數(shù)據(jù)源的通用配置會(huì)保存在DataSourceProperties中。例如url,username和password等配置都屬于通用配置;
- HikariCP的數(shù)據(jù)源是HikariDataSource,HikariCP相關(guān)的配置會(huì)保存在HikariDataSource中。例如max-lifetime,keep-alive-time等都屬于HiakriCP相關(guān)配置;
- 通過(guò)DataSourceProperties可以創(chuàng)建DataSourceBuilder;
- 通過(guò)DataSourceBuilder可以創(chuàng)建具體的數(shù)據(jù)源。
三. Springboot加載多數(shù)據(jù)源實(shí)現(xiàn)
現(xiàn)在已知,加載數(shù)據(jù)源可以分為如下三步。
- 讀取數(shù)據(jù)源配置信息;
- 創(chuàng)建數(shù)據(jù)源的bean;
- 將數(shù)據(jù)源bean注冊(cè)到IOC容器中。
因此我們可以自定義一個(gè)配置類,在配置類中讀取若干個(gè)數(shù)據(jù)源的配置信息,然后基于這些配置信息創(chuàng)建出若干個(gè)數(shù)據(jù)源,最后將這些數(shù)據(jù)源全部注冊(cè)到IOC容器中。現(xiàn)在對(duì)加載多數(shù)據(jù)源進(jìn)行演示和說(shuō)明。
首先application.yml文件內(nèi)容如下所示。
lee:
datasource:
ds1:
max-lifetime: 1600000
keep-alive-time: 90000
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: root
pool-name: testpool-1
ds2:
max-lifetime: 1600000
keep-alive-time: 90000
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: root
pool-name: testpool-2
復(fù)制代碼
自定義的配置類如下所示。
@Configuration
public class MultiDataSourceConfig {
@Bean(name = "ds1")
@ConfigurationProperties(prefix = "lee.datasource.ds1")
public DataSource ds1DataSource() {
return new HikariDataSource();
}
@Bean(name = "ds2")
@ConfigurationProperties(prefix = "lee.datasource.ds2")
public DataSource ds2DataSource() {
return new HikariDataSource();
}
}
復(fù)制代碼
首先在配置類的ds1DataSource() 和ds2DataSource() 方法中創(chuàng)建出HikariDataSource,然后由于使用了@ConfigurationProperties注解,因此lee.datasource.ds1.xxx的配置內(nèi)容會(huì)加載到name為ds1的HikariDataSource中,lee.datasource.ds2.xxx的配置內(nèi)容會(huì)加載到name為ds2的HikariDataSource中,最后name為ds1的HikariDataSource和name為ds2的HikariDataSource都會(huì)作為bean注冊(cè)到容器中。
下面是一個(gè)簡(jiǎn)單的基于JDBC的測(cè)試?yán)印?/p>
@Repository
public class MyDao implements InitializingBean {
@Autowired
@Qualifier("ds2")
private DataSource dataSource;
@Override
public void afterPropertiesSet() throws Exception {
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
statement.executeQuery("SELECT * FROM book");
ResultSet resultSet = statement.getResultSet();
while (resultSet.next()) {
System.out.println(resultSet.getString("b_name"));
}
resultSet.close();
statement.close();
connection.close();
}
}
復(fù)制代碼
四. MyBatis整合Springboot原理分析
在分析如何將多數(shù)據(jù)源應(yīng)用于MyBatis前,需要了解一下MyBatis是如何整合到Springboot中的。在超詳細(xì)解釋MyBatis與Spring的集成原理一文中,有提到將MyBatis集成到Spring中需要提供如下的配置類。
@Configuration
@ComponentScan(value = "掃描包路徑")
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory() throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(pooledDataSource());
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("Mybatis配置文件名"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("映射接口包路徑");
return msc;
}
// 創(chuàng)建一個(gè)數(shù)據(jù)源
private PooledDataSource pooledDataSource() {
PooledDataSource dataSource = new PooledDataSource();
dataSource.setUrl("數(shù)據(jù)庫(kù)URL地址");
dataSource.setUsername("數(shù)據(jù)庫(kù)用戶名");
dataSource.setPassword("數(shù)據(jù)庫(kù)密碼");
dataSource.setDriver("數(shù)據(jù)庫(kù)連接驅(qū)動(dòng)");
return dataSource;
}
}
復(fù)制代碼
也就是MyBatis集成到Spring,需要向容器中注冊(cè)SqlSessionFactory的bean,以及MapperScannerConfigurer的bean。那么有理由相信,MyBatis整合Springboot的starter包
mybatis-spring-boot-starter應(yīng)該也是在做這個(gè)事情,下面來(lái)分析一下
mybatis-spring-boot-starter的工作原理。
首先在POM中引入
mybatis-spring-boot-starter的依賴,如下所示。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
復(fù)制代碼
mybatis-spring-boot-starter會(huì)引入
mybatis-spring-boot-autoconfigure,看一下
mybatis-spring-boot-autoconfigure的spring.factories文件,如下所示。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
復(fù)制代碼
所以負(fù)責(zé)自動(dòng)裝配MyBatis的類是MybatisAutoConfiguration,該類的部分代碼如下所示。
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
......
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// 設(shè)置數(shù)據(jù)源
factory.setDataSource(dataSource);
......
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
private BeanFactory beanFactory;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
......
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
......
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}
......
}
復(fù)制代碼
歸納一下MybatisAutoConfiguration做的事情如下所示。
- 將MyBatis相關(guān)的配置加載到MybatisProperties并注冊(cè)到容器中。實(shí)際就是將application.yml文件中配置的mybatis.xxx相關(guān)的配置加載到MybatisProperties中;
- 基于Springboot加載的數(shù)據(jù)源創(chuàng)建SqlSessionFactory并注冊(cè)到容器中。MybatisAutoConfiguration使用了@AutoConfigureAfter注解來(lái)指定MybatisAutoConfiguration要在DataSourceAutoConfiguration執(zhí)行完畢之后再執(zhí)行,所以此時(shí)容器中已經(jīng)有了Springboot加載的數(shù)據(jù)源;
- 基于SqlSessionFactory創(chuàng)建SqlSessionTemplate并注冊(cè)到容器中;
- 使用AutoConfiguredMapperScannerRegistrar向容器注冊(cè)MapperScannerConfigurer。AutoConfiguredMapperScannerRegistrar實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口,因此可以向容器注冊(cè)bean。
那么可以發(fā)現(xiàn),其實(shí)MybatisAutoConfiguration干的事情和我們自己將MyBatis集成到Spring干的事情是一樣的:1. 獲取一個(gè)數(shù)據(jù)源并基于這個(gè)數(shù)據(jù)源創(chuàng)建SqlSessionFactory的bean并注冊(cè)到容器中;2. 創(chuàng)建MapperScannerConfigurer的bean并注冊(cè)到容器中。
五. MyBatis整合Springboot多數(shù)據(jù)源實(shí)現(xiàn)
mybatis-spring-boot-starter是單數(shù)據(jù)源的實(shí)現(xiàn),本節(jié)將對(duì)MyBatis整合Springboot的多數(shù)據(jù)實(shí)現(xiàn)進(jìn)行演示和說(shuō)明。
首先需要引入相關(guān)依賴,POM文件如下所示。
<?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-parent</artifactId>
<version>2.7.6</version>
</parent>
<groupId>com.lee.learn.multidatasource</groupId>
<artifactId>learn-multidatasource</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
復(fù)制代碼
然后提供多數(shù)據(jù)源的配置,application.yml文件如下所示。
lee:
datasource:
ds1:
max-lifetime: 1600000
keep-alive-time: 90000
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: root
pool-name: testpool-1
ds2:
max-lifetime: 1600000
keep-alive-time: 90000
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: root
pool-name: testpool-2
復(fù)制代碼
現(xiàn)在先看一下基于數(shù)據(jù)源ds1的MyBatis的配置類,如下所示。
@Configuration
public class MybatisDs1Config {
@Bean(name = "ds1")
@ConfigurationProperties(prefix = "lee.datasource.ds1")
public DataSource ds1DataSource() {
// 加載lee.datasource.ds1.xxx的配置到HikariDataSource
// 然后以ds1為名字將HikariDataSource注冊(cè)到容器中
return new HikariDataSource();
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory1(@Qualifier("ds1") DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 設(shè)置數(shù)據(jù)源
sqlSessionFactoryBean.setDataSource(dataSource);
// 設(shè)置MyBatis的配置文件
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer1(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
// 設(shè)置使用的SqlSessionFactory的名字
msc.setSqlSessionFactoryBeanName("sqlSessionFactory1");
// 設(shè)置映射接口的路徑
msc.setBasePackage("com.lee.learn.multidatasource.dao.mapper1");
return msc;
}
}
復(fù)制代碼
同理,基于數(shù)據(jù)源ds2的MyBatis的配置類,如下所示。
@Configuration
public class MybatisDs2Config {
@Bean(name = "ds2")
@ConfigurationProperties(prefix = "lee.datasource.ds2")
public DataSource ds2DataSource() {
// 加載lee.datasource.ds2.xxx的配置到HikariDataSource
// 然后以ds2為名字將HikariDataSource注冊(cè)到容器中
return new HikariDataSource();
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory2(@Qualifier("ds2") DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 設(shè)置數(shù)據(jù)源
sqlSessionFactoryBean.setDataSource(dataSource);
// 設(shè)置MyBatis的配置文件
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer2(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
// 設(shè)置使用的SqlSessionFactory的名字
msc.setSqlSessionFactoryBeanName("sqlSessionFactory2");
// 設(shè)置映射接口的路徑
msc.setBasePackage("com.lee.learn.multidatasource.dao.mapper2");
return msc;
}
}
復(fù)制代碼
基于上述兩個(gè)配置類,那么最終
com.lee.learn.multidatasource.dao.mapper1路徑下的映射接口使用的數(shù)據(jù)源為ds1,
com.lee.learn.multidatasource.dao.mapper2路徑下的映射接口使用的數(shù)據(jù)源為ds2。
完整的示例工程目錄結(jié)構(gòu)如下所示。
BookMapper和BookMapper.xml如下所示。
public interface BookMapper {
List<Book> queryAllBooks();
}
復(fù)制代碼
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.multidatasource.dao.mapper1.BookMapper">
<resultMap id="bookResultMap" type="com.lee.learn.multidatasource.entity.Book">
<id column="id" property="id"/>
<result column="b_name" property="bookName"/>
<result column="b_price" property="bookPrice"/>
<result column="bs_id" property="bsId"/>
</resultMap>
<select id="queryAllBooks" resultMap="bookResultMap">
SELECT * FROM book;
</select>
</mapper>
復(fù)制代碼
StudentMapper和StudentMapper.xml如下所示。
public interface StudentMapper {
List<Student> queryAllStudents();
}
復(fù)制代碼
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.multidatasource.dao.mapper2.StudentMapper">
<resultMap id="studentResultMap" type="com.lee.learn.multidatasource.entity.Student">
<id column="id" property="id"/>
<result column="name" property="studentName"/>
<result column="level" property="studentLevel"/>
<result column="grades" property="studentGrades"/>
</resultMap>
<select id="queryAllStudents" resultMap="studentResultMap">
SELECT * FROM stu;
</select>
</mapper>
復(fù)制代碼
Book和Student如下所示。
public class Book {
private int id;
private String bookName;
private float bookPrice;
private int bsId;
// 省略getter和setter
}
public class Student {
private int id;
private String studentName;
private String studentLevel;
private int studentGrades;
// 省略getter和setter
}
復(fù)制代碼
BookService和StudentService如下所示。
@Service
public class BookService {
@Autowired
private BookMapper bookMapper;
public List<Book> queryAllBooks() {
return bookMapper.queryAllBooks();
}
}
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
public List<Student> queryAllStudents() {
return studentMapper.queryAllStudents();
}
}
復(fù)制代碼
BookController和StudentsController如下所示。
@RestController
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/test/ds1")
public List<Book> queryAllBooks() {
return bookService.queryAllBooks();
}
}
@RestController
public class StudentsController {
@Autowired
private StudentService studentService;
@GetMapping("/test/ds2")
public List<Student> queryAllStudents() {
return studentService.queryAllStudents();
}
}
復(fù)制代碼
那么測(cè)試時(shí),啟動(dòng)Springboot應(yīng)用后,如果調(diào)用接口/test/ds1,會(huì)有如下的打印字樣。
testpool-1 - Starting...
testpool-1 - Start completed.
復(fù)制代碼
說(shuō)明查詢book表時(shí)的連接是從ds1數(shù)據(jù)源中獲取的,同理調(diào)用接口/test/ds2,會(huì)有如下打印字樣。
testpool-2 - Starting...
testpool-2 - Start completed.
復(fù)制代碼
說(shuō)明查詢stu表時(shí)的連接是從ds2數(shù)據(jù)源中獲取的。
至此,MyBatis完成了整合Springboot的多數(shù)據(jù)源實(shí)現(xiàn)。
六. MyBatis整合Springboot多數(shù)據(jù)源切換
在第五節(jié)中,MyBatis整合Springboot多數(shù)據(jù)源的實(shí)現(xiàn)思路是固定讓某些映射接口使用一個(gè)數(shù)據(jù)源,另一些映射接口使用另一個(gè)數(shù)據(jù)源。本節(jié)將提供另外一種思路,通過(guò)AOP的形式來(lái)指定要使用的數(shù)據(jù)源,也就是利用切面來(lái)實(shí)現(xiàn)多數(shù)據(jù)源的切換。
整體的實(shí)現(xiàn)思路如下。
- 配置并得到多個(gè)數(shù)據(jù)源;
- 使用一個(gè)路由數(shù)據(jù)源存放多個(gè)數(shù)據(jù)源;
- 將路由數(shù)據(jù)源配置給MyBatis的SqlSessionFactory;
- 實(shí)現(xiàn)切面來(lái)攔截對(duì)MyBatis映射接口的請(qǐng)求;
- 在切面邏輯中完成數(shù)據(jù)源切換。
那么現(xiàn)在按照上述思路,來(lái)具體實(shí)現(xiàn)一下。
數(shù)據(jù)源的配置類如下所示。
@Configuration
public class DataSourceConfig {
@Bean(name = "ds1")
@ConfigurationProperties(prefix = "lee.datasource.ds1")
public DataSource ds1DataSource() {
return new HikariDataSource();
}
@Bean(name = "ds2")
@ConfigurationProperties(prefix = "lee.datasource.ds2")
public DataSource ds2DataSource() {
return new HikariDataSource();
}
@Bean(name = "mds")
public DataSource multiDataSource(@Qualifier("ds1") DataSource ds1DataSource,
@Qualifier("ds2") DataSource ds2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("ds1", ds1DataSource);
targetDataSources.put("ds2", ds2DataSource);
MultiDataSource multiDataSource = new MultiDataSource();
multiDataSource.setTargetDataSources(targetDataSources);
multiDataSource.setDefaultTargetDataSource(ds1DataSource);
return multiDataSource;
}
}
復(fù)制代碼
名字為ds1和ds2的數(shù)據(jù)源沒(méi)什么好說(shuō)的,具體關(guān)注一下名字為mds的數(shù)據(jù)源,也就是所謂的路由數(shù)據(jù)源,其實(shí)現(xiàn)如下所示。
public class MultiDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> DATA_SOURCE_NAME = new ThreadLocal<>();
public static void setDataSourceName(String dataSourceName) {
DATA_SOURCE_NAME.set(dataSourceName);
}
public static void removeDataSourceName() {
DATA_SOURCE_NAME.remove();
}
@Override
public Object determineCurrentLookupKey() {
return DATA_SOURCE_NAME.get();
}
}
復(fù)制代碼
我們自定義了一個(gè)路由數(shù)據(jù)源叫做MultiDataSource,其實(shí)現(xiàn)了AbstractRoutingDataSource類,而AbstractRoutingDataSource類正是Springboot提供的用于做數(shù)據(jù)源切換的一個(gè)抽象類,其內(nèi)部有一個(gè)Map類型的字段叫做targetDataSources,里面存放的就是需要做切換的數(shù)據(jù)源,key是數(shù)據(jù)源的名字,value是數(shù)據(jù)源。當(dāng)要從路由數(shù)據(jù)源獲取Connection時(shí),會(huì)調(diào)用到AbstractRoutingDataSource提供的getConnection() 方法,看一下其實(shí)現(xiàn)。
public Connection getConnection() throws SQLException {
return determ.NETargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 得到實(shí)際要使用的數(shù)據(jù)源的key
Object lookupKey = determineCurrentLookupKey();
// 根據(jù)key從resolvedDataSources中拿到實(shí)際要使用的數(shù)據(jù)源
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
復(fù)制代碼
其實(shí)呢從路由數(shù)據(jù)源拿到實(shí)際使用的數(shù)據(jù)源時(shí),就是首先通過(guò)determineCurrentLookupKey() 方法拿key,然后再根據(jù)key從resolvedDataSources這個(gè)Map中拿到實(shí)際使用的數(shù)據(jù)源。看到這里可能又有疑問(wèn)了,在DataSourceConfig中創(chuàng)建路由數(shù)據(jù)源的bean時(shí),明明只設(shè)置了AbstractRoutingDataSource#targetDataSources的值,并沒(méi)有設(shè)置AbstractRoutingDataSource#resolvedDataSources,那為什么resolvedDataSources中會(huì)有實(shí)際要使用的數(shù)據(jù)源呢,關(guān)于這個(gè)問(wèn)題,可以看一下AbstractRoutingDataSource的afterPropertiesSet() 方法,這里不再贅述。
那么現(xiàn)在可以知道,每次從路由數(shù)據(jù)源獲取實(shí)際要使用的數(shù)據(jù)源時(shí),關(guān)鍵的就在于如何通過(guò)determineCurrentLookupKey() 拿到數(shù)據(jù)源的key,而determineCurrentLookupKey() 是一個(gè)抽象方法,所以在我們自定義的路由數(shù)據(jù)源中對(duì)其進(jìn)行了重寫,也就是從一個(gè)ThreadLocal中拿到數(shù)據(jù)源的key,有拿就有放,那么ThreadLocal是在哪里設(shè)置的數(shù)據(jù)源的key的呢,那當(dāng)然就是在切面中啦。下面一起看一下。
首先定義一個(gè)切面,如下所示。
@Aspect
@Component
public class DeterminDataSourceAspect {
@Pointcut("@annotation(com.lee.learn.multidatasource.aspect.DeterminDataSource)")
private void determinDataSourcePointcount() {}
@Around("determinDataSourcePointcount()")
public Object determinDataSource(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
DeterminDataSource determinDataSource = methodSignature.getMethod()
.getAnnotation(DeterminDataSource.class);
MultiDataSource.setDataSourceName(determinDataSource.name());
try {
return proceedingJoinPoint.proceed();
} finally {
MultiDataSource.removeDataSourceName();
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeterminDataSource {
String name() default "ds1";
}
復(fù)制代碼
切點(diǎn)是自定義的注解@DeterminDataSource修飾的方法,這個(gè)注解可以通過(guò)name屬性來(lái)指定實(shí)際要使用的數(shù)據(jù)源的key,然后定義了一個(gè)環(huán)繞通知,做的事情就是在目標(biāo)方法執(zhí)行前將DeterminDataSource注解指定的key放到MultiDataSource的ThreadLocal中,然后執(zhí)行目標(biāo)方法,最后在目標(biāo)方法執(zhí)行完畢后,將數(shù)據(jù)源的key從MultiDataSource的ThreadLocal中再移除。
現(xiàn)在已經(jīng)有路由數(shù)據(jù)源了,也有為路由數(shù)據(jù)源設(shè)置實(shí)際使用數(shù)據(jù)源key的切面了,最后一件事情就是將路由數(shù)據(jù)源給到MyBatis的SessionFactory,配置類MybatisConfig如下所示。
@Configuration
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(@Qualifier("mds") DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer1(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setSqlSessionFactoryBeanName("sqlSessionFactory");
msc.setBasePackage("com.lee.learn.multidatasource.dao");
return msc;
}
}
復(fù)制代碼
完整的示例工程目錄結(jié)構(gòu)如下。
除了上面的代碼以外,其余代碼和第五節(jié)中一樣,這里不再重復(fù)給出。
最后在BookService和StudentService的方法中添加上@DeterminDataSource注解,來(lái)實(shí)現(xiàn)數(shù)據(jù)源切換的演示。
@Service
public class BookService {
@Autowired
private BookMapper bookMapper;
@DeterminDataSource(name = "ds1")
public List<Book> queryAllBooks() {
return bookMapper.queryAllBooks();
}
}
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
@DeterminDataSource(name = "ds2")
public List<Student> queryAllStudents() {
return studentMapper.queryAllStudents();
}
}
復(fù)制代碼
同樣,啟動(dòng)Springboot應(yīng)用后,如果調(diào)用接口/test/ds1,會(huì)有如下的打印字樣。
testpool-1 - Starting...
testpool-1 - Start completed.
復(fù)制代碼
說(shuō)明查詢book表時(shí)的連接是從ds1數(shù)據(jù)源中獲取的,同理調(diào)用接口/test/ds2,會(huì)有如下打印字樣。
testpool-2 - Starting...
testpool-2 - Start completed.
復(fù)制代碼
至此,MyBatis完成了整合Springboot的多數(shù)據(jù)源切換。
總結(jié)
本文的整體知識(shí)點(diǎn)如下所示。
首先數(shù)據(jù)源其實(shí)就是數(shù)據(jù)庫(kù)連接池,負(fù)責(zé)連接的管理和借出,目前主流的有TomcatJdbc,Druid和HikariCP。
然后Springboot官方的加載數(shù)據(jù)源實(shí)現(xiàn),實(shí)際就是基于自動(dòng)裝配機(jī)制,通過(guò)
DataSourceAutoConfiguration來(lái)加載數(shù)據(jù)源相關(guān)的配置并將數(shù)據(jù)源創(chuàng)建出來(lái)再注冊(cè)到容器中。
所以模仿Springboot官方的加載數(shù)據(jù)源實(shí)現(xiàn),我們可以自己加載多個(gè)數(shù)據(jù)源的配置,然后創(chuàng)建出不同的數(shù)據(jù)源的bean,再全部注冊(cè)到容器中,這樣我們就實(shí)現(xiàn)了加載多數(shù)據(jù)源。
加載完多數(shù)據(jù)源后該怎么使用呢。首先可以通過(guò)數(shù)據(jù)源的的名字,也就是bean的名字來(lái)依賴注入數(shù)據(jù)源,然后直接從數(shù)據(jù)源拿到Connection,這樣的方式能用,但是肯定沒(méi)人會(huì)這樣用。所以結(jié)合之前MyBatis整合Spring的知識(shí),我們可以將不同的數(shù)據(jù)源設(shè)置給不同的SqlSessionFactory,然后再將不同的SqlSessionFactory設(shè)置給不同的MapperScannerConfigurer,這樣就實(shí)現(xiàn)了某一些映射接口使用一個(gè)數(shù)據(jù)源,另一些映射接口使用另一個(gè)數(shù)據(jù)源的效果。
最后,還可以借助AbstractRoutingDataSource來(lái)實(shí)現(xiàn)數(shù)據(jù)源的切換,也就是提前將創(chuàng)建好的數(shù)據(jù)源放入路由數(shù)據(jù)源中,并且一個(gè)數(shù)據(jù)源對(duì)應(yīng)一個(gè)key,然后獲取數(shù)據(jù)源時(shí)通過(guò)key來(lái)獲取,key的設(shè)置通過(guò)一個(gè)切面來(lái)實(shí)現(xiàn),這樣的方式可以在更小的粒度來(lái)切換數(shù)據(jù)源。
現(xiàn)在最后思考一下,本文的多數(shù)據(jù)源的相關(guān)實(shí)現(xiàn),最大的問(wèn)題是什么。
我認(rèn)為有兩點(diǎn)。
- 本文的多數(shù)據(jù)源的實(shí)現(xiàn),都是我們自己提供了配置類來(lái)做整合,如果新起一個(gè)項(xiàng)目,又要重新提供一套配置類;
- 數(shù)據(jù)源的個(gè)數(shù),名字都是在整合的時(shí)候確定好了,如果加數(shù)據(jù)源,或者改名字,就得改代碼,改配置類。
所以本文的數(shù)據(jù)源的實(shí)現(xiàn)方式不夠優(yōu)雅,最好是能夠有一個(gè)starter包來(lái)完成多數(shù)據(jù)源加載這個(gè)事情,讓我們僅通過(guò)少量配置就能實(shí)現(xiàn)多數(shù)據(jù)源的動(dòng)態(tài)加載和使用。
作者:半夏之沫
鏈接:
https://juejin.cn/post/7220797267715522615