SpringBoot整合多数据源,并支持动态新增与切换(详细教程)

    1springBootkafka,,;

    2SpringBoot线ThreadPoolTaskExecutor;

    3java后端接口API性能优化技巧

    4SpringBoot+MyBatis,,力。

一、概述

      在项目的开发过程中,遇到了需要从数据库中动态查询新的数据源信息并切换到该数据源做相应的查询操作,这样就产生了动态切换数据源的场景。为了能够灵活地指定具体的数据库,本文基于注解和AOP的方法实现多数据源自动切换。在使用过程中,只需要添加注解就可以使用,简单方便。(代码获取方式:见文章底部(开箱即用))

二、构建核心代码

2.1、AbstractRoutingDataSource构建

package com.wonders.dynamic;
import org.springframework.beans.factory.InitializingBean;import org.springframework.jdbc.datasource.AbstractDataSource;import org.springframework.jdbc.datasource.lookup.DataSourceLookup;import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;import org.springframework.lang.Nullable;import org.springframework.util.Assert;import org.springframework.util.CollectionUtils;
import javax.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;import java.util.Map;
/** * @Description: TODO:抽象类AbstractRoutingDataSource,实现动态数据源切换 * @Author: yyalin * @CreateDate: 2023/7/16 14:40 * @Version: V1.0 */public abstract class AbstractRoutingDataSource extends AbstractDataSource        implements InitializingBean {    //目标数据源map集合,存储将要切换的多数据源bean信息    @Nullable    private Map    //未指定数据源时的默认数据源对象    @Nullable    private Object defaultTargetDataSource;    private boolean lenientFallback = true;    //数据源查找接口,通过该接口的getDataSource(String dataSourceName)获取数据源信息    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();    //解析targetDataSources之后的DataSource的map集合    @Nullable    private Map    @Nullable    private DataSource resolvedDefaultDataSource;
    //将targetDataSources的内容转化一下放到resolvedDataSources中,将defaultTargetDataSource转为DataSource赋值给resolvedDefaultDataSource    public void afterPropertiesSet() {        //如果目标数据源为空,会抛出异常,在系统配置时应至少传入一个数据源        if (this.targetDataSources == null) {            throw new IllegalArgumentException("Property 'targetDataSources' is required");        } else {            //初始化resolvedDataSources的大小            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());            //遍历目标数据源信息map集合,对其中的key,value进行解析            this.targetDataSources.forEach((key, value) -> {                //resolveSpecifiedLookupKey方法没有做任何处理,只是将key继续返回                Object lookupKey = this.resolveSpecifiedLookupKey(key);                //将目标数据源map集合中的value值(德鲁伊数据源信息)转为DataSource类型                DataSource dataSource = this.resolveSpecifiedDataSource(value);                //将解析之后的key,value放入resolvedDataSources集合中                this.resolvedDataSources.put(lookupKey, dataSource);            });            if (this.defaultTargetDataSource != null) {                //将默认目标数据源信息解析并赋值给resolvedDefaultDataSource                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);            }
        }    }
    protected Object resolveSpecifiedLookupKey(Object lookupKey) {        return lookupKey;    }
    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {        if (dataSource instanceof DataSource) {            return (DataSource)dataSource;        } else if (dataSource instanceof String) {            return this.dataSourceLookup.getDataSource((String)dataSource);        } else {            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);        }    }
    //因为AbstractRoutingDataSource继承AbstractDataSource,而AbstractDataSource实现了DataSource接口,所有存在获取数据源连接的方法    public Connection getConnection() throws SQLException {        return this.determineTargetDataSource().getConnection();    }
    public Connection getConnection(String username, String password) throws SQLException {        return this.determineTargetDataSource().getConnection(username, password);    }
    protected DataSource determineTargetDataSource() {        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");        //调用实现类中重写的determineCurrentLookupKey方法拿到当前线程要使用的数据源的名称        Object lookupKey = this.determineCurrentLookupKey();        //去解析之后的数据源信息集合中查询该数据源是否存在,如果没有拿到则使用默认数据源resolvedDefaultDataSource        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {            dataSource = this.resolvedDefaultDataSource;        }
        if (dataSource == null) {            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");        } else {            return dataSource;        }    }
    @Nullable    protected abstract Object determineCurrentLookupKey();}

2.2、DynamicDataSource类

/** * @Description: TODO:动态数据源 * @Author: yyalin * @CreateDate: 2023/7/16 14:46 * @Version: V1.0 *//** * * 调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map, * 并将新的数据源信息添加到map中,并替换targetdatasources中的map * 切换数据源时可以使用@DataSource(value = "数据源名称"),或者DynamicDataSourceContextHolder.setContextKey("数据源名称") */@Data@AllArgsConstructor@NoArgsConstructorpublic class DynamicDataSource extends AbstractRoutingDataSource {    //备份所有数据源信息,    private Map defineTargetDataSources;
    /**     * 决定当前线程使用哪个数据源     */    @Override    protected Object determineCurrentLookupKey() {        return DynamicDataSourceHolder.getDynamicDataSourceKey();    }
}

2.3、DynamicDataSourceHolder

/** * @Description: TODO:数据源切换处理 * DynamicDataSourceHolder类主要是设置当前线程的数据源名称, * 移除数据源名称,以及获取当前数据源的名称,便于动态切换 * @Author: yyalin * @CreateDate: 2023/7/16 14:51 * @Version: V1.0 */@Slf4jpublic class DynamicDataSourceHolder {    /**     * 保存动态数据源名称     */    private static final ThreadLocal DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
    /**     * 设置/切换数据源,决定当前线程使用哪个数据源     */    public static void setDynamicDataSourceKey(String key){        log.info("数据源切换为:{}",key);        DYNAMIC_DATASOURCE_KEY.set(key);    }
    /**     * 获取动态数据源名称,默认使用mater数据源     */    public static String getDynamicDataSourceKey(){        String key = DYNAMIC_DATASOURCE_KEY.get();        return key == null ? DbsConstant.mysql_db_01 : key;    }
    /**     * 移除当前数据源     */    public static void removeDynamicDataSourceKey(){        log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());        DYNAMIC_DATASOURCE_KEY.remove();    }
}

2.4、数据源工具类

/** * @Description: TODO:数据源工具类 * @Author: yyalin * @CreateDate: 2023/7/16 15:00 * @Version: V1.0 */@Slf4j@Componentpublic class DataSourceUtils {    @Resource    DynamicDataSource dynamicDataSource;
    /**     * @Description: 根据传递的数据源信息测试数据库连接     * @Author zhangyu     */    public DruidDataSource createDataSourceConnection(DataSourceInfo dataSourceInfo) {        DruidDataSource druidDataSource = new DruidDataSource();        druidDataSource.setUrl(dataSourceInfo.getUrl());        druidDataSource.setUsername(dataSourceInfo.getUserName());        druidDataSource.setPassword(dataSourceInfo.getPassword());        druidDataSource.setDriverClassName(dataSourceInfo.getDriverClassName());        druidDataSource.setBreakAfterAcquireFailure(true);        druidDataSource.setConnectionErrorRetryAttempts(0);        try {            druidDataSource.getConnection(2000);            log.info("数据源连接成功");            return druidDataSource;        } catch (SQLException throwables) {            log.error("数据源 {} 连接失败,用户名:{},密码 {}",dataSourceInfo.getUrl(),dataSourceInfo.getUserName(),dataSourceInfo.getPassword());            return null;        }    }
    /**     * @Description: 将新增的数据源加入到备份数据源map中     * @Author zhangyu     */    public void addDefineDynamicDataSource(DruidDataSource druidDataSource, String dataSourceName){        Map        defineTargetDataSources.put(dataSourceName, druidDataSource);        dynamicDataSource.setTargetDataSources(defineTargetDataSources);        dynamicDataSource.afterPropertiesSet();    }

2.5、DynamicDataSourceConfig

/** * @Description: TODO:数据源信息配置类,读取数据源配置信息并注册成bean。 * @Author: yyalin * @CreateDate: 2023/7/16 14:54 * @Version: V1.0 */@Configuration@MapperScan("com.wonders.mapper")@Slf4jpublic class DynamicDataSourceConfig {    @Bean(name = DbsConstant.mysql_db_01)    @ConfigurationProperties("spring.datasource.mysqldb01")    public DataSource masterDataSource() {        log.info("数据源切换为:{}",DbsConstant.mysql_db_01);        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();        return dataSource;    }
    @Bean(name = DbsConstant.mysql_db_02)    @ConfigurationProperties("spring.datasource.mysqldb02")    public DataSource slaveDataSource() {        log.info("数据源切换为:{}",DbsConstant.mysql_db_02);        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();        return dataSource;    }

    @Bean(name = DbsConstant.oracle_db_01)    @ConfigurationProperties("spring.datasource.oracledb01")    public DataSource oracleDataSource() {        log.info("数据源切换为oracle:{}",DbsConstant.oracle_db_01);        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();        return dataSource;    }    @Bean    @Primary    public DynamicDataSource dynamicDataSource(){        Map        dataSourceMap.put(DbsConstant.mysql_db_01,masterDataSource());        dataSourceMap.put(DbsConstant.mysql_db_02,slaveDataSource());        dataSourceMap.put(DbsConstant.oracle_db_01,oracleDataSource());        //设置动态数据源        DynamicDataSource dynamicDataSource = new DynamicDataSource();        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());        dynamicDataSource.setTargetDataSources(dataSourceMap);        //将数据源信息备份在defineTargetDataSources中        dynamicDataSource.setDefineTargetDataSources(dataSourceMap);        return dynamicDataSource;    }
}

三、测试代码

/** * @Description: TODO * @Author: yyalin * @CreateDate: 2023/7/16 15:02 * @Version: V1.0 */@Slf4j@Api(tags="动态切换多数据源测试")@RestControllerpublic class TestController {    @Resource    DataSourceUtils dataSourceUtils;    @Autowired    private StudentMapper studentMapper;    @ApiOperation(value="动态切换多数据源测试", notes="test")    @GetMapping("/test")    public Map dynamicDataSourceTest(String id){        Map map = new HashMap<>();        //1、默认库中查询数据        Student student=studentMapper.selectById(id);        map.put("1、默认库中查询到的数据",student);        //2、指定库中查询的数据        DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);        Student student02=studentMapper.selectById(id);        map.put("2、指定库中查询的数据",student02);        //3、从数据库获取连接信息,然后获取数据        //模拟从数据库中获取的连接        DataSourceInfo dataSourceInfo = new DataSourceInfo(                "jdbc:mysql://127.0.0.1:3308/test02?useUnicode=true&characterEncoding=utf-8&useSSL=false",                 "root",                "root",                "mysqldb03",                "com.mysql.cj.jdbc.Driver");        map.put("dataSource",dataSourceInfo);        log.info("数据源信息:{}",dataSourceInfo);        //测试数据源连接        DruidDataSource druidDataSource = dataSourceUtils.createDataSourceConnection(dataSourceInfo);        if (Objects.nonNull(druidDataSource)){            //将新的数据源连接添加到目标数据源map中            dataSourceUtils.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getDatasourceKey());            //设置当前线程数据源名称-----代码形式            DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getDatasourceKey());            //在新的数据源中查询用户信息            Student student03=studentMapper.selectById(id);            map.put("3、动态数据源查询的数据",student03);            //关闭数据源连接            druidDataSource.close();        }        //4、指定oracle库中查询的数据        DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.oracle_db_01);        Student student04=studentMapper.selectById(id);        map.put("4、指定oracle库中查询的数据",student04);        return map;    }}

测试结果如下:

       从结果中可以明显的看出,通过切换不同的数据源,可以从不同的库中获取不同的数据,包括:常见库Mysql、oracle、sqlserver等数据库相互切换。也可以从数据库的某张表中获取连接信息,实现动态切换数据库。

四、使用注解方式切换数据源

      从上述TestController 中代码不难看出,若要想切换数据源需要在mapper调用之前调用:

DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);

不够简洁优雅,所以下面推荐使用注解的方式来动态进行数据源的切换。

4.1、创建注解类DataSource

/** * @Description: TODO:自定义多数据源切换注解 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 * @Author: yyalin * @CreateDate: 2023/7/17 14:00 * @Version: V1.0 */@Target({ ElementType.METHOD, ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface DataSource {    //切换数据源名称,默认mysql_db_01    public String value() default DbsConstant.mysql_db_01;}

4.2、创建切面DataSourceAspect类

/** * @Description: TODO:创建切面DataSourceAspect类 * @Author: yyalin * @CreateDate: 2023/7/17 14:03 * @Version: V1.0 */@Aspect@Componentpublic class DataSourceAspect {    // 设置DataSource注解的切点表达式    @Pointcut("@annotation(com.wonders.dynamic.DataSource)")    public void dynamicDataSourcePointCut(){}
    //环绕通知    @Around("dynamicDataSourcePointCut()")    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{        String key = getDefineAnnotation(joinPoint).value();        DynamicDataSourceHolder.setDynamicDataSourceKey(key);        try {            return joinPoint.proceed();        } finally {            DynamicDataSourceHolder.removeDynamicDataSourceKey();        }    }    /**     * 功能描述:先判断方法的注解,后判断类的注解,以方法的注解为准     * @MethodName: getDefineAnnotation     * @MethodParam: [joinPoint]     * @Return: com.wonders.dynamic.DataSource     * @Author: yyalin     * @CreateDate: 2023/7/17 14:09     */    private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();        DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);        if (Objects.nonNull(methodSignature)) {            return dataSourceAnnotation;        } else {            Class> dsClass = joinPoint.getTarget().getClass();            return dsClass.getAnnotation(DataSource.class);        }    }}

4.3、进行数据源切换

//@Mapper 与 启动类的@MapperScan({"com.example.demo.mapper"}) 二选一即可@Repositorypublic interface StudentMapper extends BaseMapper<Student> {    /**     * 功能描述:在mysql_db_01中查询数据     * @MethodName: findStudentById     * @MethodParam: [id]     * @Return: com.wonders.entity.Student     * @Author: yyalin     * @CreateDate: 2023/7/17 14:20     */    @DataSource(value = DbsConstant.oracle_db_01)    Student findStudentById(String id);}

或在service层

@Servicepublic class StudentServiceImpl implements StudentService{    @Autowired    private StudentMapper studentMapper;    //注解加在实现层才能生效    @DataSource(value = DbsConstant.mysql_db_01)    @Override    public Student findStudentById(String id) {        return studentMapper.selectById(id);    }}

4.3、测试效果

@ApiOperation(value="使用注解方式动态切换多数据源", notes="test02")    @GetMapping("/test02")    public Student test02(String id){        Student student=studentMapper.findStudentById(id);        return student;    }

–结果如下:


五、功能点

    1、使用注解的方式来动态进行数据源的切换;

2、支持动态新增新的数据源;

3、支持oraclemysql等常见数据库切换。

参考:

http://t.csdn.cn/KCW9r

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=14809,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?