前言
資料源,實際就是資料庫連接配接池,負責管理資料庫連接配接,在Springboot中,資料源通常以一個bean的形式存在于IOC容器中,也就是我們可以通過依賴注入的方式拿到資料源,然後再從資料源中擷取資料庫連接配接。
那麼什麼是多資料源呢,其實就是IOC容器中有多個資料源的bean,這些資料源可以是不同的資料源類型,也可以連接配接不同的資料庫。
本文将對多資料如何加載,如何結合MyBatis使用進行說明,知識點腦圖如下所示。
正文
一. 資料源概念和常見資料源介紹
資料源,其實就是資料庫連接配接池,負責資料庫連接配接的管理和借出。目前使用較多也是性能較優的有如下幾款資料源。
- TomcatJdbc。TomcatJdbc是Apache提供的一種資料庫連接配接池解決方案,各方面都還行,各方面也都不突出;
- Druid。Druid是阿裡開源的資料庫連接配接池,是阿裡監控系統Dragoon的副産品,提供了強大的可監控性和基于Filter-Chain的可擴充性;
- HikariCP。HikariCP是基于BoneCP進行了大量改進和優化的資料庫連接配接池,是Springboot 2.x版本預設的資料庫連接配接池,也是速度最快的資料庫連接配接池。
二. Springboot加載資料源原理分析
首先搭建一個極簡的示例工程,POM檔案引入依賴如下所示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
編寫一個Springboot的啟動類,如下所示。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
再編寫一個從資料源拿連接配接的DAO類,如下所示。
@Repository
public class MyDao implements InitializingBean {
@Autowired
private DataSource dataSource;
@Override
public void afterPropertiesSet() throws Exception {
Connection connection = dataSource.getConnection();
System.out.println("擷取到資料庫連接配接:" + connection);
}
}
在application.yml檔案中加入資料源的參數配置。
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
其中url,username和password是必須配置的,其它的僅僅是為了示範。
整體的工程目錄如下。
負責完成資料源加載的類叫做DataSourceAutoConfiguration,由spring-boot-autoconfigure包提供,DataSourceAutoConfiguration的加載是基于Springboot的自動裝配機制,不過這裡說明一下,由于本篇文章是基于Springboot的2.7.6版本,是以沒有辦法在spring-boot-autoconfigure包的spring.factories檔案中找到DataSourceAutoConfiguration,在Springboot的2.7.x版本中,是通過加載META-INF/spring/xxx.xxx.xxx.imports檔案來實作自動裝配的,但這不是本文重點,故先在這裡略做說明。
下面先看一下DataSourceAutoConfiguration的部分代碼實作。
@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 {
}
......
}
上述展示出來的代碼,做了兩件和加載資料源有關的事情。
- 将資料源的配置類DataSourceProperties注冊到了容器中;
- 将DataSourceConfiguration的靜态内部類Hikari注冊到了容器中。
先看一下DataSourceProperties的實作,如下所示。
@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;
......
}
DataSourceProperties中加載了配置在application.yml檔案中的spring.datasource.xxx等配置,像我們配置的type,driver-class-name,url,username和password都會加載在DataSourceProperties中。
再看一下DataSourceConfiguration的靜态内部類Hikari的實作,如下所示。
@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;
}
}
可知Hikari會向容器注冊一個HikariCP的資料源HikariDataSource,同時HikariDataSource也是一個配置類,其會加載application.yml檔案中的spring.datasource.hikari.xxx等和HikariCP相關的資料源配置,像我們配置的max-lifetime和keep-alive-time都會加載在HikariDataSource中。
然後還能發現,建立HikariDataSource的createDataSource方法的第一個參數是容器中的DataSourceProperties的bean,是以在建立HikariDataSource時,肯定是需要使用到DataSourceProperties裡面儲存的相關配置的,下面看一下DataSourceConfiguration的createDataSource() 方法的實作。
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
複制代碼
DataSourceProperties的initializeDataSourceBuilder() 方法會傳回一個DataSourceBuilder,具體實作如下。
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
.url(determineUrl()).username(determineUsername()).password(determinePassword());
}
也就是在建立DataSourceBuilder時,會一并設定type,driverClassName,url,username和password等屬性,其中type和driverClassName不用設定也沒關系,Springboot會做自動判斷,隻需要引用了相應的依賴即可。
那麼至此,Springboot加載資料源原理已經分析完畢,小結如下。
- 資料源的通用配置會儲存在DataSourceProperties中。例如url,username和password等配置都屬于通用配置;
- HikariCP的資料源是HikariDataSource,HikariCP相關的配置會儲存在HikariDataSource中。例如max-lifetime,keep-alive-time等都屬于HiakriCP相關配置;
- 通過DataSourceProperties可以建立DataSourceBuilder;
- 通過DataSourceBuilder可以建立具體的資料源。
三. Springboot加載多資料源實作
現在已知,加載資料源可以分為如下三步。
- 讀取資料源配置資訊;
- 建立資料源的bean;
- 将資料源bean注冊到IOC容器中。
是以我們可以自定義一個配置類,在配置類中讀取若幹個資料源的配置資訊,然後基于這些配置資訊建立出若幹個資料源,最後将這些資料源全部注冊到IOC容器中。現在對加載多資料源進行示範和說明。
首先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
自定義的配置類如下所示。
@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();
}
}
首先在配置類的ds1DataSource() 和ds2DataSource() 方法中建立出HikariDataSource,然後由于使用了@ConfigurationProperties注解,是以lee.datasource.ds1.xxx的配置内容會加載到name為ds1的HikariDataSource中,lee.datasource.ds2.xxx的配置内容會加載到name為ds2的HikariDataSource中,最後name為ds1的HikariDataSource和name為ds2的HikariDataSource都會作為bean注冊到容器中。
下面是一個簡單的基于JDBC的測試例子。
@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();
}
}
四. MyBatis整合Springboot原理分析
在分析如何将多資料源應用于MyBatis前,需要了解一下MyBatis是如何整合到Springboot中的。在超詳細解釋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;
}
// 建立一個資料源
private PooledDataSource pooledDataSource() {
PooledDataSource dataSource = new PooledDataSource();
dataSource.setUrl("資料庫URL位址");
dataSource.setUsername("資料庫使用者名");
dataSource.setPassword("資料庫密碼");
dataSource.setDriver("資料庫連接配接驅動");
return dataSource;
}
}
也就是MyBatis內建到Spring,需要向容器中注冊SqlSessionFactory的bean,以及MapperScannerConfigurer的bean。那麼有理由相信,MyBatis整合Springboot的starter包mybatis-spring-boot-starter應該也是在做這個事情,下面來分析一下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>
mybatis-spring-boot-starter會引入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
是以負責自動裝配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();
// 設定資料源
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;
}
}
......
}
歸納一下MybatisAutoConfiguration做的事情如下所示。
- 将MyBatis相關的配置加載到MybatisProperties并注冊到容器中。實際就是将application.yml檔案中配置的mybatis.xxx相關的配置加載到MybatisProperties中;
- 基于Springboot加載的資料源建立SqlSessionFactory并注冊到容器中。MybatisAutoConfiguration使用了@AutoConfigureAfter注解來指定MybatisAutoConfiguration要在DataSourceAutoConfiguration執行完畢之後再執行,是以此時容器中已經有了Springboot加載的資料源;
- 基于SqlSessionFactory建立SqlSessionTemplate并注冊到容器中;
- 使用AutoConfiguredMapperScannerRegistrar向容器注冊MapperScannerConfigurer。AutoConfiguredMapperScannerRegistrar實作了ImportBeanDefinitionRegistrar接口,是以可以向容器注冊bean。
那麼可以發現,其實MybatisAutoConfiguration幹的事情和我們自己将MyBatis內建到Spring幹的事情是一樣的:1. 擷取一個資料源并基于這個資料源建立SqlSessionFactory的bean并注冊到容器中;2. 建立MapperScannerConfigurer的bean并注冊到容器中。
五. MyBatis整合Springboot多資料源實作
mybatis-spring-boot-starter是單資料源的實作,本節将對MyBatis整合Springboot的多資料實作進行示範和說明。
首先需要引入相關依賴,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>
然後提供多資料源的配置,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
現在先看一下基于資料源ds1的MyBatis的配置類,如下所示。
@Configuration
public class MybatisDs1Config {
@Bean(name = "ds1")
@ConfigurationProperties(prefix = "lee.datasource.ds1")
public DataSource ds1DataSource() {
// 加載lee.datasource.ds1.xxx的配置到HikariDataSource
// 然後以ds1為名字将HikariDataSource注冊到容器中
return new HikariDataSource();
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory1(@Qualifier("ds1") DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 設定資料源
sqlSessionFactoryBean.setDataSource(dataSource);
// 設定MyBatis的配置檔案
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer1(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
// 設定使用的SqlSessionFactory的名字
msc.setSqlSessionFactoryBeanName("sqlSessionFactory1");
// 設定映射接口的路徑
msc.setBasePackage("com.lee.learn.multidatasource.dao.mapper1");
return msc;
}
}
同理,基于資料源ds2的MyBatis的配置類,如下所示。
@Configuration
public class MybatisDs2Config {
@Bean(name = "ds2")
@ConfigurationProperties(prefix = "lee.datasource.ds2")
public DataSource ds2DataSource() {
// 加載lee.datasource.ds2.xxx的配置到HikariDataSource
// 然後以ds2為名字将HikariDataSource注冊到容器中
return new HikariDataSource();
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory2(@Qualifier("ds2") DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 設定資料源
sqlSessionFactoryBean.setDataSource(dataSource);
// 設定MyBatis的配置檔案
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer2(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
// 設定使用的SqlSessionFactory的名字
msc.setSqlSessionFactoryBeanName("sqlSessionFactory2");
// 設定映射接口的路徑
msc.setBasePackage("com.lee.learn.multidatasource.dao.mapper2");
return msc;
}
}
基于上述兩個配置類,那麼最終com.lee.learn.multidatasource.dao.mapper1路徑下的映射接口使用的資料源為ds1,com.lee.learn.multidatasource.dao.mapper2路徑下的映射接口使用的資料源為ds2。
完整的示例工程目錄結構如下所示。
BookMapper和BookMapper.xml如下所示。
public interface BookMapper {
List<Book> queryAllBooks();
}
<?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>
StudentMapper和StudentMapper.xml如下所示。
public interface StudentMapper {
List<Student> queryAllStudents();
}
<?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>
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
}
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();
}
}
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();
}
}
那麼測試時,啟動Springboot應用後,如果調用接口/test/ds1,會有如下的列印字樣。
testpool-1 - Starting...
testpool-1 - Start completed.
說明查詢book表時的連接配接是從ds1資料源中擷取的,同理調用接口/test/ds2,會有如下列印字樣。
testpool-2 - Starting...
testpool-2 - Start completed.
說明查詢stu表時的連接配接是從ds2資料源中擷取的。
至此,MyBatis完成了整合Springboot的多資料源實作。
六. MyBatis整合Springboot多資料源切換
在第五節中,MyBatis整合Springboot多資料源的實作思路是固定讓某些映射接口使用一個資料源,另一些映射接口使用另一個資料源。本節将提供另外一種思路,通過AOP的形式來指定要使用的資料源,也就是利用切面來實作多資料源的切換。
整體的實作思路如下。
- 配置并得到多個資料源;
- 使用一個路由資料源存放多個資料源;
- 将路由資料源配置給MyBatis的SqlSessionFactory;
- 實作切面來攔截對MyBatis映射接口的請求;
- 在切面邏輯中完成資料源切換。
那麼現在按照上述思路,來具體實作一下。
資料源的配置類如下所示。
@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;
}
}
名字為ds1和ds2的資料源沒什麼好說的,具體關注一下名字為mds的資料源,也就是所謂的路由資料源,其實作如下所示。
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();
}
}
我們自定義了一個路由資料源叫做MultiDataSource,其實作了AbstractRoutingDataSource類,而AbstractRoutingDataSource類正是Springboot提供的用于做資料源切換的一個抽象類,其内部有一個Map類型的字段叫做targetDataSources,裡面存放的就是需要做切換的資料源,key是資料源的名字,value是資料源。當要從路由資料源擷取Connection時,會調用到AbstractRoutingDataSource提供的getConnection() 方法,看一下其實作。
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 得到實際要使用的資料源的key
Object lookupKey = determineCurrentLookupKey();
// 根據key從resolvedDataSources中拿到實際要使用的資料源
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;
}
其實呢從路由資料源拿到實際使用的資料源時,就是首先通過determineCurrentLookupKey() 方法拿key,然後再根據key從resolvedDataSources這個Map中拿到實際使用的資料源。看到這裡可能又有疑問了,在DataSourceConfig中建立路由資料源的bean時,明明隻設定了AbstractRoutingDataSource#targetDataSources的值,并沒有設定AbstractRoutingDataSource#resolvedDataSources,那為什麼resolvedDataSources中會有實際要使用的資料源呢,關于這個問題,可以看一下AbstractRoutingDataSource的afterPropertiesSet() 方法,這裡不再贅述。
那麼現在可以知道,每次從路由資料源擷取實際要使用的資料源時,關鍵的就在于如何通過determineCurrentLookupKey() 拿到資料源的key,而determineCurrentLookupKey() 是一個抽象方法,是以在我們自定義的路由資料源中對其進行了重寫,也就是從一個ThreadLocal中拿到資料源的key,有拿就有放,那麼ThreadLocal是在哪裡設定的資料源的key的呢,那當然就是在切面中啦。下面一起看一下。
首先定義一個切面,如下所示。
@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";
}
切點是自定義的注解@DeterminDataSource修飾的方法,這個注解可以通過name屬性來指定實際要使用的資料源的key,然後定義了一個環繞通知,做的事情就是在目标方法執行前将DeterminDataSource注解指定的key放到MultiDataSource的ThreadLocal中,然後執行目标方法,最後在目标方法執行完畢後,将資料源的key從MultiDataSource的ThreadLocal中再移除。
現在已經有路由資料源了,也有為路由資料源設定實際使用資料源key的切面了,最後一件事情就是将路由資料源給到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;
}
}
完整的示例工程目錄結構如下。
除了上面的代碼以外,其餘代碼和第五節中一樣,這裡不再重複給出。
最後在BookService和StudentService的方法中添加上@DeterminDataSource注解,來實作資料源切換的示範。
@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();
}
}
同樣,啟動Springboot應用後,如果調用接口/test/ds1,會有如下的列印字樣。
testpool-1 - Starting...
testpool-1 - Start completed.
說明查詢book表時的連接配接是從ds1資料源中擷取的,同理調用接口/test/ds2,會有如下列印字樣。
testpool-2 - Starting...
testpool-2 - Start completed.
至此,MyBatis完成了整合Springboot的多資料源切換。
總結
本文的整體知識點如下所示。
首先資料源其實就是資料庫連接配接池,負責連接配接的管理和借出,目前主流的有TomcatJdbc,Druid和HikariCP。
然後Springboot官方的加載資料源實作,實際就是基于自動裝配機制,通過DataSourceAutoConfiguration來加載資料源相關的配置并将資料源建立出來再注冊到容器中。
是以模仿Springboot官方的加載資料源實作,我們可以自己加載多個資料源的配置,然後建立出不同的資料源的bean,再全部注冊到容器中,這樣我們就實作了加載多資料源。
加載完多資料源後該怎麼使用呢。首先可以通過資料源的的名字,也就是bean的名字來依賴注入資料源,然後直接從資料源拿到Connection,這樣的方式能用,但是肯定沒人會這樣用。是以結合之前MyBatis整合Spring的知識,我們可以将不同的資料源設定給不同的SqlSessionFactory,然後再将不同的SqlSessionFactory設定給不同的MapperScannerConfigurer,這樣就實作了某一些映射接口使用一個資料源,另一些映射接口使用另一個資料源的效果。
最後,還可以借助AbstractRoutingDataSource來實作資料源的切換,也就是提前将建立好的資料源放入路由資料源中,并且一個資料源對應一個key,然後擷取資料源時通過key來擷取,key的設定通過一個切面來實作,這樣的方式可以在更小的粒度來切換資料源。
現在最後思考一下,本文的多資料源的相關實作,最大的問題是什麼。
我認為有兩點。
- 本文的多資料源的實作,都是我們自己提供了配置類來做整合,如果新起一個項目,又要重新提供一套配置類;
- 資料源的個數,名字都是在整合的時候确定好了,如果加資料源,或者改名字,就得改代碼,改配置類。
是以本文的資料源的實作方式不夠優雅,最好是能夠有一個starter包來完成多資料源加載這個事情,讓我們僅通過少量配置就能實作多資料源的動态加載和使用。