1. 原理
动态数据源,本质上是把多个数据源存储在一个 map 中,当需要使用某一个数据源时,使用 key 获取指定数据源进行处理。而在 spring 中已提供了抽象类 abstractroutingdatasource 来实现此功能,继承 abstractroutingdatasource 类并覆写其 determinecurrentlookupkey() 方法监听获取 key 即可,该方法只需要返回数据源 key 即可,也就是存放数据源的 map 的 key。
因此,我们在实现动态数据源的,只需要继承它,实现自己的获取数据源逻辑即可。abstractroutingdatasource 顶级继承了 datasource,所以它也是可以做为数据源对象,因此项目中使用它作为主数据源。
1.1. abstractroutingdatasource 源码解析

public abstract class abstractroutingdatasource extends abstractdatasource implements initializingbean {
// 目标数据源 map 集合,存储将要切换的多数据源 bean 信息,可以通过 settargetdatasource(map<object, object> mp) 设置
@nullable
private map<object, object> targetdatasources;
// 未指定数据源时的默认数据源对象,可以通过 setdefaulttargetdatasouce(object obj) 设置
@nullable
private object defaulttargetdatasource;
...
// 数据源查找接口,通过该接口的 getdatasource(string datasourcename) 获取数据源信息
private datasourcelookup datasourcelookup = new jndidatasourcelookup();
//解析 targetdatasources 之后的 datasource 的 map 集合
@nullable
private map<object, datasource> resolveddatasources;
@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 值(druid 数据源信息)转为 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);
}
// 最重要的一个方法,也是 dynamicdatasource 需要实现的方法
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();
}
1.2. 关键类说明
忽略掉 controller/service/entity/mapper/xml介绍。
application.yml:数据源配置文件。但是如果数据源比较多的话,根据实际使用,最佳的配置方式还是独立配置比较好。dynamicdatasourceregister:动态数据源注册配置文件dynamicdatasource:动态数据源配置类,继承自abstractroutingdatasourcetargetdatasource:动态数据源注解,切换当前线程的数据源dynamicdatasourceaspect:动态数据源设置切面,环绕通知,切换当前线程数据源,方法注解优先dynamicdatasourcecontextholder:动态数据源上下文管理器,保存当前数据源的key,默认数据源名,所有数据源key
1.3. 开发流程
- 添加配置文件,设置默认数据源配置,和其他数据源配置
- 编写
dynamicdatasource类,继承abstractroutingdatasource类,并实现determinecurrentlookupkey()方法 - 编写
dynamicdatasourceholder上下文管理类,管理当前线程的使用的数据源,及所有数据源的key; - 编写
dynamicdatasourceregister类通过读取配置文件动态注册多数据源,并在启动类上导入(@import)该类 - 自定义数据源切换注解
targetdatasource,并实现相应的切面,环绕通知切换当前线程数据源,注解优先级(dynamicdatasourceholder.setdynamicdatasourcekey()>method>class)
2. 实现
2.1. 引入 maven 依赖
<!-- web 模块依赖 --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <!-- spring 核心 aop 模块依赖 --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-aop</artifactid> </dependency> <!-- druid 数据源连接池依赖 --> <dependency> <groupid>com.alibaba</groupid> <artifactid>druid-spring-boot-starter</artifactid> <version>1.2.8</version> </dependency> <!-- mybatis 依赖 --> <dependency> <groupid>org.mybatis.spring.boot</groupid> <artifactid>mybatis-spring-boot-starter</artifactid> <version>2.2.2</version> </dependency> <!-- mysql驱动 --> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> <version>8.0.24</version> </dependency> <!-- lombok 模块依赖 --> <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <optional>true</optional> </dependency> <dependency> <groupid>org.apache.commons</groupid> <artifactid>commons-text</artifactid> <version>1.10.0</version> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency>
2.2. application.yml 配置文件
spring:
datasource:
type: com.alibaba.druid.pool.druiddatasource
url: jdbc:mysql://localhost:3306/test?useunicode=true&characterencoding-utf8&allowpublickeyretrieval=true&usessl=false&servertimezone=asia/shanghai&allowmultiqueries=true
driver-class-name: com.mysql.cj.jdbc.driver
username: root
password: root
custom:
datasource:
names: ds1,ds2
ds1:
type: com.alibaba.druid.pool.druiddatasource
driver-class-name: com.mysql.cj.jdbc.driver
url: jdbc:mysql://localhost:3306/content_center?useunicode
username: root
password: root
ds2:
type: com.alibaba.druid.pool.druiddatasource
driver-class-name: com.mysql.cj.jdbc.driver
url: jdbc:mysql://localhost:3306/trade?useunicode
username: root
password: root
2.3. 创建 dynamicdatasource 继承 abstractroutingdatasource 类
import org.springframework.jdbc.datasource.lookup.abstractroutingdatasource;
import lombok.data;
import lombok.noargsconstructor;
import lombok.allargsconstructor;
/**
* @description: 继承spring abstractroutingdatasource 实现路由切换
*/
@data
@noargsconstructor
@allargsconstructor
public class dynamicdatasource extends abstractroutingdatasource {
/**
* 决定当前线程使用哪种数据源
* @return 数据源 key
*/
@override
protected object determinecurrentlookupkey() {
return dynamicdatasourcecontextholder.getdatasourcetype();
}
}
2.4. 编写 dynamicdatasourceholder 类,管理 dynamicdatasource 上下文
import java.util.arraylist;
import java.util.list;
/**
* @description: 动态数据源上下文管理
*/
public class dynamicdatasourceholder {
// 存放当前线程使用的数据源类型信息
private static final threadlocal<string> dynamic_datasource_key = new threadlocal<string>();
// 存放数据源 key
private static final list<string> datasource_keys = new arraylist<string>();
// 默认数据源 key
public static final string default_datesource_key = "master";
//设置数据源
public static void setdynamicdatasourcetype(string key) {
dynamic_datasource_key.set(key);
}
//获取数据源
public static string getdynamicdatasourcetype() {
return dynamic_datasource_key.get();
}
//清除数据源
public static void removedynamicdatasourcetype() {
dynamic_datasource_key.remove();
}
public static void adddatasourcekey(string key) {
datasource_keys.add(key)
}
/**
* 判断指定 key 当前是否存在
*
* @param key
* @return boolean
*/
public static boolean containsdatasource(string key){
return datasource_keys.contains(key);
}
}
2.5. 编写 dynamicdatasourceregister 读取配置文件注册多数据源
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.mutablepropertyvalues;
import org.springframework.beans.factory.support.beandefinitionregistry;
import org.springframework.beans.factory.support.genericbeandefinition;
import org.springframework.boot.jdbc.datasourcebuilder;
import org.springframework.context.environmentaware;
import org.springframework.context.annotation.importbeandefinitionregistrar;
import org.springframework.core.env.environment;
import org.springframework.core.type.annotationmetadata;
import javax.sql.datasource;
import java.util.hashmap;
import java.util.map;
import org.apache.commons.lang3.stringutils;
import java.util.objects;
/**
* @description: 注册动态数据源
* 初始化数据源和提供了执行动态切换数据源的工具类
* environmentaware(获取配置文件配置的属性值)
*/
public class dynamicdatasourceregister implements importbeandefinitionregistrar, environmentaware {
private static final logger logger = loggerfactory.getlogger(dynamicdatasourceregister.class);
// 指定默认数据源类型 (springboot2.0 默认数据源是 hikari 如何想使用其他数据源可以自己配置)
// private static final string datasource_type_default = "com.zaxxer.hikari.hikaridatasource";
private static final string default_datasource_type = "com.alibaba.druid.pool.druiddatasource";
// 默认数据源
private datasource defaultdatasource;
// 用户自定义数据源
private map<string, datasource> customdatasources = new hashmap<>();
/**
* 加载多数据源配置
* @param env 当前环境
*/
@override
public void setenvironment(environment env) {
initdefaultdatasource(env);
initcustomdatasources(env);
}
/**
* 初始化主数据源
* @param env
*/
private void initdefaultdatasource(environment env) {
// 读取主数据源
map<string, object> dsmap = new hashmap<>();
dsmap.put("type", env.getproperty("spring.datasource.type", default_datasource_type));
dsmap.put("driver", env.getproperty("spring.datasource.driver-class-name"));
dsmap.put("url", env.getproperty("spring.datasource.url"));
dsmap.put("username", env.getproperty("spring.datasource.username"));
dsmap.put("password", env.getproperty("spring.datasource.password"));
defaultdatasource = builddatasource(dsmap);
}
/**
* 初始化更多数据源
* @param env
*/
private void initcustomdatasources(environment env) {
// 读取配置文件获取更多数据源
string dsprefixs = env.getproperty("custom.datasource.names");
if (!stringutils.isblank(dsprefixs)) {
for (string dsprefix : dsprefixs.split(",")) {
dsprefix = fsprefix.trim()
if (!stringutils.isblank(dsprefix)) {
map<string, object> dsmap = new hashmap<>();
dsmap.put("type", env.getproperty("custom.datasource." + dsprefix + ".type", default_datasource_type));
dsmap.put("driver", env.getproperty("custom.datasource." + dsprefix + ".driver-class-name"));
dsmap.put("url", env.getproperty("custom.datasource." + dsprefix + ".url"));
dsmap.put("username", env.getproperty("custom.datasource." + dsprefix + ".username"));
dsmap.put("password", env.getproperty("custom.datasource." + dsprefix + ".password"));
datasource ds = builddatasource(dsmap);
customdatasources.put(dsprefix, ds);
}
}
}
}
@override
public void registerbeandefinitions(annotationmetadata annotationmetadata, beandefinitionregistry registry) {
map<object, object> targetdatasources = new hashmap<object, object>();
// 将主数据源添加到更多数据源中
targetdatasources.put(dynamicdatasourceholder.default_datasource_key, defaultdatasource);
dynamicdatasourceholder.adddatasourcekey(dynamicdatasourceholder.default_datasource_key);
// 添加更多数据源
targetdatasources.putall(customdatasources);
for (string key : customdatasources.keyset()) {
dynamicdatasourcecontextholder.adddatasourcekey(key);
}
// 创建 dynamicdatasource
genericbeandefinition beandefinition = new genericbeandefinition();
beandefinition.setbeanclass(dynamicdatasource.class);
beandefinition.setsynthetic(true);
mutablepropertyvalues mpv = beandefinition.getpropertyvalues();
mpv.addpropertyvalue("defaulttargetdatasource", defaultdatasource);
mpv.addpropertyvalue("targetdatasources", targetdatasources);
registry.registerbeandefinition("datasource", beandefinition); // 注册到 spring 容器中
logger.info("dynamic datasource registry");
}
/**
* 创建 datasource
* @param dsmap 数据库配置参数
* @return datasource
*/
public datasource builddatasource(map<string, object> dsmap) {
try {
object type = dsmap.get("type");
if (type == null)
type = default_datasource_type;// 默认datasource
class<? extends datasource> datasourcetype = (class<? extends datasource>)class.forname((string)type);
string driverclassname = string.valueof(dsmap.get("driver"));
string url = string.valueof(dsmap.get("url"));
string username = string.valueof(dsmap.get("username"));
string password = string.valueof(dsmap.get("password"));
// 自定义 datasource 配置
datasourcebuilder<? extends datasource> factory = datasourcebuilder.create()
.driverclassname(driverclassname)
.url(url)
.username(username)
.password(password)
.type(datasourcetype);
return factory.build();
}catch (classnotfoundexception e) {
e.printstacktrace();
}
}
}
2.6. 在启动器类上添加 @import,导入 register 类
// 注册动态多数据源
@import({ dynamicdatasourceregister.class })
@springbootapplication
public class application {
public static void main(string[] args) {
springapplication.run(application.class, args);
}
}
2.7. 自定义注解 @targetdatasource
/**
* 自定义多数据源切换注解
* 优先级:dynamicdatasourceholder.setdynamicdatasourcekey() > method > class
*/
@target({ elementtype.method, elementtype.type })
@retention(retentionpolicy.runtime)
@documented
@inherited
public @interface datasource
{
/**
* 切换数据源名称
*/
public string value() default dynamicdatasourceholder.default_datesource_key;
}
2.8. 定义切面拦截 @targetdatasource
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.pointcut;
import org.springframework.core.annotation.order;
import org.springframework.stereotype.component;
import org.aspectj.lang.reflect.methodsignature;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import java.util.objects;
@aspect
// 保证在 @transactional 等注解前面执行
@order(-1)
@component
public class datasourceaspect {
// 设置 datasource 注解的切点表达式
@pointcut("@annotation(com.ayi.config.datasource.dynamicdatasource)")
public void dynamicdatasourcepointcut(){
}
//环绕通知
@around("dynamicdatasourcepointcut()")
public object around(proceedingjoinpoint joinpoint) throws throwable{
string key = getdefineannotation(joinpoint).value();
if (!dynamicdatasourceholder.containsdatasource(key)) {
logger.error("数据源[{}]不存在,使用默认数据源[{}]", key, dynamicdatasourceholder.default_datesource_key)
key = dynamicdatasourceholder.default_datesource_key;
}
dynamicdatasourceholder.setdynamicdatasourcekey(key);
try {
return joinpoint.proceed();
} finally {
dynamicdatasourceholder.removedynamicdatasourcekey();
}
}
/**
* 先判断方法的注解,后判断类的注解,以方法的注解为准
* @param joinpoint 切点
* @return targetdatasource
*/
private targetdatasource getdefineannotation(proceedingjoinpoint joinpoint){
methodsignature methodsignature = (methodsignature) joinpoint.getsignature();
targetdatasource datasourceannotation = methodsignature.getmethod().getannotation(targetdatasource.class);
if (objects.nonnull(methodsignature)) {
return datasourceannotation;
} else {
class<?> dsclass = joinpoint.gettarget().getclass();
return dsclass.getannotation(targetdatasource.class);
}
}
}
以上就是springboot自定义动态数据源的流程步骤的详细内容,更多关于springboot动态数据源的资料请关注代码网其它相关文章!
发表评论