所谓动态数据源是指运行时动态的改变数据源,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 这个类的解读,我后面再补充吧。
i am from Italy hello. Can you help me translate? /rardor
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.