当前位置: 代码网 > it编程>编程语言>Java > SpringBoot自定义动态数据源的流程步骤

SpringBoot自定义动态数据源的流程步骤

2024年07月05日 Java 我要评论
1. 原理动态数据源,本质上是把多个数据源存储在一个 map 中,当需要使用某一个数据源时,使用 key 获取指定数据源进行处理。而在 spring 中已提供了抽象类 abstractroutingd

1. 原理

动态数据源,本质上是把多个数据源存储在一个 map 中,当需要使用某一个数据源时,使用 key 获取指定数据源进行处理。而在 spring 中已提供了抽象类 abstractroutingdatasource 来实现此功能,继承 abstractroutingdatasource 类并覆写其 determinecurrentlookupkey() 方法监听获取 key 即可,该方法只需要返回数据源 key 即可,也就是存放数据源的 mapkey

因此,我们在实现动态数据源的,只需要继承它,实现自己的获取数据源逻辑即可。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动态数据源的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com