写点什么

MyBatis 整合 Springboot 多数据源实现

作者:Java你猿哥
  • 2023-04-12
    湖南
  • 本文字数:15795 字

    阅读完需:约 52 分钟

MyBatis整合Springboot多数据源实现

前言

数据源,实际就是数据库连接池,负责管理数据库连接,在 Springboot 中,数据源通常以一个 bean 的形式存在于 IOC 容器中,也就是我们可以通过依赖注入的方式拿到数据源,然后再从数据源中获取数据库连接。

那么什么是多数据源呢,其实就是 IOC 容器中有多个数据源的 bean,这些数据源可以是不同的数据源类型,也可以连接不同的数据库。

本文将对多数据如何加载,如何结合 MyBatis 使用进行说明,知识点脑图如下所示。


正文

一. 数据源概念和常见数据源介绍

数据源,其实就是数据库连接池,负责数据库连接的管理和借出。目前使用较多也是性能较优的有如下几款数据源。

  1. TomcatJdbcTomcatJdbc Apache 提供的一种数据库连接池解决方案,各方面都还行,各方面也都不突出;

  2. DruidDruid 是阿里开源的数据库连接池,是阿里监控系统 Dragoon 的副产品,提供了强大的可监控性和基于 Filter-Chain 的可扩展性;

  3. HikariCPHikariCP 是基于 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 的启动类,如下所示。

@SpringBootApplicationpublic class Application {
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}
复制代码

再编写一个从数据源拿连接的 DAO 类,如下所示。

@Repositorypublic 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
复制代码

其中 urlusername 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 {
}
......
}
复制代码

上述展示出来的代码,做了两件和加载数据源有关的事情。

  1. 将数据源的配置类 DataSourceProperties 注册到了容器中;

  2. 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 等配置,像我们配置的 typedriver-class-nameurlusername 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 时,会一并设置 typedriverClassNameurlusername password 等属性,其中 type driverClassName 不用设置也没关系,Springboot 会做自动判断,只需要引用了相应的依赖即可。

那么至此,Springboot 加载数据源原理已经分析完毕,小结如下。

  1. 数据源的通用配置会保存在 DataSourceProperties 中。例如 urlusername password 等配置都属于通用配置;

  2. HikariCP 的数据源是 HikariDataSourceHikariCP 相关的配置会保存在 HikariDataSource 中。例如 max-lifetimekeep-alive-time 等都属于 HiakriCP 相关配置;

  3. 通过 DataSourceProperties 可以创建 DataSourceBuilder

  4. 通过 DataSourceBuilder 可以创建具体的数据源。

三. Springboot 加载多数据源实现

现在已知,加载数据源可以分为如下三步。

  1. 读取数据源配置信息;

  2. 创建数据源的 bean

  3. 将数据源 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
复制代码

自定义的配置类如下所示。

@Configurationpublic 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 的测试例子。

@Repositorypublic 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 做的事情如下所示。

  1. MyBatis 相关的配置加载到 MybatisProperties 并注册到容器中。实际就是将 application.yml 文件中配置的 mybatis.xxx 相关的配置加载到 MybatisProperties 中;

  2. 基于 Springboot 加载的数据源创建 SqlSessionFactory 并注册到容器中。MybatisAutoConfiguration 使用了 @AutoConfigureAfter 注解来指定 MybatisAutoConfiguration 要在 DataSourceAutoConfiguration 执行完毕之后再执行,所以此时容器中已经有了 Springboot 加载的数据源;

  3. 基于 SqlSessionFactory 创建 SqlSessionTemplate 并注册到容器中;

  4. 使用 AutoConfiguredMapperScannerRegistrar 向容器注册 MapperScannerConfigurerAutoConfiguredMapperScannerRegistrar 实现了 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 的配置类,如下所示。

@Configurationpublic 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 的配置类,如下所示。

@Configurationpublic 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 路径下的映射接口使用的数据源为 ds1com.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 如下所示。

@Servicepublic class BookService {
@Autowired private BookMapper bookMapper;
public List<Book> queryAllBooks() { return bookMapper.queryAllBooks(); }
}
@Servicepublic class StudentService {
@Autowired private StudentMapper studentMapper;
public List<Student> queryAllStudents() { return studentMapper.queryAllStudents(); }
}
复制代码

BookController StudentsController 如下所示。

@RestControllerpublic class BookController {
@Autowired private BookService bookService;
@GetMapping("/test/ds1") public List<Book> queryAllBooks() { return bookService.queryAllBooks(); }
}
@RestControllerpublic 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 的形式来指定要使用的数据源,也就是利用切面来实现多数据源的切换。

整体的实现思路如下。

  1. 配置并得到多个数据源;

  2. 使用一个路由数据源存放多个数据源;

  3. 将路由数据源配置给 MyBatis SqlSessionFactory

  4. 实现切面来拦截对 MyBatis 映射接口的请求;

  5. 在切面逻辑中完成数据源切换。

那么现在按照上述思路,来具体实现一下。

数据源的配置类如下所示。

@Configurationpublic 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@Componentpublic 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)@Documentedpublic @interface DeterminDataSource {
String name() default "ds1";
}
复制代码

切点是自定义的注解 @DeterminDataSource 修饰的方法,这个注解可以通过 name 属性来指定实际要使用的数据源的 key,然后定义了一个环绕通知,做的事情就是在目标方法执行前将 DeterminDataSource 注解指定的 key 放到 MultiDataSource ThreadLocal 中,然后执行目标方法,最后在目标方法执行完毕后,将数据源的 key MultiDataSource ThreadLocal 中再移除。

现在已经有路由数据源了,也有为路由数据源设置实际使用数据源 key 的切面了,最后一件事情就是将路由数据源给到 MyBatis SessionFactory,配置类 MybatisConfig 如下所示。

@Configurationpublic 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 注解,来实现数据源切换的演示。

@Servicepublic class BookService {
@Autowired private BookMapper bookMapper;
@DeterminDataSource(name = "ds1") public List<Book> queryAllBooks() { return bookMapper.queryAllBooks(); }
}
@Servicepublic 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 的多数据源切换。

总结

本文的整体知识点如下所示。


首先数据源其实就是数据库连接池,负责连接的管理和借出,目前主流的有 TomcatJdbcDruid HikariCP

然后 Springboot 官方的加载数据源实现,实际就是基于自动装配机制,通过 DataSourceAutoConfiguration 来加载数据源相关的配置并将数据源创建出来再注册到容器中。

所以模仿 Springboot 官方的加载数据源实现,我们可以自己加载多个数据源的配置,然后创建出不同的数据源的 bean,再全部注册到容器中,这样我们就实现了加载多数据源。

加载完多数据源后该怎么使用呢。首先可以通过数据源的的名字,也就是 bean 的名字来依赖注入数据源,然后直接从数据源拿到 Connection,这样的方式能用,但是肯定没人会这样用。所以结合之前 MyBatis 整合 Spring 的知识,我们可以将不同的数据源设置给不同的 SqlSessionFactory,然后再将不同的 SqlSessionFactory 设置给不同的 MapperScannerConfigurer,这样就实现了某一些映射接口使用一个数据源,另一些映射接口使用另一个数据源的效果。

最后,还可以借助 AbstractRoutingDataSource 来实现数据源的切换,也就是提前将创建好的数据源放入路由数据源中,并且一个数据源对应一个 key,然后获取数据源时通过 key 来获取,key 的设置通过一个切面来实现,这样的方式可以在更小的粒度来切换数据源。

现在最后思考一下,本文的多数据源的相关实现,最大的问题是什么。

我认为有两点。

  1. 本文的多数据源的实现,都是我们自己提供了配置类来做整合,如果新起一个项目,又要重新提供一套配置类;

  2. 数据源的个数,名字都是在整合的时候确定好了,如果加数据源,或者改名字,就得改代码,改配置类。

所以本文的数据源的实现方式不够优雅,最好是能够有一个 starter 包来完成多数据源加载这个事情,让我们仅通过少量配置就能实现多数据源的动态加载和使用。

那么在下一篇文章中,将对 Springboot 加载多数据源的 starter 包的实现进行详细分析和说明。

如果觉得本篇文章对你有帮助,求求你点个赞,加个收藏最后再点个关注吧。创作不易,感谢支持!

用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
MyBatis整合Springboot多数据源实现_spring_Java你猿哥_InfoQ写作社区