写点什么

MyBatis 整合 Springboot 多数据源实现

  • 2023-04-12
    湖南
  • 本文字数:15497 字

    阅读完需:约 51 分钟

数据源,实际就是数据库连接池,负责管理数据库连接,在 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 中需要提供如下的配置类。

@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(); }
}
复制代码

那么测试时,启动 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 包的实现进行详细分析和说明。


作者:半夏之沫

链接:https://juejin.cn/post/7220797267715522615

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
MyBatis整合Springboot多数据源实现_Java_做梦都在改BUG_InfoQ写作社区