springboot,多数据源切换

需求介绍:

        要求做一个平台,有其他第三方系统接入;每个系统有自己的数据源配置,通过调用平台接口,实现将数据保存到第三方自己的数据库中;

实现过程:

        1.在平台项目运行时,通过接口获取每个第三方系统的数据源;以key-value的形式保存到全局变量中;

        2.在调用接口的时候,会通过拦截器获取到每个请求中的第三方系统标识;

        3.根据标识来切换对应的数据源

一、提供一个公共类,保存第三方系统的数据源信息

public class ThreadSystemDataSourceInfo {
    //数据源类型:mongodb, mysql, es
    private String dataType;
    //连接
    private String url;
    //用户名
    private String username;
    //密码
    private String password;
    //用来作为key存储租户配置
    private String datasourceId;
}

二、提供一个接口,用来模拟查询第三方系统的数据源

public List listDataSourceInfo() {
    List list = new ArrayList<>();
    ThreadSystemDataSourceInfo info = new ThreadSystemDataSourceInfo();
    info.setDataType("mysql");
    info.setUrl("url");
    info.setUsername("root");
    info.setPassword("123456");
    info.setDatasourceId("1");

    ThreadSystemDataSourceInfo info1 = new ThreadSystemDataSourceInfo();
    info1.setDataType("mysql");
    info1.setUrl("url1");
    info1.setUsername("root");
    info1.setPassword("123456");
    info.setDatasourceId("2");

    list.add(info);
    list.add(info1);

    return list;
}

三、系统的开发

1、配置yml,系统要配置自己的数据库

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      connection-test-query: SELECT 'x' FROM DUAL
      pool-name: HikariPool
      maximum-pool-size: 20
      connection-timeout: 10000
      validation-timeout: 3000
      minimum-idle: 10
      idle-timeout: 30000
      max-lifetime: 600000

mybatis:
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true #打开驼峰

2、创建数据源对象ThreadSystemDataSource

数据库连接池用的是kicari

public class ThreadSystemDataSource extends AbstractRoutingDataSource {
    private boolean debug = true;
    private final Logger log = LoggerFactory.getLogger(getClass());
    private Map targetDataSources;
    private Object dynamicDefaultTargetDataSource;
    private static final String defaultClassDriver = "com.mysql.cj.jdbc.Driver";

    @Override
    protected Object determineCurrentLookupKey() {
        String datasourceId = MysqlContextHolder.getDataSourceId();
        if (!StringUtils.isEmpty(datasourceId)) {
            Map dynamicTargetDataSources2 = this.targetDataSources;
            if (dynamicTargetDataSources2.containsKey(datasourceId)) {
                log.info("---当前数据源:" + datasourceId + "---");
            } else {
                log.info("不存在的数据源:");
                return null;
//                    throw new ADIException("不存在的数据源:"+datasource,500);
            }
        } else {
            log.info("---当前数据源:默认数据源---");
        }

        return datasourceId;
    }

    // 创建数据源
    public boolean createDataSource(String key, String url, String username, String password) {
        try {
            String driveClass = this.defaultClassDriver;

            if (!testDatasource(url, username, password)) {
                log.error("数据源配置有错误,url:{},username:{},password:{}", url, username, password);
                return false;
            }

            HikariConfig hikariConfig = new HikariConfig();
            // 连接池连接信息
//            hikariConfig.setMinimumIdle(mininum);
//            hikariConfig.setMaximumPoolSize(maximum);
//            hikariConfig.setMaxLifetime(lifeTime);
//            hikariConfig.setConnectionTimeout(connectionTimeOut);
//            hikariConfig.setValidationTimeout(validationTimeOut);
//            hikariConfig.setIdleTimeout(idleTimeOut);
//            hikariConfig.setPoolName(poolName);
//            hikariConfig.setConnectionTestQuery(testQuery);
            // 基础连接信息
            hikariConfig.setJdbcUrl(url);
            hikariConfig.setUsername(username);
            hikariConfig.setPassword(password);
            hikariConfig.setDriverClassName(driveClass);
            HikariDataSource datasource = new HikariDataSource(hikariConfig);

            this.targetDataSources.put(key, datasource);

            // 将map赋值给父类的TargetDataSources
            setTargetDataSources(this.targetDataSources);
            // 将TargetDataSources中的连接信息放入resolvedDataSources管理
            super.afterPropertiesSet();
            log.info(key + "数据源初始化成功");

            return true;
        } catch (Exception e) {
            log.error(e + "");
            return false;
        }
    }


    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
    }
    
    @Override
    public void setTargetDataSources(Map targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        this.targetDataSources = targetDataSources;
    }


    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    
    public boolean testDatasource(String url, String username, String password) {
        try {
            String driveClass = this.defaultClassDriver;
            Class.forName(driveClass);
            DriverManager.getConnection(url, username, password);
            return true;
        } catch (Exception e) {
            return false;
        }
    }


    private void createDataSource(ThreadSystemDataSourceInfo dataSource) {
        String datasourceId = dataSource.getDatasourceId();
        log.info("准备创建数据源" + datasourceId);
        String username = dataSource.getUsername();
        String password = dataSource.getPassword();
        String url = dataSource.getUrl();

        boolean result = this.createDataSource(datasourceId, url, username, password);
        if (!result) {
            log.error("数据源" + datasourceId + "配置正确,但是创建失败");
        }
    }
}

3、创建配置类

@Configuration
public class HikariDBConfig {
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;
    // 连接池连接信息
    @Value("${spring.datasource.hikari.maximum-pool-size}")
    private int maximum;
    @Value("${spring.datasource.hikari.connection-timeout}")
    private int connectionTimeOut;
    @Value("${spring.datasource.hikari.validation-timeout}")
    private int validationTimeOut;
    @Value("${spring.datasource.hikari.idle-timeout}")
    private int idleTimeOut;
    @Value("${spring.datasource.hikari.max-lifetime}")
    private int lifeTime;
    @Value("${spring.datasource.hikari.pool-name}")
    private String poolName;
    @Value("${spring.datasource.hikari.connection-test-query}")
    private String testQuery;

    /**
     * 主数据源,信息从配置文件获得
     * @return
     * @throws SQLException
     */
    @Bean
    @Primary // 在同样的DataSource中,首先使用被标注的DataSource
    @Qualifier("mainDataSource")
    public DataSource dataSource() throws SQLException {
        HikariConfig hikariConfig = new HikariConfig();
        // 连接池连接信息
        hikariConfig.setMaximumPoolSize(maximum);
        hikariConfig.setMaxLifetime(lifeTime);
        hikariConfig.setConnectionTimeout(connectionTimeOut);
        hikariConfig.setValidationTimeout(validationTimeOut);
        hikariConfig.setIdleTimeout(idleTimeOut);
        hikariConfig.setPoolName(poolName);
        hikariConfig.setConnectionTestQuery(testQuery);
        // 基础连接信息
        hikariConfig.setJdbcUrl(this.url);
        hikariConfig.setUsername(username);
        hikariConfig.setPassword(password);
        hikariConfig.setDriverClassName(driverClassName);
        HikariDataSource datasource = new HikariDataSource(hikariConfig);

        return datasource;
    }

    /**
     * 动态获取的数据源,初始化的时候,默认只有主数据源
     * @return
     * @throws SQLException
     */
    @Bean(name = "threadSystemDataSource")
    @Qualifier("threadSystemDataSource")
    public ThreadSystemDataSource threadDataSource() throws SQLException {
        ThreadSystemDataSource threadDataSource = new ThreadSystemDataSource();
        threadDataSource.setDebug(false);
        //配置缺省的数据源
        // 默认数据源配置 DefaultTargetDataSource
        threadDataSource.setDefaultTargetDataSource(dataSource());
        Map targetDataSources = new HashMap();
        //额外数据源配置 TargetDataSources
        targetDataSources.put("mainDataSource", dataSource());
        threadDataSource.setTargetDataSources(targetDataSources);
        return tenantDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(threadDataSource());
        //解决手动创建数据源后字段到bean属性名驼峰命名转换失效的问题
        sqlSessionFactoryBean.setConfiguration(configuration());

        // 设置mybatis的主配置文件
         ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // Resource mybatisConfigXml = resolver.getResource("classpath:mybatis/mybatis-config.xml");
        //  sqlSessionFactoryBean.setConfigLocation(mybatisConfigXml);
        // 设置别名包
        //  sqlSessionFactoryBean.setTypeAliasesPackage("com.testdb.dbsource.pojo");

        // 手动配置mybatis的mapper.xml资源路径,如果单纯使用注解方式,不需要配置该行
         sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    @ConfigurationProperties(prefix = "mybatis.configuration")
    public org.apache.ibatis.session.Configuration configuration() {
        return new org.apache.ibatis.session.Configuration();
    }
}

4、上下文工具类

public class MysqlContextHolder {
    private final static Logger log = LoggerFactory.getLogger(MysqlContextHolder.class);
    // 对当前线程的操作-线程安全的
    private static final ThreadLocal contextHolder = new ThreadLocal();

    // 调用此方法,切换数据源
    public static void setDataSourceId(String dataSource) {
        contextHolder.set(dataSource);
        log.info("已切换到数据源:{}",dataSource);
    }

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

    // 删除数据源
    public static void clearDataSource() {
        contextHolder.remove();
        log.info("已切换到主数据源");
    }
}

5、存放数据源的工具类

public class ThreadSystemDataSourceLocalMapUtil {
    //存放初始化各个租户,保存的配置信息
    private static final Map dataSourceInfoMap = new ConcurrentHashMap<>();

    public static Map getDataSourceInfoMap() {
        return dataSourceInfoMap;
    }


    public static void putTenantDataSource(String key, ThreadSystemDataSourceInfo dataSourceInfo) {
        dataSourceInfoMap.put(key, dataSourceInfo);

    }


    public static TenantDataSourceInfo getDataSource(String key) {
        return dataSourceInfoMap.get(key);
    }


    public static String getTenantDataSourceType(String key){
        ThreadSystemDataSourceInfo dataSourceInfo = dataSourceInfoMap.get(key);
        if(dataSourceInfo!=null){
            return dataSourceInfo.getDataType();
        }
        return null;
    }
}

6、初始化第三方系统的数据源

@Component
public class InitDataSource implements CommandLineRunner {
    @Autowired
    private ITenantList iTenantList;

    @Autowired
    private ThreadSystemDataSource tenantDataSource;

    /**
     * 获取租户数据源配置,创建数据源
     * @param args
     * @throws Exception
     */
    @Override
    public void run(String... args) throws Exception {
        //获取租户数据源配置
        List dataSourceInfos = iTenantList.listDataSourceInfo();

        if(CollectionUtil.isNotEmpty(dataSourceInfos)){
            //初始化数据源
            for (ThreadSystemDataSourceInfo info : dataSourceInfos) 
                    tenantDataSource.createDataSource(info.getDatasourceId(), info.getUrl(), info.getUsername(), info.getPassword());

                ThreadSystemDataSourceLocalMapUtil.putTenantDataSource(info.getDatasourceId(), info);
            }
        }
    }
}

7、开始使用

@Service
public class TenantLogServiceForMysqlImpl {

    @Resource
    private SystemMapper systemMapper;

 
    public R saveOperLog(String tenantId) {
        if(ThreadSystemDataSourceLocalMapUtil.getDataSource(key) == null){
            //没有配置,则使用默认数据源
            MysqlContextHolder.clearDataSource();
        }else{
             //根据tenantId作为key,来切换数据源
             MysqlContextHolder.setDataSourceId(key);       
        }
        
        systemMapper.insertOperlog(operLog);
        return R.ok();
    }
}

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