spring boot 动态数据源切换 - spring boot dynamic data source

所谓动态数据源是指运行时动态的改变数据源,spring boot 默认可以配置一个数据源,要同时支持 mysql 和 oracle 都不太容易,而如果能动态切换数据源,自然也能同时存在数个数据源,且可以随意切换,本文是之前几篇博客的升华,即使用 aop 技术实现动态的修改运行时数据源。

如果你懒得看具体怎么实现,直接拿我的成果吧,在 pom.xml 里添加依赖:

<dependency>
    <groupId>top.kpromise</groupId>
    <artifactId>dynamic-data-source-spring-boot-starter</artifactId>
    <version>0.0.1</version>
</dependency>

如果遇到如下错误:

The bean 'dataSource' could not be registered. A bean with that name has already been defined in class path resource

直接在 application.properties 里新增:spring.main.allow-bean-definition-overriding=true  即可,另外,你还需要配置你额外的数据库,比如:

dynamic.datasource.name=data2
dynamic.datasource.data2.url=jdbc:mysql://localhost:3506/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
dynamic.datasource.data2.username=test
dynamic.datasource.data2.password=test
dynamic.datasource.data2.driver-class-name=com.mysql.cj.jdbc.Driver

这里,name 后面是 list,你可以写 data2,也可以写 data2, data3 等,多个的时候用逗号分割,下面的 dynamic.datasource.data2.xxx 是该数据库的配置信息,此处的 data2 也可以是你自定义的任意值。

然后在 mapper 文件里添加注解,比如:

@Repository
@DataSource(name = "data2")
public interface TestMapper {
    int deleteByPrimaryKey(Long id);

    int insert(Test record);

    int insertSelective(Test record);

    @DataSource(name = "test")
    Test selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(Test record);

    int updateByPrimaryKey(Test record);
}

其中,类上的注解是针对这个类里所有方法的,而方法上的注解可以覆盖类上的注解,如果配置如上,除了 selectByPrimaryKey 将使用 test 这个数据库外,其余都使用 data2,如果你没有配置 test 这个数据库将使用你默认的数据库。

毫无疑问,这个解决方案完美的兼容已有的代码,且通过 aop 技术实现,没有耦合,如果你删除 pom.xml 里的依赖,顶多只是 application.properties 里显示警告而已,对已有系统侵入最小。下面是具体的实现。

1、首先,application.properties 里的配置是动态的,即 dynamic.datasource.data2.url 等这里的 data2 你可以随意写,但是后面却只能跟 password 等,这个的具体实现请参考:深入理解 spring boot 自定义属性 的最后部分。

2、关于 aop 请查阅:spring aop - 面向切面编程  相关的文章。

3、关于如何 使用 idea 打包并上传 jar 包到 maven 中央仓库请参考:IDEA 打包并上传 jar 包 到 maven 中央仓库

之所以提以上3点,是因为本文是在其基础上实现的,或者说本文说的内容是在其基础上才能写的。其实,spring boot 提供了动态切换数据源的方法,即 AbstractRoutingDataSource 这个类。

AbstractRoutingDataSource

这个类集成自 AbstractDataSource,也是个抽象类,你必须得实现 determineCurrentLookupKey 这个方法,而这个方法的作用就是:返回你所要使用的数据源的 key 值。比如:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

然后你需要将数据库信息注册进去,之后在执行 sql 语句前,会调用 这里的 determineCurrentLookupKey,最终调用:DynamicDataSourceContextHolder.getDataSourceType(); 而 DynamicDataSourceContextHolder 源码如下:

import java.util.ArrayList;
import java.util.List;

class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    static List<String> dataSourceIds = new ArrayList<>();

    static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    static String getDataSourceType() {
        return contextHolder.get();
    }

    static void clearDataSourceType() {
        contextHolder.remove();
    }

    static boolean isContainsDataSource(String dataSourceId) {
        return dataSourceIds.contains(dataSourceId);
    }
}

现在,大致应该明白,当你手动把数据库配置信息注册到 spring 后,在执行 sql 前,会调用 这里的 DynamicDataSourceContextHolder.getDataSourceType(); 以决定连接那个数据库,而 这个方法的值其实是 setDataSourceType 这个方法设置的,setDataSourceType 这个方法 和 clearDataSourceType 这个方法则是开放给 aop 层调用的,aop 层的代码如下:

import lombok.extern.slf4j.Slf4j;
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.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
@Order(-10)
public class DynamicDataSourceAspect {

    @Pointcut("@within(DataSource)")
    public void dataSource() {
    }

    private DataSource findDataSource(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if (dataSource == null) {
            try {
                dataSource = AnnotationUtils.findAnnotation(signature.getMethod().getDeclaringClass(), DataSource.class);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return dataSource;
    }

    @Before("dataSource()")
    public void changeDataSource(JoinPoint joinPoint) {
        DataSource dataSource = findDataSource(joinPoint);
        if (dataSource == null) return;
        String databaseName = dataSource.name();
        if (!DynamicDataSourceContextHolder.isContainsDataSource(databaseName)) {
            log.error("{} === dataSource {} not exists,use default now", joinPoint.getSignature(), databaseName);
        } else {
            log.debug("use dataSource:" + databaseName);
            log.info("{} === use dataSource {} ", joinPoint.getSignature(), databaseName);
            DynamicDataSourceContextHolder.setDataSourceType(databaseName);
        }
    }

    @After("dataSource()")
    public void clearDataSource(JoinPoint joinPoint) {
        DataSource dataSource = findDataSource(joinPoint);
        if (dataSource == null) return;
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

以上代码,先判断方法本身是否含有 @DataSource 注解,如果没有则查看类是否有,如果也没有则啥也不执行,如果有,则会在方法执行前调用 setDataSourceType, 传入的参数就是 @DataSource(name="xxx") 里的 xxx,在方法执行结束,则调用 clearDataSourceType 方法,重置使用的数据库信息,以免影响别的 sql 执行。

完整的代码请查看:https://github.com/ijustyce/dynamic-data-source-spring-boot-starter 关于 AbstractRoutingDataSource 这个类的解读,我后面再补充吧。

本博客若无特殊说明则由 full-stack-trip 原创发布
转载请点名出处:全栈之旅 > spring boot 动态数据源切换 - spring boot dynamic data source
本文地址:https://www.kpromise.top/spring-boot-dynamic-data-source/

在 “spring boot 动态数据源切换 - spring boot dynamic data source” 上有 2 条评论

    1. Hello, this post still has some bug, @Transactional may not work. I will write a new post in the next few days and try my best to translate to English.

发表评论

电子邮件地址不会被公开。 必填项已用*标注