美文网首页Java
MyBatis整合Springboot多数据源实现

MyBatis整合Springboot多数据源实现

作者: 喝杯Java润润BUG | 来源:发表于2023-04-11 09:58 被阅读0次

    前言

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

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

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

    image.png

    正文

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

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

    1. TomcatJdbc。TomcatJdbcApache提供的一种数据库连接池解决方案,各方面都还行,各方面也都不突出;
    2. Druid。Druid是阿里开源的数据库连接池,是阿里监控系统Dragoon的副产品,提供了强大的可监控性和基于Filter-Chain的可扩展性;
    3. HikariCP。HikariCP是基于BoneCP进行了大量改进和优化的数据库连接池,是Springboot 2.x版本默认的数据库连接池,也是速度最快的数据库连接池。

    二. Springboot加载数据源原理分析

    首先搭建一个极简的示例工程,POM文件引入依赖如下所示。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    

    编写一个Spring Boot的启动类,如下所示。

    @SpringBootApplication
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
    

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

    @Repository
    public class MyDao implements InitializingBean {
    
        @Autowired
        private DataSource dataSource;
    
        @Override
        public void afterPropertiesSet() throws Exception {
            Connection connection = dataSource.getConnection();
            System.out.println("获取到数据库连接:" + connection);
        }
    
    }
    

    application.yml文件中加入数据源的参数配置。

    spring:
      datasource:
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          max-lifetime: 1600000
          keep-alive-time: 90000
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
        username: root
        password: root
    

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

    整体的工程目录如下。

    image.png

    负责完成数据源加载的类叫做
    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等配置,像我们配置的type,driver-class-name,url,usernamepassword都会加载在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()方法的实现。

    public DataSourceBuilder<?> initializeDataSourceBuilder() {
        return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
                .url(determineUrl()).username(determineUsername()).password(determinePassword());
    }
    

    也就是在创建DataSourceBuilder时,会一并设置type,driverClassName,url,usernamepassword等属性,其中typedriverClassName不用设置也没关系,Springboot会做自动判断,只需要引用了相应的依赖即可。

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

    1. 数据源的通用配置会保存在DataSourceProperties中。例如url,usernamepassword等配置都属于通用配置;
    2. HikariCP的数据源是HikariDataSource,HikariCP相关的配置会保存在HikariDataSource中。例如max-lifetime,keep-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中的。在超详细解释MyBatisSpring的集成原理一文中,有提到将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。

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

    image.png

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

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

    image.png

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

    最后在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的多数据源切换。

    总结

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

    image.png

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

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

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

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

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

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

    我认为有两点。

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

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

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

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

    相关文章

      网友评论

        本文标题:MyBatis整合Springboot多数据源实现

        本文链接:https://www.haomeiwen.com/subject/ubkgddtx.html