SpringBoot整合MybatisPlus多数据源
  TEZNKK3IfmPf 2023年11月13日 37 0

相信在很多使用MybatisPlus框架的小伙伴都会遇到多数据源的配置问题,并且官网也给出了推荐使用多数据源 (dynamic-datasource-spring-boot-starter) 组件来实现。由于最近项目也在使用这个组件来实现多数据源切换,因此想了解一下该组件是如何运行的,经过自己的调试,简单记录一下这个组件的实现,也以便日后组件如果出问题了或者某些地方需要开次开发时有个参考。

1 简单实现数据源切换

1.1 数据库demo

本例子使用的是同一个MYSQL服务,不同数据库来进行调试的,具体如图所示

SpringBoot整合MybatisPlus多数据源

建表语句如下:

CREATE TABLE `class_t` (
  `name` varchar(30) DEFAULT NULL,
  `number` varchar(30) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `user_t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(30) DEFAULT NULL,
  `user_sex` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

1.2 SpringBoot demo

1.2.1 添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>

1.2.2 配置YML文件

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      maximum-pool-size: 15
      idle-timeout: 30000
      max-lifetime: 1800000
      connection-timeout: 30000
      pool-name: OasisHikariCP
      connection-test-query: SELECT 1
    dynamic:
      primary: db1    #默认主数据源
      datasource:
        db1:                 #配置主数据源
          url: jdbc:mysql://127.0.0.1:3306/test?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        db2:                #配置其他数据源
          url: jdbc:mysql://127.0.0.1:3306/test2?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver

1.2.3 实体层

UserEntity.java

/**
 * @description: DB1中的实体
 * @date: 2021/7/13 13:38 <br>
 * @author: wuKeFan <br>
 * @version: 1.0 <br>
 */
@TableName("user_t")
public class UserEntity {

    private long id;

    private String userName;

    private String userSex;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserSex() {
        return userSex;
    }

    public void setUserSex(String userSex) {
        this.userSex = userSex;
    }
}

ClassEntity.java

/**
 * @description: DB2中的实体
 * @date: 2021/7/13 13:40 <br>
 * @author: wuKeFan <br>
 * @version: 1.0 <br>
 */
@TableName("class_t")
public class ClassEntity {

    private String name;

    private String number;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
}

1.2.4 mapper层

UserMapper.java(使用默认数据源)

/**
 * @description: UserMapper <br>
 * @date: 2021/7/13 13:41 <br>
 * @author: wuKeFan <br>
 * @version: 1.0 <br>
 */
public interface UserMapper extends BaseMapper<UserEntity> {
}

ClassMapper.java(使用另外一个数据源)

/**
 * @description: ClassMapper <br>
 * @date: 2021/7/13 13:41 <br>
 * @author: wuKeFan <br>
 * @version: 1.0 <br>
 */
@DS("db2")     //使用另外一个数据源
public interface ClassMapper extends BaseMapper<ClassEntity> {
}

1.2.5 单元测试

SpringBoot整合MybatisPlus多数据源

结果已经是可以完美运行多数据源。

2 源码解析

在我们搞项目中,不仅要学会用这些组件,更重要的是 要知其所以然,知道他是如何实现的,其实原理也就是网上能搜到的基于切面的代理处理方式,但是其中有些内容还是值得去学习。

2.1 自动装配

首先我们从 dynamic-datasource组件的自动装配开始

SpringBoot整合MybatisPlus多数据源

接下来让我们来看一下 这个自动装配类,所装配的Bean

@Slf4j
@Configuration
//启动SpringBoot 自动装配 DynamicDataSourceProperties外部化配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
//声明装配加载顺序,在 DataSourceAutoConfiguration 之前加载
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
//当自动装配时,引入并自动装配下列三个自动装配类
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceHealthCheckConfiguration.class})
//自动装配加载条件 当 spring.datasource.dynamic = true时 进行自动装配的加载,默认缺省为true
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {

    //注入外部化配置
    private final DynamicDataSourceProperties properties;
    
    
    private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;

    //构造函数注入
    public DynamicDataSourceAutoConfiguration(
            DynamicDataSourceProperties properties,
            ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) {
        this.properties = properties;
        this.dataSourcePropertiesCustomizers = dataSourcePropertiesCustomizers.getIfAvailable();
    }

    //多数据源加载接口,默认的实现为从yml信息中加载所有数据源 
    @Bean
    public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
        return new YmlDynamicDataSourceProvider(properties.getDatasource());
    }

    //实现DataSource JAVA JNDI 后期Spring 容器中 所有的数据库连接都从该实现Bean 中获取
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(properties.getPrimary());
        dataSource.setStrict(properties.getStrict());
        dataSource.setStrategy(properties.getStrategy());
        dataSource.setP6spy(properties.getP6spy());
        dataSource.setSeata(properties.getSeata());
        return dataSource;
    }

    //设置动态数据源转换切换配置器
    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @Bean
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(properties.getOrder());
        return advisor;
    }

    //数据库事务的切面配置类
    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
    @Bean
    public Advisor dynamicTransactionAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
        return new DefaultPointcutAdvisor(pointcut, new DynamicLocalTransactionAdvisor());
    }

    //DynamicDataSourceAnnotationInterceptor 切面配置器中所需要的执行链,
    //主要用来确定使用哪个数据源
    @Bean
    @ConditionalOnMissingBean
    public DsProcessor dsProcessor(BeanFactory beanFactory) {
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }

    //Bean注入后所执行的方法,本Demo中目前暂无使用
    @Override
    public void afterPropertiesSet() {
        if (!CollectionUtils.isEmpty(dataSourcePropertiesCustomizers)) {
            for (DynamicDataSourcePropertiesCustomizer customizer : dataSourcePropertiesCustomizers) {
                customizer.customize(properties);
            }
        }
    }

}

大体上的自动装配已经介绍完了,接下来我们逐个将重要的代码段或者类来进行解释

2.2 DynamicDataSourceCreatorAutoConfiguration类分析

这个类主要是进行数据源加载的 主要代码如下

@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {

    //描述Bean的 注入顺序
    public static final int JNDI_ORDER = 1000;
    public static final int DRUID_ORDER = 2000;
    public static final int HIKARI_ORDER = 3000;
    public static final int BEECP_ORDER = 4000;
    public static final int DBCP2_ORDER = 5000;
    public static final int DEFAULT_ORDER = 6000;

    private final DynamicDataSourceProperties properties;

    //默认的数据源创造器
    @Primary
    @Bean
    @ConditionalOnMissingBean
    public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {
        DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
        defaultDataSourceCreator.setProperties(properties);
        defaultDataSourceCreator.setCreators(dataSourceCreators);
        return defaultDataSourceCreator;
    }

    //省略部分代码

    /**
     * 存在Hikari数据源时, 加入创建器
     */
    @ConditionalOnClass(HikariDataSource.class)
    @Configuration
    public class HikariDataSourceCreatorConfiguration {
        @Bean
        @Order(HIKARI_ORDER)
        @ConditionalOnMissingBean
        public HikariDataSourceCreator hikariDataSourceCreator() {
            return new HikariDataSourceCreator(properties.getHikari());
        }
    }

    //省略部分代码

}

当Spring 容器注入 DefaultDataSourceCreator 实例后 ,接下来就被 DynamicDataSourceProvider 这个类所使用。

2.3 DynamicDataSourceProvider 分析

@Slf4j
@AllArgsConstructor
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {

    /**
     * 所有数据源
     */
    private final Map<String, DataSourceProperty> dataSourcePropertiesMap;

    //通过构造函数注入所有的 数据源 然后调用该父类方法创建数据源集合
    @Override
    public Map<String, DataSource> loadDataSources() {
        return createDataSourceMap(dataSourcePropertiesMap);
    }
}
@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {

    //从Spring 容器中获取注入好的 DefaultDataSourceCreator 
    @Autowired
    private DefaultDataSourceCreator defaultDataSourceCreator;

    //创建数据源集合
    protected Map<String, DataSource> createDataSourceMap(
            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            DataSourceProperty dataSourceProperty = item.getValue();
            String poolName = dataSourceProperty.getPoolName();
            if (poolName == null || "".equals(poolName)) {
                poolName = item.getKey();
            }
            dataSourceProperty.setPoolName(poolName);
            dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }
}

2.4 DynamicDataSourceAnnotationAdvisor 分析

这个其实就是Spring AOP的切面配置器 主要代码如下

public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    //切面增强方法
    private final Advice advice;

    private final Pointcut pointcut;

   //构造方法注入
    public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
        this.advice = dynamicDataSourceAnnotationInterceptor;
        this.pointcut = buildPointcut();
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    
    @Override
    public Advice getAdvice() {
        return this.advice;
    }

    //省略部分代码
    
    
    //当有类或者方法中有 DS.class 注解时 进行 切面增强
    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
        Pointcut mpc = new AnnotationMethodPoint(DS.class);
        return new ComposablePointcut(cpc).union(mpc);
    }

    //省略部分代码 
}

2.5 DynamicDataSourceAnnotationInterceptor 分析

该类为切面增强,即当上面的DynamicDataSourceAnnotationAdvisor 拦截到类或者方法中有 DS.class 注解时 ,调用该增强类进行处理

public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    /**
     * The identification of SPEL.
     */
    private static final String DYNAMIC_PREFIX = "#";

    private final DataSourceClassResolver dataSourceClassResolver;
    private final DsProcessor dsProcessor;

    public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
        dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
        this.dsProcessor = dsProcessor;
    }

    //AOP拦截后进行 切面增强方法
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //选择数据源
        String dsKey = determineDatasourceKey(invocation);
        //使用基于ThreadLocal的实现切换数据源
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    //通过调用 DsProcessor 来链式调用进行 数据源的确认
    private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }
}

2.6 DefaultPointcutAdvisor 分析

该切面增强为事务增强,设置此增强类后,不能与Spring 源事务或者 @Transactional 注解共用。

@Slf4j
public class DynamicLocalTransactionAdvisor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
            return methodInvocation.proceed();
        }
        boolean state = true;
        Object o;
        String xid = UUID.randomUUID().toString();
        TransactionContext.bind(xid);
        try {
            o = methodInvocation.proceed();
        } catch (Exception e) {
            state = false;
            throw e;
        } finally {
            ConnectionFactory.notify(state);
            TransactionContext.remove();
        }
        return o;
    }
}

2.7 DynamicDataSourceContextHolder 核心切换类

public final class DynamicDataSourceContextHolder {

    /**
     * 为什么要用链表存储(准确的是栈)
     * <pre>
     * 为了支持嵌套切换,如ABC三个service都是不同的数据源
     * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
     * 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
     * </pre>
     */
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        @Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };

    private DynamicDataSourceContextHolder() {
    }

    /**
     * 获得当前线程数据源
     *
     * @return 数据源名称
     */
    public static String peek() {
        return LOOKUP_KEY_HOLDER.get().peek();
    }

    /**
     * 设置当前线程数据源
     * <p>
     * 如非必要不要手动调用,调用后确保最终清除
     * </p>
     *
     * @param ds 数据源名称
     */
    public static String push(String ds) {
        String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
        LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
        return dataSourceStr;
    }

    /**
     * 清空当前线程数据源
     * <p>
     * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
     * </p>
     */
    public static void poll() {
        Deque<String> deque = LOOKUP_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        }
    }

    /**
     * 强制清空本地线程
     * <p>
     * 防止内存泄漏,如手动调用了push可调用此方法确保清除
     * </p>
     */
    public static void clear() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

大致核心的代码已经介绍完了,接下来我们逐步debugger,摸清其执行流程。

2.8 数据源切换执行流程

现在当我们执行上面的SpringBoot demo中的 调用注解 @DS("db2") 的 Mapper 查询数据库时,他的顺序如下(只给出涉及到该组件的相关类)

  1. ClassMapper#selectList() : 执行Mybatis查询操作

  1. DynamicDataSourceAnnotationInterceptor#invoke() : Spring AOP 拦截到带有 @DS("db2") 并执行代理增强操作

  1. DataSourceClassResolver#findDSKey() : 查找有注解@DS() 的 类或方法,获取对应的数据源Key 值 也就是 db2。

  1. DynamicDataSourceContextHolder#push() : 设置当前线程数据源

  1. DynamicRoutingDataSource#getConnection(): 调用父类方法获取数据库连接 这里两种处理方式 如下所示

    public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        //无事务时 即当前操作为 查询
        if (StringUtils.isEmpty(xid)) {
            return determineDataSource().getConnection();
        } else {
            //有事物时 ,先从 之前DynamicDataSourceContextHolder 中获取数据源 先进先出原则
            String ds = DynamicDataSourceContextHolder.peek();
            ds = StringUtils.isEmpty(ds) ? "default" : ds;
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
             //创建数据源
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
        }
    }
  1. DynamicRoutingDataSource#getDataSource 设置数据源

 public DataSource getDataSource(String ds) {
        //如果当前无 数据源声明 则使用默认数据源
        if (StringUtils.isEmpty(ds)) {
            return determinePrimaryDataSource();
        } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return groupDataSources.get(ds).determineDataSource();
        } else if (dataSourceMap.containsKey(ds)) {
            //如果当前存在数据源则取出该数据源返回
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return dataSourceMap.get(ds);
        }
        if (strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        }
        return determinePrimaryDataSource();
    }
  1. 执行剩余的数据库操作至结束。

3 小结

大体上写的略微混乱,但是只要我们知道其自动装配时 ,实例化了哪些Bean,并且知道这些Bean 是干什么的 ,合适调用的,根据执行流程逐步Debugger调试,就可以明白dynamic-datasource组件是如何进行数据源切换的,在流程中我认为比较经典也是比较核心的地方已经标注出源码。我们可以借鉴 DynamicDataSourceContextHolder 这个公共类的思想,扩展和优化我们现有的项目中某些跨资源调用的问题。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月13日 0

暂无评论

推荐阅读
TEZNKK3IfmPf