【SpringBoot】通过数据库保存额外数据源信息,做数据源的动态切换

Java,SpringBoot实现动态切换数据源,数据源信息来源:yml+MySQL。动态加载时机自定义。切换时机方法调用前后,各线程分别切换。

别的动态切换已经很多了。我这里的需求是用户可选择数据源。

不废话,上代码:

首先application.yml中配置主数据库,配置通用即可。没有影响。这个主数据库中包换了其他数据源的配置信息。需要保存的信息如下:

连接方式可选择默认hikariCP,可以选择其他连接如druid,其他连接方式构造连接对象的代码需自行查找。

禁用SpringBoot的数据源自动配置

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

因为我们需要一个@Configration,用于在继承SpringBoot默认连接对象前,显式的加载配置,这样我们才可以直接使用这个连接对象做些事。

@Configuration
public class DynamicDataSourceConfig {
    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    // 加载默认连接对象
    @Bean
    public DataSource firstDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.setDriverClassName(driverClassName);
        return new HikariDataSource(config);
    }
    // 如果其他连接对象是固定,也可以通过类似默认对象一样的方式全部加载进来
    ...
    
    // 加载连接池,将默认连接对象初始化进去
    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource) {
        return new DynamicDataSource(firstDataSource);
    }
}

和一个继承了AbstractRoutingDataSource的类,在其中操作连接对象,包括默认连接对象,以及我们要加载的其他连接对象。

// 连接池操作
// 首先提供一个初始化方法,加默认配置显示的加载进来,重点是显示的保存这个连接池对象,方便后续操作切换
// 其次覆盖determineCurrentLookupKey方法,使我们可随时切换连接对象
public class DynamicDataSource extends AbstractRoutingDataSource {

    // 连接池对象
    private final Map<Object, Object> targetDataSourceMap;

    // 加载默认连接,并将连接池对象保存到这里
    public DynamicDataSource(Object firstDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", firstDataSource);
        // 设置默认连接
        super.setDefaultTargetDataSource(firstDataSource);
        // 设置连接池
        super.setTargetDataSources(targetDataSources);
        // 刷新配置,使连接池生效
        super.afterPropertiesSet();

        this.targetDataSourceMap = targetDataSources;
    }

    // 从当前线程参数中获取要切换的数据源并设置到连接池中
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource() != null ? DataSourceContextHolder.getDataSource() : "master";
    }

    // 添加数据源
    // 需要注意的是,该方法的调用时机,否则可能出现还没有添加数据源,就尝试切换数据源的问题
    public void addDataSource(List<DataSourceDto> dataSources) {
        if (dataSources != null && !dataSources.isEmpty()) {
            for (int i = 0; i < dataSources.size(); i++) {

                String key = "slave-" + dataSources.get(i).getId();
                if (!dataSources.contains(key)) {
                    HikariConfig config = new HikariConfig();
                    config.setJdbcUrl(dataSources.get(i).getUrl());
                    config.setUsername(dataSources.get(i).getUsername());
                    config.setPassword(dataSources.get(i).getPassword());
                    config.setDriverClassName(dataSources.get(i).getDriverClassName());
                    targetDataSourceMap.put(key, new HikariDataSource(config));
                    // 添加后刷新配置,也可以全部更新后一起刷新
                    super.afterPropertiesSet();
                }
            }
        }
    }
}

每个线程需要独立持有当前线程使用的连接对象的key。因此需要一个线程对象操作类。这里和其他的动态数据源切换中的持有方式都是一摸一样的。

// 设置当前线程数据源连接对象持有对象,每个线程独立使用对应的连接对象,只是保存作用,需要连接池自行取用
public class DataSourceContextHolder {
    // 线程内容
    private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源
     * @param dataSourceName 数据源名称
     */
    public static void setDataSource(String dataSourceName){
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /**
     * 获取当前线程的数据源
     * @return 数据源名称
     */
    public static String getDataSource(){
        return DATASOURCE_HOLDER.get();
    }

    /**
     * 删除当前数据源
     */
    public static void removeDataSource(){
        DATASOURCE_HOLDER.remove();
    }
}

 切换方法可以直接调用,也可以以注解的方式在需要切换数据源的方法前后切换数据源,在需要切换的方法上使用注解@DS即可。以用户选择使用数据源的情况为例,注解需要写为根据参数设置数据源:

// 使用有多种方式,可以主动调用DataSourceContextHolder的设置方法,也可以使用注解的方式解耦代码
@Aspect
@Order(1)
@Component
public class DBAspect {

    @Pointcut("@annotation(com.md.db.annotation.DS)")
    public void pointcut() {};

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint jp) throws Throwable {
        Object[] args = jp.getArgs();
        String[] paramNames = ((CodeSignature)jp.getSignature()).getParameterNames();

        // 方法调用前切换数据源
        for (int i = 0; i < paramNames.length; i++) {
            if ("dsId".equals(paramNames[i])) {
                DataSourceContextHolder.setDataSource("slave-" + args[i]);
                break;
            }
        }

        try {
            return jp.proceed();
        } finally {
            // 方法调用后返回默认数据源,否则会影响不需要切换数据源而没有加注解的方法
            DataSourceContextHolder.removeDataSource();
        }
    }
}

其他情况自行修改代码。 

至于动态添加数据库的时机,我会在项目启动后去添加动态连接对象。也可以在前端用户到了数据源切换页面发送请求后再加载数据源,只要保证在其他需要切换数据源的方法前调用即可。

    @Resource
    private DynamicDataSource dynamicDataSource;

    // 添加数据源
    List<DataSourceDto> sourceList = this.dbMapper.getSourceList();
    this.dynamicDataSource.addDataSource(sourceList);
当然,以上方法仅是demo,实际工作中还有许多问题,比如动态数据源删除,更新等等。就自行思考修改吧。
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot中,我们可以通过注解实现多数据源切换。具体步骤如下: 1. 配置多个数据源 在application.properties文件中配置多个数据源,例如: ``` # 主数据源 spring.datasource.url=jdbc:mysql://localhost:3306/db1 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver # 从数据源 spring.datasource.secondary.url=jdbc:mysql://localhost:3306/db2 spring.datasource.secondary.username=root spring.datasource.secondary.password=123456 spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver ``` 2. 创建数据源配置类 创建两个数据源配置类,用于配置不同的数据源,例如: ``` @Configuration @MapperScan(basePackages = "com.example.demo.mapper.primary", sqlSessionTemplateRef = "primarySqlSessionTemplate") public class PrimaryDataSourceConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "primarySqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Bean(name = "primarySqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } @Configuration @MapperScan(basePackages = "com.example.demo.mapper.secondary", sqlSessionTemplateRef = "secondarySqlSessionTemplate") public class SecondaryDataSourceConfig { @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondarySqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Bean(name = "secondarySqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate(@Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } ``` 其中,@MapperScan注解用于扫描Mapper接口,sqlSessionTemplateRef属性指定使用的SqlSessionTemplate。 3. 创建数据源切换注解 创建一个数据源切换注解,用于动态切换数据源,例如: ``` @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String value() default "primary"; } ``` 其中,value属性指定使用的数据源,默认为主数据源。 4. 创建数据源切换切面 创建一个数据源切换切面,用于根据注解动态切换数据源,例如: ``` @Aspect @Component public class DataSourceAspect { @Pointcut("@annotation(com.example.demo.annotation.DataSource)") public void dataSourcePointCut() { } @Before("dataSourcePointCut()") public void before(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class); if (dataSource != null) { DataSourceContextHolder.setDataSource(dataSource.value()); } } @After("dataSourcePointCut()") public void after(JoinPoint joinPoint) { DataSourceContextHolder.clearDataSource(); } } ``` 其中,@Pointcut注解用于定义切点,@Before注解用于在切点方法执行之前切换数据源,@After注解用于在切点方法执行之后清除数据源。 5. 创建数据源上下文 创建一个数据源上下文,用于保存当前线程使用的数据源,例如: ``` public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } public static String getDataSource() { return contextHolder.get(); } public static void clearDataSource() { contextHolder.remove(); } } ``` 6. 使用注解切换数据源 在需要切换数据源的方法或类上加上@DataSource注解,例如: ``` @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @DataSource("primary") public List<User> getPrimaryUsers() { return userMapper.selectAll(); } @Override @DataSource("secondary") public List<User> getSecondaryUsers() { return userMapper.selectAll(); } } ``` 在方法上加上@DataSource注解,指定使用的数据源。 以上就是通过注解实现多数据源切换的步骤。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值