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
:动态数据源配置类,继承自abstractroutingdatasource
targetdatasource
:动态数据源注解,切换当前线程的数据源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动态数据源的资料请关注代码网其它相关文章!
发表评论