SpringBoot整合多数据源

1. 什么是多数据源?

最常见的单一应用中最多涉及到一个数据库,即是一个数据源(Datasource)。那么顾名思义,多数据源就是在一个单一应用中涉及到了两个及以上的数据库了。

其实在配置数据源的时候就已经很明确这个定义了,如以下代码:

@Bean(name = "dataSource")
public DataSource dataSource() {
    DruidDataSource druidDataSource = new DruidDataSource();
    druidDataSource.setUrl(url);
    druidDataSource.setUsername(username);
    druidDataSource.setDriverClassName(driverClassName);
    druidDataSource.setPassword(password);
    return druidDataSource;
}

[!note]

urlusernamepassword这三个属性已经唯一确定了一个数据库了,DataSource则是依赖这三个创建出来的。

则多数据源即是配置多个DataSource(暂且这么理解)。

2. 何时用到多数据源?

正如前言介绍到的一个场景,相信大多数做过医疗系统的都会和HIS打交道,为了简化护士以及医生的操作流程,必须要将必要的信息从HIS系统对接过来,据我了解的大致有两种方案如下:

  1. HIS提供视图,比如医护视图、患者视图等,而此时其他系统只需要定时的从HIS视图中读取数据同步到自己数据库中即可。
  2. HIS提供接口,无论是webService还是HTTP形式都是可行的,此时其他系统只需要按照要求调接口即可。

很明显第一种方案涉及到了至少两个数据库了,一个是HIS数据库,一个自己系统的数据库,在单一应用中必然需要用到多数据源的切换才能达到目的。

当然多数据源的使用场景还是有很多的,以上只是简单的一个场景。

[!tip]

本人不才,第一份工作就是在一家医疗行业做的Java后端开发,也是从那时知道HIS,并且当时正好是用到了多数据源。

3. 整合单一的数据源

本文使用阿里的数据库连接池druid,添加依赖如下:

<!--druid连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.9</version>
</dependency>

阿里的数据库连接池非常强大,比如数据监控数据库加密等等内容,本文仅仅演示与Spring Boot整合的过程,一些其他的功能后续可以自己研究添加。

Druid连接池的starter的自动配置类是DruidDataSourceAutoConfigure,类上标注如下一行注解:

@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})

DruidDataSourceAutoConfigure配置类

@EnableConfigurationProperties这个注解使得配置文件中的配置生效并且映射到指定类的属性。

DruidStatProperties中指定的前缀是spring.datasource.druid,这个配置主要是用来设置连接池的一些参数。

DataSourceProperties中指定的前缀是spring.datasource,这个主要是用来设置数据库的urlusernamepassword等信息。

因此我们只需要在全局配置文件中指定数据库的一些配置以及连接池的一些配置信息即可,前缀分别是spring.datasource.druidspring.datasource

以下是个人随便配置的(application.yaml):

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      url: jdbc:mysql://localhost:3306/test01
      username: root
      #明文密码
      password: root
      #初始化连接数
      initial-size: 10
      # 最大连接数
      max-active: 50
      #最小连接数
      min-idle: 10
      #获取连接最大等待时间
      max-wait: 5000
      #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      validation-query: SELECT 1
      validation-query-timeout: 20000
      #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      test-on-borrow: false
      #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      test-on-return: false
      #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      test-while-idle: true
      #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      #一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      #StatViewServlet配置。(因为暴露的监控信息比较敏感,支持密码加密和访问ip限定)
      stat-view-servlet:
        enabled: false
      filter:
        stat:
        log-slow-sql: true
        slow-sql-millis: 1000
        merge-sql: true
        wall:
        config:
          enabled: true
          multi-statement-allow: true

[!note]

在全局配置文件application.properties文件中配置以上的信息即可注入一个数据源到Spring Boot中。其实这仅仅是一种方式,下面介绍另外一种方式。

在自动配置类中DruidDataSourceAutoConfigure中有如下一段代码:

@ConditionalOnMissingBean@Bean这两个注解的结合,意味着我们可以覆盖,只需要提前在IOC中注入一个DataSource类型的Bean即可。

因此我们在自定义的配置类中定义如下配置即可:

/**
 * @Bean:向IOC容器中注入一个Bean
 * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
 * @return
 */
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource(){
    //做一些其他的自定义配置,比如密码加密等......
    return new DruidDataSource();
}

以上介绍了两种数据源的配置方式,第一种比较简单,第二种适合扩展,按需选择。

3.1 @EnableConfigurationProperties注解讲解

这里简单讲解一下它,不感兴趣的同学可以继续看下一小节。

在 Spring Boot 中,@EnableConfigurationProperties是一个非常重要的注解,主要用来启用配置属性绑定的功能。它和 @ConfigurationProperties注解配合使用,可以将配置文件(如application.propertiesapplication.yml)中的内容绑定到一个Java Bean上,从而方便地管理和使用这些配置。

3.1 基本作用

@EnableConfigurationProperties的作用是告诉 Spring Boot 启用对带有@ConfigurationProperties注解的类的支持。

简单来说,它的职责是:

  • 激活并注册 某些被标注为@ConfigurationProperties的类,让它们能够将配置文件中的内容注入到对应的属性中。
  • 它的典型用法是声明在一个配置类(如@Configuration标注的类)上。

3.2 使用场景

当我们有很多配置项需要从配置文件中加载,并且需要将这些配置封装为一个 Java 对象时,就可以使用 @EnableConfigurationProperties配合@ConfigurationProperties


3.3 具体用法示例

3.3.1 配置文件

application.ymlapplication.properties文件中添加配置:

app:
  name: MySpringApp
  version: 1.0.0
  description: This is a demo application

3.3.2 创建配置类

创建一个 POJO 类,用来接收配置文件中的内容:

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private String version;
    private String description;
    // Getter 和 Setter 方法省略
}

3.3.3 启用配置属性绑定

在一个配置类中使用@EnableConfigurationProperties注解来激活这个功能:

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
    // 其他配置可以放在这里
}

3.3.4 使用绑定的配置类

在Spring管理的任何Bean中,可以通过依赖注入的方式使用这个配置类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AppService {
    private final AppProperties appProperties;

    @Autowired
    public AppService(AppProperties appProperties) {
        this.appProperties = appProperties;
    }

    public void printAppDetails() {
        System.out.println("App Name: " + appProperties.getName());
        System.out.println("App Version: " + appProperties.getVersion());
        System.out.println("App Description: " + appProperties.getDescription());
    }
}

从Spring Boot 2.2开始,如果你直接将@ConfigurationProperties注解的类声明为Spring的Bean(通过@Component注解),就不需要显式地使用@EnableConfigurationProperties。比如:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private String version;
    private String description;

    // Getter 和 Setter 方法省略
}

这样直接声明为Spring Bean后,Spring Boot会自动处理,不需要额外添加@EnableConfigurationProperties


3.4 核心原理

  1. 作用机制

    • @EnableConfigurationProperties实际上是通过一个ImportSelector(即 EnableConfigurationPropertiesImportSelector)加载的。
    • 它会扫描并注册@ConfigurationProperties注解的类为Spring容器中的Bean。
  2. 支持多种格式

    • 支持从application.propertiesapplication.yml文件中读取配置。
    • 支持复杂类型的嵌套绑定,如列表、对象等。
  3. 自动绑定

    • Spring Boot使用Binder类完成配置绑定的具体逻辑。

3.5 注意事项

  1. 配置类必须有Getter/Setter方法,否则属性无法正确绑定。
  2. 如果使用@EnableConfigurationProperties,记得将@ConfigurationProperties的类注册到Spring容器中。
  3. 避免使用非标准的命名格式,配置文件中的键应该遵循小写字母和短横线连接的形式

通过@EnableConfigurationProperties@ConfigurationProperties的结合使用,Spring Boo 提供了强大的类型安全配置支持,让开发者可以更方便地管理和使用配置文件中的内容。

3.6 补充

如果你仔细阅读了3.3具体用法示例这一小节,你可能会有点疑惑:AppProperties是一个配置类,为啥还需要AppConfig类上显式的使用@EnableConfigurationProperties(AppProperties.class)这个注解配置呢?为什么需要这个 AppConfig 类呢?

其实这取决于具体的场景和使用习惯。

3.6.1 为什么需要AppConfig类?

AppConfig类的主要作用是作为一个集中配置的地方,它可以:

  1. 明确声明和管理配置类:通过@EnableConfigurationProperties(AppProperties.class),显式声明AppProperties作为一个配置属性类,方便团队合作时阅读代码的人快速定位配置的入口。

  2. 作为扩展配置的入口:在实际项目中,AppConfig通常会用来定义更多的 Bean、依赖或额外的配置,方便统一管理。

    @Configuration
    @EnableConfigurationProperties(AppProperties.class)
    public class AppConfig {
        @Bean
        public SomeService someService() {
            return new SomeService(); // 一个示例 Bean
        }
    }
    
  3. @Configuration 的组合@EnableConfigurationProperties本身可以直接标注在配置类上。将它与@Configuration结合,可以统一管理所有需要的配置。

简而言之:AppConfig是一个扩展性设计,让配置逻辑更清晰、集中,为项目后期增加更多配置管理提供方便。

3.6.2 如果没有AppConfig,直接用@Component,会怎么样?

从 Spring Boot 2.2 开始,如果你直接在AppProperties类上添加@Component,可以完全省略 @EnableConfigurationProperties。例如:

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name;
    private String version;
    private String description;

    // Getter 和 Setter
}

这种方式更加简洁,因为:Spring Boot自动将@Component修饰的类加载为Bean,并且不需要再额外创建一个配置类(如 AppConfig)。

但这样做有一个缺点:不够显式

在复杂的项目中,可能会有多个@ConfigurationProperties类,如果每个都用@Component,管理起来就不够集中。

@EnableConfigurationProperties的方式更适合团队协作和大型项目,因为它将配置逻辑集中在一个地方。

3.2 @Bean与@ConditionalOnMissingBean注解讲解

3.2.1 @Bean

@Bean 注解是Spring Framework中用于将一个方法的返回值注册为Spring容器中的一个Bean的注解。它的常见用途是手动定义 Bean,而不是通过自动扫描。

3.2.1.1 initMethod属性

@Bean(initMethod = "init")表示在这个Bean初始化时,Spring会调用指定的init方法来完成初始化逻辑。

initMethod的作用:通常用于在创建 Bean 后执行一些额外的初始化逻辑,比如:打开资源(数据库连接、文件等)、

设置一些初始化参数、验证依赖关系等。

示例:

@Bean(initMethod = "initialize")
public MyService myService() {
    return new MyService();
}

// MyService 类
public class MyService {
    public void initialize() {
        System.out.println("Initializing MyService...");
    }
}

在这个例子中,当容器创建MyServiceBean 时,会调用initialize()方法。

3.2.2 @ConditionalOnMissingBean

@ConditionalOnMissingBean是Spring Boot提供的条件注解之一,属于条件注解系列,用于控制某个Bean的创建条件。

@ConditionalOnMissingBean 表示仅在Spring容器中没有某种类型的Bean时,才会创建当前方法返回的Bean。

  • 如果容器中已经存在指定类型的 Bean,则不会再创建该 Bean。
  • 通常用于定义默认的 Bean,但允许开发者通过覆盖来提供自定义实现

示例:

@Bean
@ConditionalOnMissingBean
public MyService myService() {
    return new MyService();
}
  • 如果容器中已经存在一个MyService类型的Bean,则这个方法不会执行。
  • 如果容器中没有任何MyService类型的Bean,则创建一个默认的MyService实例。

@ConditionalOnMissingBean也可以指定一些参数,用于更精细地控制条件,比如:

  • value:指定Bean的类型
  • name:指定Bean的名称

示例:

@Bean
@ConditionalOnMissingBean(name = "customService")
public MyService defaultService() {
    return new MyService();
}

如果容器中没有名称为customService的 Bean,则创建defaultService

4 .整合Mybatis

Spring Boot整合Mybatis其实很简单,简单的几步就搞定,首先添加依赖:

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

第二步找到自动配置类MybatisAutoConfiguration,有如下一行代码:

@EnableConfigurationProperties({MybatisProperties.class})

MybatisAutoConfiguration类

老套路了,全局配置文件中配置前缀为mybatis的配置将会映射到该类中的属性。

可配置的东西很多,比如XML文件的位置、类型处理器等等。

如果需要通过包扫描的方式注入Mapper,则需要在配置类上加入一个注解:@MapperScan,其中的value属性指定需要扫描的包。

[!note]

直接在全局配置文件配置各种属性是一种比较简单的方式,其实的任何组件的整合都有不少于两种的配置方式,下面来介绍下配置类如何配置。

MybatisAutoConfiguration自动配置类有如下一段代码:

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{}

SqlSessionFactory这个Bean

@ConditionalOnMissingBean@Bean真是老搭档了,意味着我们又可以覆盖,只需要在IOC容器中注入SqlSessionFactory(Mybatis六剑客之一生产者)。

在自定义配置类中注入即可,如下:

/**
 * 注入SqlSessionFactory
 */
@Bean("sqlSessionFactory1")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    //注意,如果是mybatis-plus,则要用MybatisSqlSessionFactoryBean
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                                             .getResources("classpath*:/mapper/**/*.xml"));
    org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
    // 自动将数据库中的下划线转换为驼峰格式
    configuration.setMapUnderscoreToCamelCase(true);
    configuration.setDefaultFetchSize(100);
    configuration.setDefaultStatementTimeout(30);
    sqlSessionFactoryBean.setConfiguration(configuration);
    return sqlSessionFactoryBean.getObject();
}

以上介绍了配置Mybatis的两种方式,其实在大多数场景中使用第一种已经够用了,至于为什么介绍第二种呢?当然是为了多数据源的整合而做准备了。

MybatisAutoConfiguration中有一行很重要的代码,如下:

@ConditionalOnSingleCandidate(DataSource.class)

@ConditionalOnSingleCandidate这个注解的意思是当IOC容器中只有一个候选Bean的实例才会生效。

这行代码标注在Mybatis的自动配置类中有何含义呢?下面介绍,哈哈哈~

5. 多数据源如何整合?

上文留下的问题:为什么的Mybatis自动配置上标注如下一行代码:

@ConditionalOnSingleCandidate(DataSource.class)

以上这行代码的言外之意:当IOC容器中只有一个数据源DataSource,这个自动配置类才会生效。

哦?照这样搞,多数据源是不能用Mybatis吗?

可能大家会有一个误解,认为多数据源就是多个的DataSource并存的,当然这样说也不是不正确。

多数据源的情况下并不是多个数据源并存的,Spring提供了AbstractRoutingDataSource这样一个抽象类,使得能够在多数据源的情况下任意切换,相当于一个动态路由的作用,作者称之为动态数据源。因此Mybatis只需要配置这个动态数据源即可。

5.1 什么是动态数据源?

动态数据源简单的说就是能够自由切换的数据源,类似于一个动态路由的感觉,Spring 提供了一个抽象类AbstractRoutingDataSource,这个抽象类中哟一个属性,如下:

@Nullable
private Map<Object, Object> targetDataSources;

AbstractRoutingDataSource类源码

targetDataSources是一个Map结构,所有需要切换的数据源都存放在其中,根据指定的KEY进行切换。当然还有一个默认的数据源。

AbstractRoutingDataSource这个抽象类中有一个抽象方法需要子类实现,如下:

protected abstract Object determineCurrentLookupKey();

[!important]

determineCurrentLookupKey()这个方法的返回值决定了需要切换的数据源的KEY,就是根据这个KEYtargetDataSources取值(数据源)。

5.2 数据源切换如何保证线程隔离?

数据源属于一个公共的资源,在多线程的情况下如何保证线程隔离呢?不能我这边切换了影响其他线程的执行。

说到线程隔离,自然会想到ThreadLocal了,将切换数据源的KEY(用于从targetDataSources中取值)存储在ThreadLocal中,执行结束之后清除即可。

单独封装了一个DataSourceHolder,内部使用ThreadLocal隔离线程,代码如下:

/**
 * 使用ThreadLocal存储切换数据源后的KEY
 */
public class DataSourceHolder {

    //线程  本地环境
    private static final ThreadLocal<String> dataSources = new ThreadLocal();

    //设置数据源
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    //获取数据源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除数据源
    public static void clearDataSource() {
        dataSources.remove();
    }
}

5.3 如何构造一个动态数据源?

上文说过只需继承一个抽象类AbstractRoutingDataSource,重写其中的一个方法determineCurrentLookupKey()即可。代码如下:

/**
 * 动态数据源,继承AbstractRoutingDataSource
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 返回需要使用的数据源的key,将会按照这个KEY从Map获取对应的数据源(切换)
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取出KEY
        return DataSourceHolder.getDataSource();
    }
}

上述代码很简单,分析如下:

  1. 一个多参的构造方法,指定了默认的数据源和目标数据源。
  2. 重写determineCurrentLookupKey()方法,返回数据源对应的KEY,这里是直接从ThreadLocal中取值,就是上文封装的DataSourceHolder

5.4 定义一个注解

为了操作方便且低耦合,不能每次需要切换的数据源的时候都要手动调一下接口吧,可以定义一个切换数据源的注解,如下:

/**
 * 切换数据源的注解
 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {

    /**
     * 默认切换的数据源KEY
     */
    String DEFAULT_NAME = "hisDataSource";

    /**
     * 需要切换到数据的KEY
     */
    String value() default DEFAULT_NAME;
}

注解中只有一个value属性,指定了需要切换数据源的KEY

有注解还不行,当然还要有切面,代码如下:

package com.nxz.aspect;

import com.nxz.annotation.SwitchDataSource;
import com.nxz.util.DataSourceHolderUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Author nxz
 * @Date 2025/1/14 17:06
 * @description: 切换数据源AOP
 */
@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {
    @Pointcut("@annotation(com.nxz.annotation.SwitchDataSource)")
    public void pointcut() {
    }

    /**
     * 在方法执行之前切换到指定的数据源
     *
     * @param joinPoint
     */
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        //因为是对注解进行切面,所以这边无需做过多判定,直接获取注解的值,进行环绕,
        //将数据源设置成指定value的值,然后结束后,清除当前线程数据源
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchDataSource switchDataSource = method.getAnnotation(SwitchDataSource.class);
        log.info("[切换到数据源]:" + switchDataSource.value());
        DataSourceHolderUtil.setDataSource(switchDataSource.value());
    }

    /**
     * 方法执行之后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源
     */
    @After(value = "pointcut()")
    public void afterOpt() {
        DataSourceHolderUtil.clearDataSource();
        log.info("[切回默认数据源]");
    }
}

这个ASPECT很容易理解,beforeOpt()在方法之前执行,取值@SwitchSource中value属性设置到ThreadLocal中;afterOpt()方法在方法执行之后执行,清除掉ThreadLocal中的KEY,保证了如果不切换数据源,则用默认的数据源。

注意,切面需要有AOP的支持,如果没有,则需要引入:

<!--spring切面aop依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

5.5 如何与Mybatis整合?

单一数据源与Mybatis整合上文已经详细讲解了,数据源DataSource作为参数构建了SqlSessionFactory,同样的思想,只需要把这个数据源换成动态数据源即可。注入的代码如下:

/**
 * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
 * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
 */
@Primary
@Bean("sqlSessionFactory2")
public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
    //注意,如果是mybatis-plus,则要用MybatisSqlSessionFactoryBean
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dynamicDataSource);
    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                                             .getResources("classpath*:/mapper/**/*.xml"));
    org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
    configuration.setMapUnderscoreToCamelCase(true);
    configuration.setDefaultFetchSize(100);
    configuration.setDefaultStatementTimeout(30);
    sqlSessionFactoryBean.setConfiguration(configuration);
    return sqlSessionFactoryBean.getObject();
}

[!note]

与Mybatis整合很简单,只需要把数据源替换成自定义的动态数据源DynamicDataSource。也就是sqlSessionFactoryBean这个方法入参的地方。

那么动态数据源如何注入到IOC容器中呢?看上文自定义的DynamicDataSource构造方法,肯定需要两个数据源了,因此必须先注入两个或者多个数据源到IOC容器中,如下:

/**
 * @Bean:向IOC容器中注入一个Bean
 * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
 */
@ConfigurationProperties(prefix = "spring.datasource")
@Bean("dataSource")
public DataSource dataSource(){
    return DataSourceBuilder.create().build();
}
/**
 * 向IOC容器中注入另外一个数据源
 * 全局配置文件中前缀是spring.datasource.his
 */
@Bean(name = SwitchSource.DEFAULT_NAME)
@ConfigurationProperties(prefix = "spring.datasource.his")
public DataSource hisDataSource() {
    return DataSourceBuilder.create().build();
}

[!caution]

如果你是用的Druid连接池,那么这里的配置有点变化。你需要将DataSource对象换成DruidDataSource对象,DataSourceBuilder对象换成DruidDataSourceBuilder对象。并且在数据库的配置yaml配置文件中,要指名datasource的type。

以上构建的两个数据源,一个是默认的数据源,一个是需要切换到的数据源(targetDataSources,这样就组成了动态数据源了。数据源的一些信息,比如urlusername需要自己在全局配置文件中根据指定的前缀配置即可,代码不再贴出。

动态数据源的注入代码如下:

/**
 * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
 * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
 */
@Primary
@Bean("sqlSessionFactory2")
public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
    //注意,如果是mybatis-plus,则要用MybatisSqlSessionFactoryBean
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dynamicDataSource);
    org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
    configuration.setMapUnderscoreToCamelCase(true);
    configuration.setDefaultFetchSize(100);
    configuration.setDefaultStatementTimeout(30);
    sqlSessionFactoryBean.setConfiguration(configuration);
    return sqlSessionFactoryBean.getObject();
}

[!caution]

这里还有一个问题:IOC中存在多个数据源了,那么事务管理器怎么办呢?它也懵逼了,到底选择哪个数据源呢?因此事务管理器肯定还是要重新配置的。

事务管理器此时管理的数据源将是动态数据源DynamicDataSource,配置如下:

/**
 * 重写事务管理器,管理动态数据源
 */
@Primary
@Bean(value = "transactionManager2")
public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

至此,Mybatis与多数据源的整合就完成了。

5.6 演示

现在我们来完整的演示一遍。

加入Druid以及Mybatis的xml依赖配置,这里就不在多说。

我的数据库配置文件如下:

spring:
  datasource:
    #配置第一个数据库
    db1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/user
      username: root
      password: root
      #如果这里是用的Druid连接池,这里需要指明类型
      #type: com.alibaba.druid.pool.DruidDataSource
    #配置第二个数据库
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/school
      username: root
      password: root

[!caution]

各个版本的 springboot 配置datasource时参数有所变化,例如低版本配置数据库url时使用url属性,高版本使用jdbc-url属性,请注意区分。

编写数据源切换工具类:

package com.nxz.util;

/**
 * @Author nxz
 * @Date 2025/1/14 16:55
 * @description: 数据源切换处理,需要存入ThreadLocal隔离线程,这里存入和获取均是拿到的数据源key
 */
public class DataSourceHolderUtil {
    private static final ThreadLocal<String> dataSources = new ThreadLocal<>();

    //设置数据源
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    //获取数据源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除数据源
    public static void clearDataSource() {
        dataSources.remove();
    }
}

动态数据源重写determineCurrentLookupKey方法:

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 重写determineCurrentLookupKey方法拿到数据源Key
     *
     * @return 数据源Key
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取出KEY
        return DataSourceHolderUtil.getDataSource();
    }
}

之后编写动态数据源配置类:

package com.nxz.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author nxz
 * @Date 2025/1/14 16:42
 * @description: 动态数据源配置类
 */
@Configuration
public class DynamicDataSourceConfig {

    @ConfigurationProperties(prefix = "spring.datasource.db1")
    @Bean("userDataSource")
    public DataSource userDataSource(){
        return DataSourceBuilder.create().build();
    }

    @ConfigurationProperties(prefix = "spring.datasource.db2")
    @Bean("schoolDataSource")
    public DataSource schoolDataSource(){
        return DataSourceBuilder.create().build();
    }

    //将动态数据源设置为主数据源
    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource(@Qualifier("userDataSource") DataSource userDataSource,
                                               @Qualifier("schoolDataSource") DataSource schoolDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("db1", userDataSource);
        targetDataSources.put("db2", schoolDataSource);

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(userDataSource);
        //设置目标数据源
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }

    //设置为主SqlSessionFactory
    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
        //注意,如果是mybatis-plus,则要用MybatisSqlSessionFactoryBean
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:/mapper/**/*.xml"));
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

    //数据源事务配置,并设置为主事务管理器
    @Bean
    @Primary
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

对应的切换数据源注解以及AOP切面:

package com.nxz.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchDataSource {

    /**
     * 默认切换的数据源KEY
     */
    String DEFAULT_NAME = "db1";

    /**
     * 需要切换到数据的KEY
     */
    String value() default DEFAULT_NAME;
}
package com.nxz.aspect;

import com.nxz.annotation.SwitchDataSource;
import com.nxz.util.DataSourceHolderUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Author nxz
 * @Date 2025/1/14 17:06
 * @description: 切换数据源AOP
 */
@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {
    @Pointcut("@annotation(com.nxz.annotation.SwitchDataSource)")
    public void pointcut() {
    }

    /**
     * 在方法执行之前切换到指定的数据源
     *
     * @param joinPoint
     */
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        //因为是对注解进行切面,所以这边无需做过多判定,直接获取注解的值,进行环绕,
        //将数据源设置成指定value的值,然后结束后,清除当前线程数据源
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchDataSource switchDataSource = method.getAnnotation(SwitchDataSource.class);
        log.info("[切换到数据源]:" + switchDataSource.value());
        DataSourceHolderUtil.setDataSource(switchDataSource.value());
    }

    /**
     * 方法执行之后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源
     */
    @After(value = "pointcut()")
    public void afterOpt() {
        DataSourceHolderUtil.clearDataSource();
        log.info("[切回默认数据源]");
    }
}

现在所有的配置都准备好了,现在我们来测试,给出服务接口以及测试接口:

package com.nxz.service.impl;

import com.nxz.annotation.SwitchDataSource;
import com.nxz.domain.School;
import com.nxz.domain.User;
import com.nxz.mapper.UserAndSchoolMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Author nxz
 * @Date 2025/1/14 17:52
 * @description: 多数据源服务
 */
@Service
public class MultipleDataSourceService {
    @Resource
    private UserAndSchoolMapper userAndSchoolMapper;

    // 使用第一个数据源
    @SwitchDataSource
    @Transactional
    public void useDb1() {
        // 执行数据库操作
        List<User> userList = userAndSchoolMapper.selectUserList();
    }

    // 使用第二个数据源
    @SwitchDataSource(value = "db2")
    @Transactional
    public void useDb2() {
        // 执行数据库操作
        List<School> schoolList = userAndSchoolMapper.selectSchoolList();
    }
}

测试接口:

package com.nxz;

import com.nxz.service.impl.MultipleDataSourceService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

/**
 * @Author nxz
 * @Date 2025/1/14 18:05
 * @description: 多数据源测试
 */
@SpringBootTest
public class MultipleDataSourceTest {
    @Resource
    private MultipleDataSourceService service;
    @Test
    void t1(){
        service.useDb1();
    }

    @Test
    void t2(){
        service.useDb2();
    }
}

执行t1,结果如下:

t1方法执行结果

可见执行成功并且执行完毕之后,又切换回了默认数据源。

6. 最后

截止到这里,整合多数据源就结束了。当然,上诉大部分都是自定义配置的方式来整合多数据源的,其实还有注解的方式,也就是使用这个依赖dynamic-datasource-spring-boot-starter来做。

虽然注解的方式比较简单,但我们还是要知其所以然,我个人更加倾向于使用自定义的方式来整合。


参考:https://blog.csdn.net/qq_45408390/article/details/119574030

参考:SpringBoot教程(二十五) | SpringBoot配置多个数据源-CSDN博客