【springboot】实现动态数据源(在线切换、新增、删除、编辑)的方案,拒绝多数据源硬编码方案

1. 前言

思路:想要实现灵活的动态数据源,我们肯定不能如传统动态数据源一样将数据源配置文件中,更加灵活的方式是基于数据库中数据源配置的持久化。

需改进点:在 2.7. DBChangeService 中获取所有数据源列表,并从数据源列表中过滤出目标数据源源的过程存在性能损耗,性能损耗点在于 获取数据源列表 的过程,我本地实际是从数据源查询的,更合理的方式应该是直接存储到内存中,避免网关开销。

说明
1、我在写本文章时,并没有做过多的文字说,但是代码中有非常详细的代码注解
2、如下代码省略了对 os_datasourceentity映射mapper查询,但是核心代码没有少一点。原项目中是做了缓存,链接如下: 查看缓存逻辑
3、本文中并没有给出前端示例,完整项目请查看如下链接:查看完整项目

2. 源代码

2.1. 表结构
CREATE TABLE `os_datasource` (
  `id` varchar(20) NOT NULL COMMENT '主键',
  `name` varchar(255) DEFAULT NULL COMMENT '数据源名称',
  `drive` varchar(100) DEFAULT NULL COMMENT '驱动',
  `url` varchar(500) DEFAULT NULL COMMENT '连接信息',
  `user_name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `database_type` varchar(255) DEFAULT NULL COMMENT '数据库类型',
  `code` varchar(255) DEFAULT NULL COMMENT '暂留字段',
  `del_flag` int DEFAULT '0' COMMENT '是否删除(1是 0否)',
  PRIMARY KEY (`id`)
) COMMENT='数据源';
2.2. Entity
@Data
public class DataSource {....
}
2.3. Mapper
@Repository
public interface DataSourceMapper {....
}
@Service
public class DataSourceServiceImpl implements DataSourceService {....
}
2.4. DruidDBConfig

该类用于配置主数据源

package com.os.core.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.os.core.datasource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableTransactionManagement
public class DruidDBConfig {
    /* adi数据库连接信息 */
    @Value("${spring.datasource.url}")
    private String dbUrl;
    @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.initial-size}")
    private int initialSize;
    @Value("${spring.datasource.min-idle}")
    private int minIdle;
    @Value("${spring.datasource.max-active}")
    private int maxActive;
    @Value("${spring.datasource.max-wait}")
    private int maxWait;
    @Value("${spring.datasource.time-between-eviction-runs-millis}")
    private long timeBetweenEvictionRunsMillis;
    @Value("${spring.datasource.min-evictable-idle-time-millis}")
    private long minEvictableIdleTimeMillis;
    @Value("${spring.datasource.max-evictable-idle-time-millis}")
    private long maxEvictableIdleTimeMillis;
    @Value("${spring.datasource.validation-query}")
    private String validationQuery;
    @Value("${spring.datasource.test-while-idle}")
    private boolean testWhileIdle;
    @Value("${spring.datasource.test-on-borrow}")
    private boolean testOnBorrow;
    @Value("${spring.datasource.test-on-return}")
    private boolean testOnReturn;
    @Value("${spring.datasource.filters}")
    private String filters;

    private String mybatisConfig = "classpath:mybatis/mybatis-config.xml";
    private String mapperXmlPath = "classpath*:mapper/**/*Mapper.xml";

    /**
     * 个性化配置数据源
     */
    public DataSource dataSource() throws SQLException {
        DruidDataSource datasource = new DruidDataSource();
        /* 基础连接信息 */
        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);
        /* 连接池连接信息 */
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);
        datasource.setMaxWait(maxWait);
        /* 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 */
        datasource.setPoolPreparedStatements(true);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(20);
        /* 对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒 */
        datasource.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slowSqlMillis=15000");
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        /* 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,
           执行validationQuery检测连接是否有效。
        */
        datasource.setTestOnReturn(testOnReturn);
        /* 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、
           testWhileIdle都不会起作用。
        */
        datasource.setValidationQuery(validationQuery);
        /* 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall */
        datasource.setFilters(filters);
        /* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        /* 配置一个连接在池中最小生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
        /* 打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,
           则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,
           只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断
        */
        datasource.setKeepAlive(true);
        /* 是否移除泄露的连接/超过时间限制是否回收。 */
        datasource.setRemoveAbandoned(true);
        /* 泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时 */
        datasource.setRemoveAbandonedTimeout(3600);
        /* 移除泄露连接发生是是否记录日志 */
        datasource.setLogAbandoned(true);
        return datasource;
    }

    /**
     * 配置动态数据源。提示:DynamicDataSource 是 DataSource 的子类。
     */
    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource() throws SQLException {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        /* 设置默认数据源配置 */
        dynamicDataSource.setDefaultTargetDataSource(dataSource());
        /* 必须设置targetDataSource,因为在AbstractRoutingDataSource的afterPropertiesSet()中会判断targetDataSource是否为空*/
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put("mainDataSource", dataSource());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }

    /**
     * 配置SqlSessionFactory
     * @param dynamicDataSource 动态数据源bean
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 这里一定要注意使用的是我们自定义个的DynamicDataSource,如果不是,则切换数据源会失效
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        /* 设置mybatis的主配置文件 */
        sqlSessionFactoryBean.setConfigLocation(resolver.getResource(mybatisConfig));
        /* 手动配置mybatis的mapper.xml资源路径 */
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperXmlPath));
        return sqlSessionFactoryBean.getObject();
    }
}
2.5. DynamicDataSource

该类用于实现动态数据源的创建、检查、删除、替换等操作。

package com.os.core.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.stat.DruidDataSourceStatManager;
import com.os.common.entity.datasource.DataSource;
import com.os.common.exception.DataSourceConfigException;
import com.os.common.exception.DataSourceCreateException;
import com.os.common.exception.DataSourceNotFoundException;
import com.os.common.utils.MyUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class DynamicDataSource extends AbstractRoutingDataSource {
    private final Logger log = LoggerFactory.getLogger(getClass());
    private Map<Object, Object> dynamicTargetDataSources;
    private Object dynamicDefaultTargetDataSource;
    /* 连接池连接信息 */
    @Value("${spring.datasource.initial-size}")
    private int initialSize;
    @Value("${spring.datasource.min-idle}")
    private int minIdle;
    @Value("${spring.datasource.max-active}")
    private int maxActive;
    @Value("${spring.datasource.max-wait}")
    private int maxWait;
    @Value("${spring.datasource.time-between-eviction-runs-millis}")
    private long timeBetweenEvictionRunsMillis;
    @Value("${spring.datasource.min-evictable-idle-time-millis}")
    private long minEvictableIdleTimeMillis;
    @Value("${spring.datasource.max-evictable-idle-time-millis}")
    private long maxEvictableIdleTimeMillis;
    @Value("${spring.datasource.validation-query}")
    private String validationQuery;
    @Value("${spring.datasource.test-while-idle}")
    private boolean testWhileIdle;
    @Value("${spring.datasource.test-on-borrow}")
    private boolean testOnBorrow;
    @Value("${spring.datasource.test-on-return}")
    private boolean testOnReturn;
    @Value("${spring.datasource.filters}")
    private String filters;

    public void clearDSCache() {
        this.dynamicTargetDataSources = new HashMap<>();
    }

    /**
     * 检测并创建动态数据源:数据源不存在则创建,存在则创建
     * @param dataSource
     * @throws Exception
     */
    @Transactional
    public boolean createDataSourceWithCheck(DataSource dataSource) throws Exception {
        String datasourceId = String.valueOf(dataSource.getId());
        String datasourceName = dataSource.getName();
        log.info("正在检查数据源:[{}]", datasourceName);
        /* 备份目标数据源 */
        Map<Object, Object> dynamicTargetDataSourcesBak = this.dynamicTargetDataSources;
        /* 检查目标数据源是否存在 */
        if (dynamicTargetDataSourcesBak.containsKey(datasourceId)) {
            log.info("数据源[{}]之前已经创建,准备测试数据源是否正常", datasourceName);
            /* 获取数据源*/
            DruidDataSource druidDataSource = (DruidDataSource) dynamicTargetDataSourcesBak.get(datasourceId);
            /* 数据源连接测试 */
            boolean rightFlag = true;
            Connection connection = null;
            try {
                log.info("[{}]数据源的概况:{}个闲置连接, {}个活动连接", datasourceName, druidDataSource.getPoolingCount(),druidDataSource.getActiveCount());
                connection = druidDataSource.getConnection();
                log.info("数据源[{}]正常", datasourceName);
            } catch (Exception e) {
                rightFlag = false;
                log.error(e.getMessage());
                log.info("缓存数据源[{}]已失效,准备删除",datasourceName);
                if(deleteDataSources(datasourceId)) {
                    log.info("缓存数据源删除成功");
                } else {
                    log.error("缓存数据源删除失败");
                }
            } finally {
                if(null != connection) {
                    connection.close();
                }
            }
            if(rightFlag) {
                return true;
            } else {
                boolean res = createDataSource(dataSource);
                if (res) {
                    return true;
                } else {
                    return false;
                }
            }
        } else {
            boolean res = createDataSource(dataSource);
            if (res) {
                return true;
            } else {
                return false;
            }
        }

    }

    /**
     * 删除数据源
     * @param datasourceId
     * @return
     */
    private boolean deleteDataSources(String datasourceId) {
        Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
        if (dynamicTargetDataSources2.containsKey(datasourceId)) {
            Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
            for (DruidDataSource l : druidDataSourceInstances) {
                if (datasourceId.equals(l.getName())) {
                    dynamicTargetDataSources2.remove(datasourceId);
                    DruidDataSourceStatManager.removeDataSource(l);
                    /* 将map赋值给父类的TargetDataSources */
                    setTargetDataSources(dynamicTargetDataSources2);
                    /* 将TargetDataSources中的连接信息放入resolvedDataSources管理 */
                    super.afterPropertiesSet();
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 创建数据源
     * @param dataSource
     * @throws Exception
     */
    private boolean createDataSource(DataSource dataSource) throws Exception {
        String datasourceId = String.valueOf(dataSource.getId());
        String datasourceName = dataSource.getName();
        log.info("准备创建数据源:[{}]",datasourceName);
        String databaseType = dataSource.getDatabaseType();
        String userName = dataSource.getUserName();
        String password = dataSource.getPassword();
        String url = dataSource.getUrl();
        String driveClass = dataSource.getDrive();
        if(testDatasource(driveClass,url,userName,password)) {
            boolean result = this.createDataSource(datasourceId, driveClass, url, userName, password, databaseType,datasourceName);
            if(!result) {
                throw new DataSourceCreateException();
            }
            log.info("数据源[{}]创建成功",datasourceName);
            return true;
        } else {
            throw new DataSourceConfigException();
        }
    }

    /**
     * 测试数据源连接是否有效
     * @param driveClass
     * @param url
     * @param username
     * @param password
     * @return
     */
    private boolean testDatasource(String driveClass, String url, String username, String password) {
        try {
            Class.forName(driveClass);
            DriverManager.getConnection(url, username, password);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataSourceConfigException();
        }
    }

    /**
     * 创建数据源
     * @param key 数据源key
     * @param driveClass 驱动
     * @param url url
     * @param username 用户名
     * @param password 密码
     * @param databasetype 数据库类型
     * @param datasourceName 数据源名称
     */
    private boolean createDataSource(String key, String driveClass, String url, String username, String password, String databasetype,String datasourceName) {
        try {
            try { /* 排除连接不上的错误*/
                Class.forName(driveClass);
                DriverManager.getConnection(url, username, password);// 相当于连接数据库
            } catch (Exception e) {
                return false;
            }
            /* 创建数据源 */
            @SuppressWarnings("resource")
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setName(key);
            druidDataSource.setDriverClassName(driveClass);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            /* 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时*/
            druidDataSource.setInitialSize(initialSize);
            /* 最大连接池数量 */
            druidDataSource.setMaxActive(maxActive);
            /* 获取连接时最大等待时间,单位毫秒。当链接数已经达到了最大链接数的时候,应用如果还要获取链接就会出现等待的现象,等待链接释放并回到链接池,如果等待的时间过长就应该踢掉这个等待,不然应用很可能出现雪崩现象 */
            druidDataSource.setMaxWait(maxWait);
            /* 最小连接池数量 */
            druidDataSource.setMinIdle(minIdle);
            /* 申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用 */
            druidDataSource.setTestOnBorrow(testOnBorrow);
            /* 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
            druidDataSource.setTestWhileIdle(testWhileIdle);
            druidDataSource.setTestOnReturn(testOnReturn);
            /* 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */
            druidDataSource.setValidationQuery(validationQuery);
            /* 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall */
            druidDataSource.setFilters(filters);
            /* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
            druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
            /* 配置一个连接在池中最小生存的时间,单位是毫秒 */
            druidDataSource.setMinEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
            /* 打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断 */
            druidDataSource.setKeepAlive(true);
            /* 是否移除泄露的连接/超过时间限制是否回收。 */
            druidDataSource.setRemoveAbandoned(true);
            /* 泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时 */
            druidDataSource.setRemoveAbandonedTimeout(3600);
            /* 移除泄露连接发生是是否记录日志 */
            druidDataSource.setLogAbandoned(true);
            druidDataSource.init();
            this.dynamicTargetDataSources.put(key, druidDataSource);
            /* 将map赋值给父类的TargetDataSources */
            setTargetDataSources(this.dynamicTargetDataSources);
            log.info("数据源[{}]初始化成功",datasourceName);
            return true;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new DataSourceCreateException();
        }
    }

    /**
     * 设置当前要使用的数据源,key是数据源的唯一表示,value是数据源实体.
     * 通过 super.setTargetDataSources() 将当前已经存在的数据源map集,传递给 AbstractRoutingDataSource 的 targetDataSources
     *
     * 该方法是在动态数据源被初始化后被调用(在当前类中),不能说是在数据源被切换时被调用,以为数据源切换有两种情况:
     * 1. 数据源没有被初始化,那么会初始化数据源即createDataSource(),然后修改DBContextHolder
     * 2. 数据源已经被初始化,那么会直接修改DBContextHolder
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet(); // 利用afterPropertiesSet()将 targetDataSources 赋值给 resolvedDataSources
        this.dynamicTargetDataSources = targetDataSources;
    }

    /**
     * 设置默认数据源,默认数据源不需要key
     * 通过 super.setDefaultTargetDataSource() 将默认数据源,传递给 AbstractRoutingDataSource 的 defaultTargetDataSource
     *
     * 该方法是在DynamicDataSource被实例化过程中被调用(com.os.core.config.DruidDBConfig =>  dynamicDataSource())
     */
    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
    }

    /**
     * 该方法是实现动态数据源切换的核心方法。
     * 由上可知,我们已经创建数据源都是存储在map中的,determineCurrentLookupKey()的作用就是拿到目标数据源
     * 的key,从而获取到数据源。
     *
     * 该方法是在AbstractRoutingDataSource中被调用的。
     */
    @Override
    protected Object determineCurrentLookupKey() {
        /* 获取缓存中的key */
        String datasourceKey = DBContextHolder.getDataSource();
        /* 判断数据源 */
        if (!MyUtil.isEmpty(datasourceKey)) {
            Map<Object, Object> dynamicTargetDataSourcesBak = this.dynamicTargetDataSources;
            /* 判断缓存中的数据源与设置的目标数据源是否一致 */
            if (!dynamicTargetDataSourcesBak.containsKey(datasourceKey)) {
               throw new DataSourceNotFoundException();
            }
        } else {
            log.info("当前数据源:[默认数据源]");
        }
        return datasourceKey;
    }
}
2.6. DBContextHolder

该类用于记录动态数据源上下文信息。

package com.os.core.datasource;

import com.os.common.utils.MyUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class DBContextHolder {
    private static final Logger log = LoggerFactory.getLogger(DBContextHolder.class);
    // 对当前线程的操作-线程安全的
    private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
 
    // 调用此方法,切换数据源
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
        log.info("已完成数据源的切换");
    }
 
    // 获取数据源
    public static String getDataSource() {
        return contextHolder.get();
    }
 
    // 删除数据源
    public static void clearDataSource() {
        String dataSource = getDataSource();
        if (!MyUtil.isEmpty(dataSource)) {
            contextHolder.remove();
            log.info("已切换到主数据源");
        }
    }
}
2.7. DBChangeService

该类用于实现动态数据源的切换。

package com.os.core.datasource;

import com.os.common.entity.datasource.DataSource;
import com.os.common.exception.DataSourceNotFoundException;
import com.os.common.exception.ErrorException;
import com.os.common.utils.MyUtil;
import com.os.system.service.DataSourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLException;
import java.util.List;

@Service
public class DBChangeService {
    @Autowired
    DataSourceService dataSourceService;
    @Autowired
    DynamicDataSource dynamicDataSource;

    public List<DataSource> get() throws SQLException {
        return dataSourceService.selectList();
    }

    public void clearDSCache() {
        dynamicDataSource.clearDSCache();
    }
    /**
     * 切换到默认数据源
     */
    public void changeDefaultBD() {
        /* 默认切换到主数据源,进行整体资源的查找*/
        DBContextHolder.clearDataSource();
    }

    public boolean changeBD(String datasourceId) throws Exception {
        /* 默认切换到主数据源,进行整体资源的查找*/
        DBContextHolder.clearDataSource();
        /* 获取所有数据源 */
        List<DataSource> dataSourcesList;
        try {
            dataSourcesList = dataSourceService.selectList();
            if (MyUtil.isEmpty(dataSourcesList)) {
                throw new DataSourceNotFoundException();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new ErrorException();
        }
        /* 寻找目标数据源*/
        for (DataSource dataSource : dataSourcesList) {
            if (String.valueOf(dataSource.getId()).equals(datasourceId)) {
                /* 创建数据源 */
                boolean res = dynamicDataSource.createDataSourceWithCheck(dataSource);
                if (res) {
                    /* 设置缓存 */
                    DBContextHolder.setDataSource(String.valueOf(dataSource.getId()));
                    return true;
                } else {
                    return false;
                }
            }
        }
        throw new DataSourceNotFoundException();
    }
}
2.8. @DynamicDatasource

该注解用于标记数据源的切换入口,以及待切换的目标数据源。

package com.os.common.annotation.ds;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDatasource {
    String value() default "datasourceId";
}
2.9. DatasourceAspect

该类为动态数据源注解切面,其作用为完成数据源切换的实际入口(切换到目标数据源)和出口(切回到主数据源)位置。

package com.os.core.interceptor.ds;

import com.os.common.annotation.ds.DynamicDatasource;
import com.os.common.utils.MyUtil;
import com.os.core.datasource.DBChangeService;
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.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Aspect
@Order(-1) 
@Component
public class DatasourceAspect {
    @Autowired
    DBChangeService dbChangeService ;

    @Pointcut("@annotation(com.os.common.annotation.ds.DynamicDatasource)")
    public void DsPointcut() {

    }

    @Before("DsPointcut()")
    public void beforeMethod(JoinPoint joinPoint) throws Exception {
        Map<String, Object> map = parameterMap(joinPoint);
        DynamicDatasource changeDs = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(DynamicDatasource.class);
        Object o = map.get(changeDs.value());
        if (MyUtil.isEmpty(o)) {
            return;
        }
        dbChangeService.changeBD(o.toString());
    }

    @After("DsPointcut()")
    public void afterMethod(JoinPoint joinPoint) {
        dbChangeService.changeDefaultBD();
    }

    /**
     * 获取参数map
     * @param joinPoint 切点
     * @return 参数名-参数值的map
     */
    private Map<String, Object> parameterMap(JoinPoint joinPoint) {
        Object[] parameterValues = joinPoint.getArgs();
        String[] parameterNames = ((CodeSignature) (joinPoint.getSignature())).getParameterNames();
        HashMap<String, Object> map = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            map.put(parameterNames[i],parameterValues[i]);
        }
        return map;
    }
}

2.10. Demo
package com.os.core.service.base.impl;

import com.os.common.annotation.ds.DynamicDatasource;
import com.os.common.entity.table.ColumnDetails;
import com.os.common.entity.table.TableCreateSQL;
import com.os.common.entity.table.TableDesc;
import com.os.core.service.base.BaseInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BaseInfoServiceImpl implements BaseInfoService {
    @Autowired
    XxxMapper xxxMapper;

    /**
     * 基于动态数据源获取表基础信息列表列表
     * @param datasourceId 数据源ID  // 核心参数
     */
    @Override
    @DynamicDatasource  // 核心注解
    public List<TableDesc> selectList(String datasourceId) {
        return xxxMapper.selectList();
    }
}
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我来简单介绍一下Java中如何创建多数据源实现动态切换。 首先,我们需要在Spring Boot中引入多数据源的依赖,比如Druid或HikariCP等,这里以Druid为例。 在pom.xml中添加以下依赖: ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> ``` 接着,在application.yml中配置多个数据源: ```yml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver initial-size: 5 max-active: 10 max-wait: 3000 filters: stat,wall,log4j datasource-one: url: jdbc:mysql://localhost:3306/db_one username: root password: root datasource-two: url: jdbc:mysql://localhost:3306/db_two username: root password: root ``` 以上配置了两个数据源,分别为datasource-one和datasource-two。 接下来,我们需要创建多个数据源的配置类,用于动态创建数据源并进行切换。 ```java @Configuration public class DataSourceConfig { @Bean(name = "datasourceOne") @ConfigurationProperties(prefix = "spring.datasource.datasource-one") public DataSource dataSourceOne() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "datasourceTwo") @ConfigurationProperties(prefix = "spring.datasource.datasource-two") public DataSource dataSourceTwo() { return DruidDataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dataSource(@Qualifier("datasourceOne") DataSource dataSourceOne, @Qualifier("datasourceTwo") DataSource dataSourceTwo) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.DATASOURCE_ONE, dataSourceOne); targetDataSources.put(DataSourceType.DATASOURCE_TWO, dataSourceTwo); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(dataSourceOne); return dataSource; } } ``` 代码中定义了两个数据源,分别为datasourceOne和datasourceTwo,同时在DynamicDataSource中配置了这两个数据源,并设置了默认数据源为datasourceOne。 最后,我们需要创建一个数据源的枚举类,用于在切换数据源时进行标识。 ```java public enum DataSourceType { DATASOURCE_ONE, DATASOURCE_TWO } ``` 至此,多数据源的配置已经完成。在需要切换数据源的地方,我们只需要调用DynamicDataSource中的setDataSource方法即可。 ```java DynamicDataSource.setDataSource(DataSourceType.DATASOURCE_TWO); ``` 以上就是Java中如何创建多数据源实现动态切换的简单介绍,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值