• 花了半天时间,使用spring-boot实现动态数据源,切换自如


      在一个项目中使用多个数据源的情况很多,所以动态切换数据源是项目中标配的功能,当然网上有相关的依赖可以使用,比如动态数据源,其依赖为,

    <dependency>
      <groupId>com.baomidougroupId>
      <artifactId>dynamic-datasource-spring-boot-starterartifactId>
      <version>3.5.1version>
    dependency>

      今天,不使用现成的API,手动实现一个动态数据源。

    一、环境及依赖

    springboot、mybatis-plus的基础上实现动态数据源切换,

    springboot:2.3.3.RELEASE

    mybatis-plus-boot-starter:3.5.0

    mysql驱动:8.0.32

    除了这些依赖外没有其他的,目标是动态切换数据源。

    二、实现思路

      先来看下,单数据源的情况。

      在使用springboot和mybatis-plus时,我们没有配置数据源(DataSource),只配置了数据库相关的信息,便可以连接数据库进行数据库的操作,这是为什么呐。其实是基于spring-boot的自动配置,也就是autoConfiguration,在自动配置下有DataSourceAutoConfiguration类,该类会生成一个数据源并注入到spring的容器中,这样就可以使用该数据源提供的连接,访问数据库了。

      感兴趣的小伙伴可以了解下这个类的具体实现逻辑。

      要实现多数据源,并且可以自动切换。那么肯定就不能再使用DataSourceAutoConfigurtation了,因为它只能产生一个数据源,多个数据源要怎么办,spring提供了AbstractRoutingDataSource类,该类是一个抽象类,仅有一个抽象方法需要实现

    Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
    Allows for arbitrary keys. The returned key needs to match the stored lookup key type, 
    as resolved by the resolveSpecifiedLookupKey method.
    @Nullable
    protected abstract Object determineCurrentLookupKey();

    可以根据该类实现一个动态数据源。好了,现在了解了实现思路,开始实现一个动态数据源,要做以下的准备工作。

    1、配置文件;

    2、自定义动态数据源;

    2.1、配置文件

    由于是多数据源,那么在配置文件中肯定是多个配置,不能再是一个数据库的配置了,这里使用两个mysql的配置进行演示,

    #master 默认数据源
    spring:
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
          username: root
          password: 123456
    #slave 从数据源
        slave:
          driver-class-name: com.mysql.cj.jdbc.Driver
          jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
          username: root
          password: 123456

    这里使用了一个master一个slave两个数据源配置,其地址是一致的,但数据库示例不一样。 有了数据源的信息下一步要实现自己的数据源,

    2.2、自定义动态数据源

      前边说,spring提供了AbstractRoutingDataSource类可以实现动态数据源,看下实现。

    DynamicDatasource.java

    package com.wcj.my.config.dynamic.source;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * 动态数据源
     * @date 2023/6/8 19:18
     */
    public class DynamicDatasource extends AbstractRoutingDataSource {
        /**
         * Determine the current lookup key. This will typically be
         * implemented to check a thread-bound transaction context.
         * 

    Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ @Override protected Object determineCurrentLookupKey() { return DynamicDatasourceHolder.getDataSource(); } }

    这里的determineCurrentLookupKey方法,需要返回一个数据源,也就是说返回一个数据源的映射,这里返回一个DynamicDatasourceHolder.getDataSource()方法的返回值,DynamicDatasourceHolder是一个保存多个数据源的地方,

    DynamicDatasourceHolder.java

    package com.wcj.my.config.dynamic.source;
    
    import java.util.Queue;
    import java.util.concurrent.ArrayBlockingQueue;
    
    /**
     * @date 2023/6/8 19:42
     */
    public class DynamicDatasourceHolder {
        //保存数据源的映射
        private static Queue queue = new ArrayBlockingQueue(1);
        public static String getDataSource() {
            return queue.peek();
        }
        public static void setDataSource(String dataSourceKey) {
            queue.add(dataSourceKey);
        }
        public static void removeDataSource(String dataSourceKey) {
            queue.remove(dataSourceKey);
        }
    }

    该类很简单,使用一个队列保存数据源的映射,提供获取/设置数据源的方法。

    这里使用ThreadLocal类更合适,这样可以实现线程的隔离,一个请求会有一个线程来处理,保证每隔线程使用的数据源是一样的。

    到现在为止依旧没有出现如何创建多数据源,下面就来了,不着急。

    DynamicDatasourceConfig.java

    package com.wcj.my.config.dynamic.source;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @date 2023/6/8 19:51
     */
    @Configuration
    @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
    public class DynamicDatasourceConfig {
        @Bean("master")
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDatasource(){
            return DataSourceBuilder.create().build();
        }
        @Bean("slave")
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDatasource(){
            return DataSourceBuilder.create().build();
        }
        @Bean
        @Primary
        public DataSource dataSource(){
            Map dataSourceMap = new HashMap<>(2);
            dataSourceMap.put("master", masterDatasource());
            dataSourceMap.put("slave", slaveDatasource());
    
            DynamicDatasource dynamicDatasource=new DynamicDatasource();
            dynamicDatasource.setTargetDataSources(dataSourceMap);
            dynamicDatasource.setDefaultTargetDataSource(masterDatasource());
            return dynamicDatasource;
        }
    }

    首先,在该类上有个一个@Configuration注解,标明这是一个配置类;

    其次,有一个@EnableAutonConfiguration注解,该注解中有个数组类型的exclude属性,排除不需要自动配置的类,这里排除的是当然就是DataSourceAutoConfiguration类了;因为下面会自动生成数据源,不需要自动配置了;

    然后,在类中是标有@Bean的方法,这些方法便是生成数据源类,且映射为”master“、”slave“,可以有多个。使用的是DataSourceBuilder类帮助生成;

    最后,生成一个DynamicDatasource,且标有@Primary注解,这里需要设置”master“、”slave“两个映射代表的数据源;

    这样便向spring容器中注入了三个数据源,分别是”master“、”slave“代表的数据源,他们是需要实际使用的数据源。还有一个是DynamicDatasource,提供数据源的设置。这三个都是DataSource的子类。

    三、使用多数据源

      上面已经完成了多数据源的配置,下面看怎么使用吧,还记得DynamicDatasourceHolder类中有set/get方法吗,就是使用这个类提供的方法,

    UserSerivce.java

    package com.wcj.my.service;
    
    import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
    import com.wcj.my.dto.UserDto;
    import com.wcj.my.entity.User;
    import com.wcj.my.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @date 2023/6/8 15:19
     */
    @Service
    public class UserService {
        @Autowired
        private UserMapper userMapper;
        /**默认使用master数据源
        */
        public boolean saveUser(UserDto userDto) {
    
            User user = new User();
            user.setUName(userDto.getName());
            user.setUCode(userDto.getCode());
            user.setUAge(userDto.getAge());
            user.setUAddress(userDto.getAddress());
            int num = userMapper.insert(user);
            if (num > 0) {
                return true;
            }
            return false;
        }
        /**
         *使用slave数据源
         */
        public boolean saveUserSlave(UserDto userDto) {
            DynamicDatasourceHolder.setDataSource("slave");
            User user = new User();
            user.setUName(userDto.getName());
            user.setUCode(userDto.getCode());
            user.setUAge(userDto.getAge());
            user.setUAddress(userDto.getAddress());
            int num = userMapper.insert(user);
            DynamicDatasourceHolder.removeDataSource("slave");
            if (num > 0) {
                return true;
            }
            return false;
        }
    }

      上面的service层方法在调用dao层方法的时候,使用DynamicDatasourceHolder.setDataSource()方法设置了需要使用的数据源, 通过这样的方式便可以实现动态数据源了。

      不知道,小伙伴们有没有感觉到,这样每次在调用方法的时候都需要设置数据源是不是很麻烦,有没有一种更方面的方式,比如说注解。

    四、动态数据源注解@DDS

       现在来实现一个动态数据源的注解来代替上面的每次都调用DynamicDatasourceHolder.setDataSource()方法来设置数据源。

      先看下,@DDS注解的定义

    DDS.java

    package com.wcj.my.config.dynamic.source.aspect;
    
    import org.springframework.stereotype.Component;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**动态数据源的注解
     * 用在类和方法上,方法上的优先级大于类上的
     * 默认值是master
     * @date 2023/6/9 16:19
     */
    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Component
    public @interface DDS {
        String value() default "master";
    }

    注解@DDS使用在类和方法上,切方法上的优先级大于类上的。有一个value的属性,指明使用的数据源,默认是”master“。

    实现一个切面,来切@DDS注解

     DynamicDatasourceAspect.java

    package com.wcj.my.config.dynamic.source.aspect;
    
    import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
    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.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.util.Objects;
    
    /**
     * 动态数据源切面
     * @date 2023/6/9 16:23
     */
    @Aspect
    @Component
    public class DynamicDatasourceAspect {
        /**
         * 切点,切的是带有@DDS的注解
         */
        @Pointcut("@annotation(com.wcj.my.config.dynamic.source.aspect.DDS)")
        public void dynamicDatasourcePointcut(){
    
        }
        /**
         * 环绕通知
         * @param joinPoint
         * @return
         * @throws Throwable
         */
        @Around("dynamicDatasourcePointcut()")
        public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
            String datasourceKey="master";
    
            //类上的注解
            Class targetClass=joinPoint.getTarget().getClass();
            DDS annotation=targetClass.getAnnotation(DDS.class);
    
            //方法上的注解
            MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
            DDS annotationMethod=methodSignature.getMethod().getAnnotation(DDS.class);
            if(Objects.nonNull(annotationMethod)){
                datasourceKey=annotationMethod.value();
            }else{
                datasourceKey=annotation.value();
            }
            //设置数据源
            DynamicDatasourceHolder.setDataSource(datasourceKey);
            try{
               return joinPoint.proceed();
            }finally {
                DynamicDatasourceHolder.removeDataSource(datasourceKey);
            }
        }
    }

     这样一个动态数据源的注解便可以了,看下怎么使用,

    UserServiceByAnnotation.java

    package com.wcj.my.service;
    
    import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;
    import com.wcj.my.config.dynamic.source.aspect.DDS;
    import com.wcj.my.dto.UserDto;
    import com.wcj.my.entity.User;
    import com.wcj.my.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @date 2023/6/8 15:19
     */
    @Service
    public class UserServiceByAnnotation {
        @Autowired
        private UserMapper userMapper;
        @DDS("master")
        public boolean saveUser(UserDto userDto){
            User user=new User();
            user.setUName(userDto.getName());
            user.setUCode(userDto.getCode());
            user.setUAge(userDto.getAge());
            user.setUAddress(userDto.getAddress());
            int num=userMapper.insert(user);
            if(num>0){
                return true;
            }
            return false;
        }
        @DDS("slave")
        public boolean saveUserSlave(UserDto userDto){
            User user=new User();
            user.setUName(userDto.getName());
            user.setUCode(userDto.getCode());
            user.setUAge(userDto.getAge());
            user.setUAddress(userDto.getAddress());
            int num=userMapper.insert(user);
            if(num>0){
                return true;
            }
            return false;
        }
    }

    使用起来很简单,在需要切换数据源的方法或类上使用@DDS注解即可,使用value来改变数据源就好了。

    五、动态数据源的原理

      很多小伙伴可能和我有一样的疑惑,使用DynamicDatasourceHolder.setDataSource或@DDS就可以设置数据源了,是怎么实现的,下面分析下,我们指定dao层的Mapper其实是一个代理对象,其会使用mybatis中的sqlSessionTempalte进行数据库的操作,在sqlSessionTemplate中会使用DefaultSqlSession对象,最终会使用DataSource,而使用了动态数据源的对象中会注入一个DynamicDataSource,在进行数据库操作时最终会获得一个数据库连接,这里便会使用DynamicDataSource获得一个连接,由于它继承了AbstractRoutingDataSource类,看下其getConnection方法,

    @Override
    	public Connection getConnection() throws SQLException {
    		return determineTargetDataSource().getConnection();
    	}

    看下determineTargetDataSource()方法,

    protected DataSource determineTargetDataSource() {
    		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");、
              //自己实现的,在调用方法时进行了设置,实现动态数据源的目的    
    		Object lookupKey = determineCurrentLookupKey();
    		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 + "]");
    		}
    		return dataSource;
    	}

    看上面的注释,determineCurrentLookupkey()方法便是在DynamicDatasource类中进行了实现,从而实现了动态设置数据源的目的。

    六、总结 

    本文动手实现了一个动态数据源,并切提供了注解的方式,主要有以下几点

    1、继承AbstractRoutingDataSource类的determineCurrentLookupkey()方法,动态设置数据源;

    2、取消DataSourceAutoConfiguration的自动配置,手动向spring容器中注入多个数据源;

    3、基于@DDS注解动态设置数据源;

    最后,本文用到的源码均可通过下方公众号获得。

    推荐阅读

    mybatis-plus是什么框架,使用起来简单吗?文末有彩蛋

    周末折腾了两天,踩了无数个坑,终于把win7装成了centos7

  • 相关阅读:
    Stream流详解
    使用Go语言和chromedp库下载Instagram图片:简易指南
    Spring 事务传播机制源码浅析——PROPAGATION_PROPAGATION_NESTED 事务嵌套
    k8s安装,linux-ubuntu上面kubernetes详细安装过程
    WMS系统后端开发-用户权限
    kafka常用命令
    C语言中的结构体对齐原则是什么?如何进行结构体的对齐控制?
    python如何学习
    ES6那些不知道的事儿
    网络安全-终端防护设备
  • 原文地址:https://www.cnblogs.com/teach/p/17474800.html