SpringBoot+mybatisPlus + dynamic-datasource实现真正的动态切换数据源(附核心代码)

文章目录

  • 前言
  • 创建主库
  • 生成mapper等代码
  • 定义新数据源
  • 创建初始化runner类
  • 创建Mybatis配置类

前言

系统要调整为S A S S版实现多 租 户功能,首先想到的两个解决方案就是:
1、通过表字段隔离租户数据信息
2、通过分库来隔离租户数据(这种方案还是比较安全的)

方案最终确定为第二种实现。接下来就是要实现动 态 切 换 数 据 源,以满足不同租户访问自己的数据源了

创建主库

用来存储不同用户的key值对应的数据源信息

CREATE TABLE `tenant_datasource` (
  `id` int NOT NULL AUTO_INCREMENT,
  `tenant_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户key',
  `tenant_type` int DEFAULT '0' COMMENT '数据库类型',
  `url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库连接URL',
  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库连接用户名',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数据库连接密码',
  `active` bit(1) DEFAULT b'1' COMMENT '数据是否有效',
  `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_uid` (`tenant_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

生成mapper等代码

我之前发不过去一个开源的自动生成代码工具
https://gitee.com/yuxuntoo/yuxuntoo_generator
自行下载,生成代码,可选择生成的mybatis或者是mybatisPlus都可以。

定义新数据源

定一个新的数据源继承一个抽象类AbstractRoutingDataSource。
其中,必须实现一个方法protected Object determineCurrentLookupKey(),
必须实现其方法动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key,通过key在resolvedDataSources这个map中获取对应的数据源,resolvedDataSources的值是由afterPropertiesSet()这个方法从TargetDataSources获取的

package com.example.tenant.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.example.tenant.constant.GlobalConstant;
import com.example.tenant.entity.Datasource;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils;

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

/**
 * 自定义一个数据源继承AbstractRoutingDataSource
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
    /**
     * 用于保存租户key和数据源的映射关系,目标数据源map的拷贝
     */
    public Map copyTargetDataSources;

    /**
     * 动态数据源构造器
     * @param defaultDataSource 默认数据源
     * @param targetDataSource 目标数据源映射
     */
    public DynamicDataSource(DataSource defaultDataSource, Map targetDataSource){
        copyTargetDataSources = targetDataSource;
        super.setDefaultTargetDataSource(defaultDataSource);
        // 存放数据源的map
        super.setTargetDataSources(copyTargetDataSources);
        // afterPropertiesSet是负责解析成可用的目标数据源
        super.afterPropertiesSet();
    }

    /**
     * 必须实现其方法
     * 动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource
     * 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key
     * 通过key在resolvedDataSources这个map中获取对应的数据源,resolvedDataSources的值是由afterPropertiesSet()这个方法从
     * TargetDataSources获取的
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        logger.info("Current DataSource is [{}]", DataSourceContextHolder.getDBKey());
        return DataSourceContextHolder.getDBKey();
    }

    /**
     * 添加数据源到目标数据源map中
     * @param datasource
     */
    public void addDataSource(Datasource datasource) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(datasource.getUrl());
        druidDataSource.setUsername(datasource.getUsername());
        druidDataSource.setPassword(datasource.getPassword());
        // 将传入的数据源对象放入动态数据源类的静态map中,然后再讲静态map重新保存进动态数据源中
        copyTargetDataSources.put(datasource.getTenantKey(), druidDataSource);
        super.setTargetDataSources(copyTargetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 是否存在租户key
     *
     * @param tenantKey 租户key
     */
    public Boolean existTenantKey(String tenantKey) {
        if (StringUtils.isEmpty(tenantKey)) {
            return false;
        }
        return copyTargetDataSources.containsKey(tenantKey);
    }

    /**
     * 切换租户
     *
     * @param tenantKey 租户key
     */
    public void switchTenant(String tenantKey) {
        logger.info("切换租户:{},当前线程名称:{}", DataSourceContextHolder.getDBKey(), Thread.currentThread().getName());
        DataSourceContextHolder.setDBKey(tenantKey);
    }

    /**
     * 切换到默认租户信息
     */
    public void switchDefaultTenant() {
        if (GlobalConstant.TENANT_CONFIG_KEY.equals(DataSourceContextHolder.getDBKey())) {
            return;
        }
        switchTenant(GlobalConstant.TENANT_CONFIG_KEY);
    }

}

创建初始化runner类

初始化类继承ApplicationRunner,将需要切换的数据源全部通过DynamicDataSource,拿到我们的缓存中

springBoot项目启动时,若想在启动之后直接执行某一段代码,就可以用 ApplicationRunner这个接口,并实现接口里面run(ApplicationArguments args)方法,方法中写上自己的想要的代码逻辑。

package com.example.tenant.init;

import com.example.tenant.config.DataSourceContextHolder;
import com.example.tenant.config.DynamicDataSource;
import com.example.tenant.entity.Datasource;
import com.example.tenant.mapper.DatasourceMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

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

/**
 * 初始化runner
 * @version 1.0
 * @user: Camel
 * @date: 2023/6/13 10:09
 * @description:   springBoot项目启动时,若想在启动之后直接执行某一段代码,就可以用 ApplicationRunner这个接口,
 *                  并实现接口里面的run(ApplicationArguments args)方法,方法中写上自己的想要的代码逻辑。
 */
@Component
@Order(value = 1)
public class InitializationRunner implements ApplicationRunner {
    private Logger logger = LoggerFactory.getLogger(InitializationRunner.class);
    @Resource
    private DatasourceMapper tenantDatasourceMapper;

    @Autowired
    private DynamicDataSource dynamicDataSource;

    @Override
    public void run(ApplicationArguments args) {
        // 租户端不进行服务调用
        logger.info("服务启动,初始化数据源,以供切换数据源使用");
        //切换默认数据源 即tenant库的数据源,用于查询tenant表中的所有tenant数据库配置
        DataSourceContextHolder.setDBKey("default");
        //设置所有数据源信息
        logger.info("获取当前数据源:" + DataSourceContextHolder.getDBKey());
        List tenantInfoList = tenantDatasourceMapper.selectList(null);
        for (Datasource info : tenantInfoList) {
            dynamicDataSource.addDataSource(info);
        }
        logger.info("初始化租户动态数据源已完成,共加载 【{}】 个", dynamicDataSource.copyTargetDataSources.size());
    }
}

创建Mybatis配置类

从中获取到yml文件的默认数据源,也就是我们保存租户及对应数据源的数据库表。并配置事务控制等bean

package com.example.tenant.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.example.tenant.constant.GlobalConstant;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

/**
 * Mybatis配置类,并且支持事务
 * @version 1.0
 * @user: Camel
 * @date: 2023/6/13 10:09
 * @description:   
 */
@Configuration
@MapperScan({"com.xxx.xxx.mapper"})
public class MybatisConfig {
    private Logger logger = LoggerFactory.getLogger(MybatisConfig.class);
    /**
     * 配置事务
     * @param dynamicDataSource
     * @return
     */
    @Bean
    @Qualifier("transactionManager")
    public PlatformTransactionManager txManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }

    /**
     * 配置文件yml中的默认数据源
     * @return
     */
    @Bean(name = "defaultDataSource")
    @ConfigurationProperties(prefix="spring.datasource")
    public DataSource getDefaultDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 将动态数据源对象放入spring中管理
     * @return
     */
    @Bean
    public DynamicDataSource dynamicDataSource() {

        Map targetDataSources = new HashMap<>();
        logger.info("将druid数据源放入默认动态数据源对象中");
        targetDataSources.put(GlobalConstant.TENANT_CONFIG_KEY, getDefaultDataSource());
        return new DynamicDataSource(getDefaultDataSource(), targetDataSources);
    }

    /**
     * 数据库连接会话工厂
     * @param dynamicDataSource 自定义动态数据源
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/**/*.xml"));
        return bean.getObject();
    }

    @Bean
    public GlobalConfig getGlobalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        //已删除
        dbConfig.setLogicDeleteValue("0");
        //未删除
        dbConfig.setLogicNotDeleteValue("1");
        dbConfig.setLogicDeleteField("active");
        globalConfig.setDbConfig(dbConfig);
        return globalConfig;
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

# 拦截器实现动态切换
通过拦截器,获取用户信息来完成动态数据源的切换工作

package com.example.tenant.config;

import com.example.tenant.interceptor.DynamicDatasourceInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 数据源拦截器
 * @version 1.0
 * @user: Camel
 * @date: 2023/6/13 10:07
 * @description:
 */
@Configuration
public class DynamicDatasourceInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 不需要拦截的接口路径
        String[] excludePath = {};
        // 如果拦截全部可以设置为 /**,example:"/sys/role/**"
        String[] path = {};

        DynamicDatasourceInterceptor dynamicDatasourceInterceptor = new DynamicDatasourceInterceptor();

        registry.addInterceptor(dynamicDatasourceInterceptor).addPathPatterns(path).excludePathPatterns(excludePath);
    }
}

好了,欢迎大家点击下方卡片,关注《coder练习生》

你可能感兴趣的:(mysql,spring,boot,后端,java)