您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

MyBatis整合Springboot多数据源实现

时间:2023-04-12 13:48:56  来源:稀土掘金  作者:半夏之沫

前言

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

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

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

 

正文

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

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

  1. TomcatJdbcTomcatJdbcApache提供的一种数据库连接池解决方案,各方面都还行,各方面也都不突出;
  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的启动类,如下所示。

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

其中urlusernamepassword是必须配置的,其它的仅仅是为了演示。

整体的工程目录如下。

 

负责完成数据源加载的类叫做
DataSourceAutoConfiguration,由spring-boot-autoconfigure包提供,
DataSourceAutoConfiguration的加载是基于Springboot的自动装配机制,不过这里说明一下,由于本篇文章是基于Springboot2.7.6版本,所以没有办法在spring-boot-autoconfigure包的spring.factories文件中找到
DataSourceAutoConfiguration,在Springboot2.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-nameurlusernamepassword都会加载在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-lifetimekeep-alive-time都会加载在HikariDataSource中。

然后还能发现,创建HikariDataSourcecreateDataSource方法的第一个参数是容器中的DataSourcePropertiesbean,所以在创建HikariDataSource时,肯定是需要使用到DataSourceProperties里面保存的相关配置的,下面看一下DataSourceConfigurationcreateDataSource() 方法的实现。

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时,会一并设置typedriverClassNameurlusernamepassword等属性,其中typedriverClassName不用设置也没关系,Springboot会做自动判断,只需要引用了相应的依赖即可。

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

  1. 数据源的通用配置会保存在DataSourceProperties中。例如urlusernamepassword等配置都属于通用配置;
  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
复制代码

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

@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的配置内容会加载到nameds1HikariDataSource中,lee.datasource.ds2.xxx的配置内容会加载到nameds2HikariDataSource中,最后nameds1HikariDataSourcenameds2HikariDataSource都会作为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,需要向容器中注册SqlSessionFactorybean,以及MapperScannerConfigurerbean。那么有理由相信,MyBatis整合Springbootstarter
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-autoconfigurespring.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. 获取一个数据源并基于这个数据源创建SqlSessionFactorybean并注册到容器中;2. 创建MapperScannerConfigurerbean并注册到容器中。

五. 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
复制代码

现在先看一下基于数据源ds1MyBatis的配置类,如下所示。

@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;
    }

}
复制代码

同理,基于数据源ds2MyBatis的配置类,如下所示。

@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

完整的示例工程目录结构如下所示。

 

BookMapperBookMapper.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>
复制代码

StudentMapperStudentMapper.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>
复制代码

BookStudent如下所示。

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

}
复制代码

BookServiceStudentService如下所示。

@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();
    }

}
复制代码

BookControllerStudentsController如下所示。

@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的形式来指定要使用的数据源,也就是利用切面来实现多数据源的切换。

整体的实现思路如下。

  1. 配置并得到多个数据源;
  2. 使用一个路由数据源存放多个数据源;
  3. 将路由数据源配置给MyBatisSqlSessionFactory
  4. 实现切面来拦截对MyBatis映射接口的请求;
  5. 在切面逻辑中完成数据源切换。

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

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

@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;
    }

}
复制代码

名字为ds1ds2的数据源没什么好说的,具体关注一下名字为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 determ.NETargetDataSource().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,然后再根据keyresolvedDataSources这个Map中拿到实际使用的数据源。看到这里可能又有疑问了,在DataSourceConfig中创建路由数据源的bean时,明明只设置了AbstractRoutingDataSource#targetDataSources的值,并没有设置AbstractRoutingDataSource#resolvedDataSources,那为什么resolvedDataSources中会有实际要使用的数据源呢,关于这个问题,可以看一下AbstractRoutingDataSourceafterPropertiesSet() 方法,这里不再赘述。

那么现在可以知道,每次从路由数据源获取实际要使用的数据源时,关键的就在于如何通过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放到MultiDataSourceThreadLocal中,然后执行目标方法,最后在目标方法执行完毕后,将数据源的keyMultiDataSourceThreadLocal中再移除。

现在已经有路由数据源了,也有为路由数据源设置实际使用数据源key的切面了,最后一件事情就是将路由数据源给到MyBatisSessionFactory,配置类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;
    }

}
复制代码

完整的示例工程目录结构如下。

 

除了上面的代码以外,其余代码和第五节中一样,这里不再重复给出。

最后在BookServiceStudentService的方法中添加上@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的多数据源切换。

总结

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

 

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

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

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

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

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

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

我认为有两点。

  1. 本文的多数据源的实现,都是我们自己提供了配置类来做整合,如果新起一个项目,又要重新提供一套配置类;
  2. 数据源的个数,名字都是在整合的时候确定好了,如果加数据源,或者改名字,就得改代码,改配置类。

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


作者:半夏之沫
链接:
https://juejin.cn/post/7220797267715522615



Tags:MyBatis   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Mybatis参数映射搞不明白?来试试这个工具吧!
之前在《使用技巧-Mybatis参数映射》《使用技巧-Mybatis参数映射(2)》提到了Mybatis的一些参数映射技巧,但是平时使用的时候有些小伙伴可能不知道自己写的#{}表达式能不能获取...【详细内容】
2024-02-28  Search: MyBatis  点击:(33)  评论:(0)  加入收藏
如何在Spring项目中配置MP(MyBatis-Plus)集成?
在Spring项目中集成MP,需要进行以下配置:1. 引入依赖:在项目的pom.xml文件中添加MP相关依赖,例如:```xml<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plu...【详细内容】
2024-01-09  Search: MyBatis  点击:(87)  评论:(0)  加入收藏
Mybatis占位符#和$的区别?源码解读
本文针对笔者日常开发中对 Mybatis 占位符 #{} 和 ${} 使用时机结合源码,思考总结而来 &bull; Mybatis 版本 3.5.11 &bull; Spring boot 版本 3.0.2 &bull; mybatis-spring...【详细内容】
2023-10-27  Search: MyBatis  点击:(399)  评论:(0)  加入收藏
看完这篇文章,你也可以手写MyBatis部分源码(JDBC)
一、持久化机制持久化(persistence): 把数据保存到可调电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化...【详细内容】
2023-10-09  Search: MyBatis  点击:(332)  评论:(0)  加入收藏
Mybatis-Flex初体验
本篇文章内容主要包括: MyBatis-Flex 介绍MyBatis-Flex 是一个优雅的 MyBatis 增强框架,它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何...【详细内容】
2023-09-24  Search: MyBatis  点击:(196)  评论:(0)  加入收藏
MyBatis简单易用的背后隐藏的挑战
MyBatis,作为一款备受欢迎的持久层框架,它的简单易用以及灵活的配置吸引了无数的开发者。然而,随着项目的不断发展,规模的逐渐扩大,MyBatis的一些挑战也开始逐渐浮出水面。首先,由...【详细内容】
2023-09-15  Search: MyBatis  点击:(242)  评论:(0)  加入收藏
MyBatis缓存机制
MyBatis 的缓存机制属于本地缓存,适用于单机系统,它的作用是减少数据库的查询次数,提高系统性能。MyBaits 中包含两级本地缓存: 一级缓存:SqlSession 级别的,是 MyBatis 自带的缓...【详细内容】
2023-09-12  Search: MyBatis  点击:(229)  评论:(0)  加入收藏
对比 MyBatis 和 MyBatis-Plus 批量插入、批量更新的性能和区别
1 环境准备1.1 搭建 MyBatis-Plus 环境 创建 maven springboot 工程 导入依赖:web 启动器、jdbc、、java 连接 mysql、Lombok、druid 连接池启动器、mybatis-plus 启动器 编...【详细内容】
2023-09-08  Search: MyBatis  点击:(191)  评论:(0)  加入收藏
Spring Data JPA 和 MyBatis 谁更强?
我无法明确的告诉你JPA和MyBatis在国内哪个会更流行,我本人更喜欢JPA,但是我本人日常开发用MyBatis多。但是我的回答绝对不是在划水,而是我多年来自己的一点小小的思考。MyBati...【详细内容】
2023-08-22  Search: MyBatis  点击:(335)  评论:(0)  加入收藏
Mybatis-Plus可能会导致数据库死锁
一、场景还原1.版本信息MySQL版本:5.6.36-82.1-logMybatis-Plus的starter版本:3.3.2存储引擎:InnoDB2.死锁现象A同学在生产环境使用了Mybatis-Plus提供的com.baomidou.mybatisp...【详细内容】
2023-08-14  Search: MyBatis  点击:(171)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(12)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(16)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条