天天看點

MyBatis整合Springboot多資料源實作

MyBatis整合Springboot多資料源實作

前言

資料源,實際就是資料庫連接配接池,負責管理資料庫連接配接,在Springboot中,資料源通常以一個bean的形式存在于IOC容器中,也就是我們可以通過依賴注入的方式拿到資料源,然後再從資料源中擷取資料庫連接配接。

那麼什麼是多資料源呢,其實就是IOC容器中有多個資料源的bean,這些資料源可以是不同的資料源類型,也可以連接配接不同的資料庫。

本文将對多資料如何加載,如何結合MyBatis使用進行說明,知識點腦圖如下所示。

MyBatis整合Springboot多資料源實作

正文

一. 資料源概念和常見資料源介紹

資料源,其實就是資料庫連接配接池,負責資料庫連接配接的管理和借出。目前使用較多也是性能較優的有如下幾款資料源。

  1. TomcatJdbc。TomcatJdbc是Apache提供的一種資料庫連接配接池解決方案,各方面都還行,各方面也都不突出;
  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>           

編寫一個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           

其中url,username和password是必須配置的,其它的僅僅是為了示範。

整體的工程目錄如下。

MyBatis整合Springboot多資料源實作

負責完成資料源加載的類叫做DataSourceAutoConfiguration,由spring-boot-autoconfigure包提供,DataSourceAutoConfiguration的加載是基于Springboot的自動裝配機制,不過這裡說明一下,由于本篇文章是基于Springboot的2.7.6版本,是以沒有辦法在spring-boot-autoconfigure包的spring.factories檔案中找到DataSourceAutoConfiguration,在Springboot的2.7.x版本中,是通過加載META-INF/spring/xxx.xxx.xxx.imports檔案來實作自動裝配的,但這不是本文重點,故先在這裡略做說明。

下面先看一下DataSourceAutoConfiguration的部分代碼實作。

@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {

    ......

    @Configuration(proxyBeanMethods = false)
    @Conditional(PooledDataSourceCondition.class)
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
            DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
            DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
    protected static class PooledDataSourceConfiguration {

    }

    ......

}           

上述展示出來的代碼,做了兩件和加載資料源有關的事情。

  1. 将資料源的配置類DataSourceProperties注冊到了容器中;
  2. 将DataSourceConfiguration的靜态内部類Hikari注冊到了容器中。

先看一下DataSourceProperties的實作,如下所示。

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

	private ClassLoader classLoader;

	private boolean generateUniqueName = true;

	private String name;

	private Class<? extends DataSource> type;

	private String driverClassName;

	private String url;

	private String username;

	private String password;
	
	......
	
}           

DataSourceProperties中加載了配置在application.yml檔案中的spring.datasource.xxx等配置,像我們配置的type,driver-class-name,url,username和password都會加載在DataSourceProperties中。

再看一下DataSourceConfiguration的靜态内部類Hikari的實作,如下所示。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
        matchIfMissing = true)
static class Hikari {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    HikariDataSource dataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }

}           

可知Hikari會向容器注冊一個HikariCP的資料源HikariDataSource,同時HikariDataSource也是一個配置類,其會加載application.yml檔案中的spring.datasource.hikari.xxx等和HikariCP相關的資料源配置,像我們配置的max-lifetime和keep-alive-time都會加載在HikariDataSource中。

然後還能發現,建立HikariDataSource的createDataSource方法的第一個參數是容器中的DataSourceProperties的bean,是以在建立HikariDataSource時,肯定是需要使用到DataSourceProperties裡面儲存的相關配置的,下面看一下DataSourceConfiguration的createDataSource() 方法的實作。

protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
    return (T) properties.initializeDataSourceBuilder().type(type).build();
}
複制代碼           

DataSourceProperties的initializeDataSourceBuilder() 方法會傳回一個DataSourceBuilder,具體實作如下。

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

也就是在建立DataSourceBuilder時,會一并設定type,driverClassName,url,username和password等屬性,其中type和driverClassName不用設定也沒關系,Springboot會做自動判斷,隻需要引用了相應的依賴即可。

那麼至此,Springboot加載資料源原理已經分析完畢,小結如下。

  1. 資料源的通用配置會儲存在DataSourceProperties中。例如url,username和password等配置都屬于通用配置;
  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的配置内容會加載到name為ds1的HikariDataSource中,lee.datasource.ds2.xxx的配置内容會加載到name為ds2的HikariDataSource中,最後name為ds1的HikariDataSource和name為ds2的HikariDataSource都會作為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,需要向容器中注冊SqlSessionFactory的bean,以及MapperScannerConfigurer的bean。那麼有理由相信,MyBatis整合Springboot的starter包mybatis-spring-boot-starter應該也是在做這個事情,下面來分析一下mybatis-spring-boot-starter的工作原理。

首先在POM中引入mybatis-spring-boot-starter的依賴,如下所示。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>           

mybatis-spring-boot-starter會引入mybatis-spring-boot-autoconfigure,看一下mybatis-spring-boot-autoconfigure的spring.factories檔案,如下所示。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration           

是以負責自動裝配MyBatis的類是MybatisAutoConfiguration,該類的部分代碼如下所示。

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

    ......

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        // 設定資料源
        factory.setDataSource(dataSource);
        
        ......
        
        return factory.getObject();
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }

    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

        private BeanFactory beanFactory;

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

            ......

            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
            
            ......
            
            registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
        }

        @Override
        public void setBeanFactory(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }

    }
    
    ......

}           

歸納一下MybatisAutoConfiguration做的事情如下所示。

  1. 将MyBatis相關的配置加載到MybatisProperties并注冊到容器中。實際就是将application.yml檔案中配置的mybatis.xxx相關的配置加載到MybatisProperties中;
  2. 基于Springboot加載的資料源建立SqlSessionFactory并注冊到容器中。MybatisAutoConfiguration使用了@AutoConfigureAfter注解來指定MybatisAutoConfiguration要在DataSourceAutoConfiguration執行完畢之後再執行,是以此時容器中已經有了Springboot加載的資料源;
  3. 基于SqlSessionFactory建立SqlSessionTemplate并注冊到容器中;
  4. 使用AutoConfiguredMapperScannerRegistrar向容器注冊MapperScannerConfigurer。AutoConfiguredMapperScannerRegistrar實作了ImportBeanDefinitionRegistrar接口,是以可以向容器注冊bean。

那麼可以發現,其實MybatisAutoConfiguration幹的事情和我們自己将MyBatis內建到Spring幹的事情是一樣的:1. 擷取一個資料源并基于這個資料源建立SqlSessionFactory的bean并注冊到容器中;2. 建立MapperScannerConfigurer的bean并注冊到容器中。

五. MyBatis整合Springboot多資料源實作

mybatis-spring-boot-starter是單資料源的實作,本節将對MyBatis整合Springboot的多資料實作進行示範和說明。

首先需要引入相關依賴,POM檔案如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.lee.learn.multidatasource</groupId>
    <artifactId>learn-multidatasource</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

</project>           

然後提供多資料源的配置,application.yml檔案如下所示。

lee:
  datasource:
    ds1:
      max-lifetime: 1600000
      keep-alive-time: 90000
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
      username: root
      password: root
      pool-name: testpool-1
    ds2:
      max-lifetime: 1600000
      keep-alive-time: 90000
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://192.168.101.8:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
      username: root
      password: root
      pool-name: testpool-2           

現在先看一下基于資料源ds1的MyBatis的配置類,如下所示。

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

}           

同理,基于資料源ds2的MyBatis的配置類,如下所示。

@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。

完整的示例工程目錄結構如下所示。

MyBatis整合Springboot多資料源實作

BookMapper和BookMapper.xml如下所示。

public interface BookMapper {

    List<Book> queryAllBooks();

}           
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.multidatasource.dao.mapper1.BookMapper">
    <resultMap id="bookResultMap" type="com.lee.learn.multidatasource.entity.Book">
        <id column="id" property="id"/>
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
        <result column="bs_id" property="bsId"/>
    </resultMap>

    <select id="queryAllBooks" resultMap="bookResultMap">
        SELECT * FROM book;
    </select>

</mapper>           

StudentMapper和StudentMapper.xml如下所示。

public interface StudentMapper {

    List<Student> queryAllStudents();

}           
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.multidatasource.dao.mapper2.StudentMapper">
    <resultMap id="studentResultMap" type="com.lee.learn.multidatasource.entity.Student">
        <id column="id" property="id"/>
        <result column="name" property="studentName"/>
        <result column="level" property="studentLevel"/>
        <result column="grades" property="studentGrades"/>
    </resultMap>

    <select id="queryAllStudents" resultMap="studentResultMap">
        SELECT * FROM stu;
    </select>

</mapper>           

Book和Student如下所示。

public class Book {

    private int id;
    private String bookName;
    private float bookPrice;
    private int bsId;

    // 省略getter和setter

}

public class Student {

    private int id;
    private String studentName;
    private String studentLevel;
    private int studentGrades;

    // 省略getter和setter

}           

BookService和StudentService如下所示。

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

}           

BookController和StudentsController如下所示。

@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. 将路由資料源配置給MyBatis的SqlSessionFactory;
  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;
    }

}           

名字為ds1和ds2的資料源沒什麼好說的,具體關注一下名字為mds的資料源,也就是所謂的路由資料源,其實作如下所示。

public class MultiDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> DATA_SOURCE_NAME = new ThreadLocal<>();

    public static void setDataSourceName(String dataSourceName) {
        DATA_SOURCE_NAME.set(dataSourceName);
    }

    public static void removeDataSourceName() {
        DATA_SOURCE_NAME.remove();
    }

    @Override
    public Object determineCurrentLookupKey() {
        return DATA_SOURCE_NAME.get();
    }

}           

我們自定義了一個路由資料源叫做MultiDataSource,其實作了AbstractRoutingDataSource類,而AbstractRoutingDataSource類正是Springboot提供的用于做資料源切換的一個抽象類,其内部有一個Map類型的字段叫做targetDataSources,裡面存放的就是需要做切換的資料源,key是資料源的名字,value是資料源。當要從路由資料源擷取Connection時,會調用到AbstractRoutingDataSource提供的getConnection() 方法,看一下其實作。

public Connection getConnection() throws SQLException {
    return determineTargetDataSource().getConnection();
}

protected DataSource determineTargetDataSource() {
   Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
   // 得到實際要使用的資料源的key
   Object lookupKey = determineCurrentLookupKey();
   // 根據key從resolvedDataSources中拿到實際要使用的資料源
   DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
   }
   if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
   }
   return dataSource;
}           

其實呢從路由資料源拿到實際使用的資料源時,就是首先通過determineCurrentLookupKey() 方法拿key,然後再根據key從resolvedDataSources這個Map中拿到實際使用的資料源。看到這裡可能又有疑問了,在DataSourceConfig中建立路由資料源的bean時,明明隻設定了AbstractRoutingDataSource#targetDataSources的值,并沒有設定AbstractRoutingDataSource#resolvedDataSources,那為什麼resolvedDataSources中會有實際要使用的資料源呢,關于這個問題,可以看一下AbstractRoutingDataSource的afterPropertiesSet() 方法,這裡不再贅述。

那麼現在可以知道,每次從路由資料源擷取實際要使用的資料源時,關鍵的就在于如何通過determineCurrentLookupKey() 拿到資料源的key,而determineCurrentLookupKey() 是一個抽象方法,是以在我們自定義的路由資料源中對其進行了重寫,也就是從一個ThreadLocal中拿到資料源的key,有拿就有放,那麼ThreadLocal是在哪裡設定的資料源的key的呢,那當然就是在切面中啦。下面一起看一下。

首先定義一個切面,如下所示。

@Aspect
@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放到MultiDataSource的ThreadLocal中,然後執行目标方法,最後在目标方法執行完畢後,将資料源的key從MultiDataSource的ThreadLocal中再移除。

現在已經有路由資料源了,也有為路由資料源設定實際使用資料源key的切面了,最後一件事情就是将路由資料源給到MyBatis的SessionFactory,配置類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;
    }

}           

完整的示例工程目錄結構如下。

MyBatis整合Springboot多資料源實作

除了上面的代碼以外,其餘代碼和第五節中一樣,這裡不再重複給出。

最後在BookService和StudentService的方法中添加上@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的多資料源切換。

總結

本文的整體知識點如下所示。

MyBatis整合Springboot多資料源實作

首先資料源其實就是資料庫連接配接池,負責連接配接的管理和借出,目前主流的有TomcatJdbc,Druid和HikariCP。

然後Springboot官方的加載資料源實作,實際就是基于自動裝配機制,通過DataSourceAutoConfiguration來加載資料源相關的配置并将資料源建立出來再注冊到容器中。

是以模仿Springboot官方的加載資料源實作,我們可以自己加載多個資料源的配置,然後建立出不同的資料源的bean,再全部注冊到容器中,這樣我們就實作了加載多資料源。

加載完多資料源後該怎麼使用呢。首先可以通過資料源的的名字,也就是bean的名字來依賴注入資料源,然後直接從資料源拿到Connection,這樣的方式能用,但是肯定沒人會這樣用。是以結合之前MyBatis整合Spring的知識,我們可以将不同的資料源設定給不同的SqlSessionFactory,然後再将不同的SqlSessionFactory設定給不同的MapperScannerConfigurer,這樣就實作了某一些映射接口使用一個資料源,另一些映射接口使用另一個資料源的效果。

最後,還可以借助AbstractRoutingDataSource來實作資料源的切換,也就是提前将建立好的資料源放入路由資料源中,并且一個資料源對應一個key,然後擷取資料源時通過key來擷取,key的設定通過一個切面來實作,這樣的方式可以在更小的粒度來切換資料源。

現在最後思考一下,本文的多資料源的相關實作,最大的問題是什麼。

我認為有兩點。

  1. 本文的多資料源的實作,都是我們自己提供了配置類來做整合,如果新起一個項目,又要重新提供一套配置類;
  2. 資料源的個數,名字都是在整合的時候确定好了,如果加資料源,或者改名字,就得改代碼,改配置類。

是以本文的資料源的實作方式不夠優雅,最好是能夠有一個starter包來完成多資料源加載這個事情,讓我們僅通過少量配置就能實作多資料源的動态加載和使用。