MyBatis整合Springboot多数据源实现

news2024/11/24 9:58:36

前言

数据源,实际就是数据库连接池,负责管理数据库连接,在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();
}
复制代码

DataSourcePropertiesinitializeDataSourceBuilder() 方法会返回一个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整合Springbootstartermybatis-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路径下的映射接口使用的数据源为ds1com.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 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,然后再根据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包来完成多数据源加载这个事情,让我们仅通过少量配置就能实现多数据源的动态加载和使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422153.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

easyrecovery2023电脑文件数据恢复软件功能介绍

EasyRecovery功能全面&#xff0c;即便是没有经验的小白用户也可以很快上手&#xff0c;让你足不出户即可搞定常见的数据丢失问题。 在使用和操作存储设备期间&#xff0c;数据丢失问题在所难免。比如&#xff0c;误删除某个文件、不小心将有数据的分区格式化、误清空了有重要…

【ZUUL2踩坑】题一:Ribbon集成动态properties存在的原生风险

目录 一、问题背景 二、问题分析 1、配置文件空档期的问题 一、问题背景 JAVA的Properties工具有两种写配置文件的方式&#xff0c;一种是覆盖&#xff0c;一种是追加。 但是动态配置文件一般需要进行创建或更新&#xff0c;不会选择追加内容&#xff0c;所以只能选择进行配…

你的 Kubernetes 安全吗?最新benchmark的重要趋势解读

导语 疫情过后经济处在缓慢复苏的阶段&#xff0c;对于企业应该优先考虑数字化转型&#xff0c;因为它可以促进增长和创新。 不可避免地&#xff0c;当今的数字化转型计划依赖于云的可扩展性和灵活性。 虽然在云中启动应用程序和服务带来了许多机遇&#xff0c;但也带来了新的…

函数栈帧的创建与销毁

魔王的介绍&#xff1a;&#x1f636;‍&#x1f32b;️一名双非本科大一小白。魔王的目标&#xff1a;&#x1f92f;努力赶上周围卷王的脚步。魔王的主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王.&#x1f525;&#x1f525;&#x1f525; ❤️‍&#x1…

【数据结构与算法】基于回溯算法实现八皇后问题

八皇后问题是一个经典的计算机科学问题&#xff0c;它的目标是将8个皇后放置在一个大小为88的棋盘上&#xff0c;使得每个皇后都不会攻击到其他的皇后。皇后可以攻击同一行、同一列和同一对角线上的棋子。 一、八皇后问题介绍 八皇后问题最早由国际西洋棋大师马克斯贝瑟尔在18…

Pandas入门实践3 -数据可视化

人类大脑擅长于在数据的视觉表现中寻找模式;因此在这一节中&#xff0c;我们将学习如何使用pandas沿着Matplotlib和Seaborn库来可视化数据&#xff0c;以获得更多的特性。我们将创建各种可视化&#xff0c;帮助我们更好地理解数据。 使用pandas绘图 我们可以使用plot()方法创…

网络安全之防病毒网关

目录 网络安全之防病毒网关 恶意软件 按照传播方式分类 病毒 蠕虫 木马 按照功能分类 后门 勒索 挖矿 恶意代码的特征 下载特征 后门特征 信息收集特征 自身感染特性 文件感染特性 网络攻击特性 病毒威胁场景 病毒传播途径 电子信息 网络共享 P2P 系统漏洞 广…

电压有效值电容和电感的电压电流相位关系以及电抗和容抗值推导

注意下面所有www表示的都是角速度而不是频率 电压有效值 高中物理中知道有效值电压是根据电阻发热的功率等效得到的 对于正弦波的电压&#xff0c;UUmsinwtUU_{m}sinwtUUm​sinwt,对应的电流IUmRsinwtI\frac{U_{m}}{R}sinwtIRUm​​sinwt 求得一个周期的发热量 ∫0TI2Rdt∫0T…

5分钟告诉你如何成为一名黑客?从萌新成为大佬,只需掌握这5点(思维、编程语言、网络安全、入侵实操、法律)

说到黑客&#xff0c;大家脑海里是不是都已经显现了他的模样 仅用一台电脑 就能黑手机 黑银行卡、 黑摄像头、 让 ATM 疯狂吐钞&#xff0c; 真的是太酷了… 试问谁还能没有个黑客梦呢&#xff1f; 本篇文章&#xff0c;小编就是要带大家揭秘黑客的神秘面纱&#xff0c;…

【Cisco Packet Tracer| 二.telnet方式远程登录交换机】

文章目录一.PC0通过console线连接交换机二.PC1通过Telnet远程登录交换机1.PC1通过双绞线连接交换机2.给主机设置IP地址3.给交换机配置一个虚拟的管理接口4.全局模式下设置交换机进入特权模式的密码5.设置5个虚拟终端用户6.测试6.1测试主机和交换机是否在同一个网段中6.2主机远程…

基于Tensorflow搭建卷积神经网络CNN(花卉识别)保姆及级教程

项目介绍 TensorFlow2.X 搭建卷积神经网络&#xff08;CNN&#xff09;&#xff0c;实现人脸识别&#xff08;可以识别自己的人脸哦&#xff01;&#xff09;。搭建的卷积神经网络是类似VGG的结构(卷积层与池化层反复堆叠&#xff0c;然后经过全连接层&#xff0c;最后用softm…

Vulnhub:Digitalworld.local (Mercy v2)靶机

kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.130 信息收集 端口扫描 nmap -A -v -sV -T5 -p- --scripthttp-enum 192.168.111.130 使用enum4linux对目标smb服务进行枚举 enum4linux -a 192.168.111.130 目标文件共享的目录 目标存在的用户 8080端口的网…

电阻器的原理、类型、参数以及生活中常见的应用

电阻器是电子电路中最基本的元件之一&#xff0c;它的作用是限制电流流过的大小&#xff0c;在电子电路中广泛应用于电流控制、电压分压、信号衰减等方面。在本文中&#xff0c;我们将详细介绍电阻器的原理、类型、参数以及生活中常见的应用。 一、电阻器的原理 电阻器是一种…

【Docker】Docker复杂安装(mysql+redis)

安装mysql主从复制 主从复制原理 主从搭建步骤 新建主服务器容器实例3307 [root192 ~]# docker run -d -p 3307:3306 --privilegedtrue -v /tmp/mysql-master/log:/var/log/mysql -v /tmp/mysql-master/data:/var/lib/mysql -v /tmp/mysql-master/conf:/etc/mysql -e MYS…

魔兽世界巫妖王架设教程-娱乐版

相信各位拿到一个优秀的魔兽端&#xff0c;在单机把玩一番之后&#xff0c;肯定都想着能不能假设一个外网服务器&#xff0c;然后让朋友们来到自己的服务器上玩耍&#xff0c;自己还能体会一下在众多凡人面前当神&#xff08;GM&#xff09;的乐趣。网上这方面的教程有一些&…

【嵌入式环境下linux内核及驱动学习笔记-(3-字符设备驱动详解)】

目录1、文件系统与设备驱动2、设备文件2.1 linux的文件种类&#xff1a;2.2 设备分类3、 设备号3.1 dev_t类型3.2 与设备号相关的操作介绍3.2.1 宏 MKDEV3.2.2 宏 MAJOR3.2.3 宏 MINOR3.2.4 命令mknod3.2.5 register_chrdev_region()3.2.6 alloc_chrdev_region()3.2.7 unregist…

【剑指offer-C++】JZ82:二叉树中和为某一值的路径(一)

【剑指offer-C】JZ82&#xff1a;二叉树中和为某一值的路径[一]题目描述解题思路题目描述 描述&#xff1a;给定一个二叉树root和一个值 sum &#xff0c;判断是否有从根节点到叶子节点的节点值之和等于 sum 的路径。 1.该题路径定义为从树的根结点开始往下一直到叶子结点所经…

一篇文章 学会 Vue3 极速入门 (附带增删改查 案例 + Springboot)

vue3前置 00-导学 这将是你能看到的最快速Vue3 入门文章&#xff0c; 我们将快速的 去学习Vue3相关的知识&#xff0c;并结合后端做一个增删改查的项目&#xff0c;能够帮助你快速的上手Vue3&#xff0c; 包含了Vue 所含的所有特性&#xff0c; 你会知道 Vue3和Vue2 的区别&am…

Day944.度量指标 -系统重构实战

度量指标 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于度量指标的内容。 很多时候在研发过程中&#xff0c;都习惯性地用“拍脑袋”的方式来看待一个事情。例如这个代码写得不好、这个自动化测试覆盖不充分、版本的发布频率太差了等等。往往只知道哪里有问题&…

后台服务异常?测试右移告警监控早知道。。。。

目录 引言 “测试右移”思想下实践步骤 什么是“测试右移” 一、收到问题反馈 二、沟通定位问题 1.服务架构 三、讨论并选定解决方案 1.讨论分析解决方案 2.选定解决方案 四、解决方案实现 1.总体方案设计 2.编写监控脚本 3.配置服务器定时任务 五、测试环境验证…