基于若依(Ruoyi)的多数据源实现(简化版)

一、基于若依(Ruoyi)的多数据源实现(简化版)

若依里面可以集成多种数据源,但是在ruoyi项目中对数据源的配置分在各个包下,这里为了方便多数据源的功能引入以及学习,将ruoyi的多数据源配置提取出来,以便之后需要可以直接复制粘贴引用。
注意:该数据源是以mysql为案例讲解, 数据库连接池使用的是druid,这里不仅仅只能配置mysql一种数据源,凡是遵循jdbc规范的数据源都可以配置,只需要引用相关的驱动以及按照下面的讲解做好相关的配置即可。

二、具体步骤

1. 数据源切换处理工具类

DynamicDataSourceContextHolder 类是用来操作当前线程变量的,这个会在后面用上(传递数据源map容器的key),作为当前线程数值传递的中介,能够起到隔离的作用。

/**
 * 数据源切换处理
 */
public class DynamicDataSourceContextHolder
{
    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType)
    {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType()
    {
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }
}

2.自定义数据源注解

可以将自定义数据源注解添加到方法上(添加到方法只对方法有效)或者类上(添加到类上对当前类里的所有方法有效),然后通过枚举类指定数据源。aop切面会去判断是否有该注解,有的话进行数据源的切换处理(见后面AOP的逻辑)。

枚举类 这里指定你所拥有的数据源

/**
 * 数据源
 * 
 */
public enum DataSourceType
{
    /**
     * 主库
     */
    MASTER,

    /**
     * 从库
     */
    SLAVE
}

自定义注解,标记哪些方法需要数据源动态切换。

package com.ruoyi.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.ruoyi.common.enums.DataSourceType;
/**
 * 自定义多数据源切换注解
 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     */
    public DataSourceType value() default DataSourceType.MASTER;
}

相关的aop逻辑,通过aop前置拦截,将数据源的key存入当前线程变量中,当 spring 调用determineCurrentLookupKey() 方法时
返回相应的数据源的key

package com.ruoyi.framework.aspectj;
import java.util.Objects;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;

/**
 * 多数据源处理
 */
@Aspect
@Order(1)
@Component
public class DataSourceAspect
{

  //aop切点,有相关注解的方法才做增强处理
    @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
            + "|| @within(com.ruoyi.common.annotation.DataSource)")
    public void dsPointCut(){}

    @Around("dsPointCut()") //环绕通知
    public Object around(ProceedingJoinPoint point) throws Throwable
    {
        DataSource dataSource = getDataSource(point);//获取犯法或者类上的注解
        if (StringUtils.isNotNull(dataSource))
        {
            //拿到注解配置的value枚举类,再将配置的枚举类存入到当前线程变量中,之后可以在线程变量中查找当前的配置的是哪个值
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try
        {
            return point.proceed();
        }
        finally
        {
            // 清除当前的线程变量里数据源的key值
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
    
    /**
     * 该方法是获取被增强的方法或类上的DataSource注解,然后返回
     */
    public DataSource getDataSource(ProceedingJoinPoint point)
    {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource))
        {
            return dataSource;
        }

        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

3. 实现抽象类(AbstractRoutingDataSource)

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

/**
 * 动态数据源
 *
 */
public class DynamicDataSource extends AbstractRoutingDataSource
{
// targetDataSources:将所有的数据源以Map的形式传入到AbstractRoutingDataSource的实现类中,以供后面进行动态数据源选择
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
    {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
  //系统每次选择数据源的时候会执行这个方法拿到key,再通过key去内部的数据源targetDataSources Map中找到对应的数据源对象。
    @Override
    protected Object determineCurrentLookupKey()
    {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

4.注入多个数据源

通过@Configuration @Bean @ConfigurationProperties 三个注解将需要注入的数据源注入交给spring管理。

package com.ruoyi.framework.config;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource;

/**
 * druid 配置多数据源  在这里注入多个数据源(配置文件见后面)
 */
@Configuration
public class DruidConfig
{
 //这里以map的方式存储所有的自定义的数据源,其中key是枚举类的字符串值,value是创建好且赋值好的数据源对象。
  private static Map<Object, Object> targetDataSources = new HashMap<>();

   // DruidProperties(属性配置对象,见后面6 spring会根据配置文件的值将该对象的属性赋值,
   // 然后调用dataSource()方法将创建好的数据源对象赋值好然后交给spring管理,
   // 这里是配置的主数据源也是默认的数据源
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DataSource dataSource = druidProperties.dataSource(DruidDataSourceBuilder.create().build())
        targetDataSources.put(DataSourceType.MASTER.name(),dataSource)
        return dataSource;
    }
   // 配置从数据源,注意从数据源配置“name = "enabled", havingValue = "true"”参数,
   // 这样可通过修改配置文件来决定从数据源是否生效,其他同master配置。
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DataSource dataSource = druidProperties.dataSource(DruidDataSourceBuilder.create().build())
        targetDataSources.put(DataSourceType.SLAVE.name(),dataSource)
        return dataSource;
    }
    
/**
* 之后所有需要增加的数据源可以在这里从上到下依次添加,
* 只需要按照上面的方式(slaveDataSource的方法照葫芦画瓢)注入bean,然后再在配置文件中配置好相关的参数,
* 最后添加该数据源所对应的枚举类作为该数据源的key即可
**/


  //注入DynamicDataSource 的实现类交给spring管理
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
}

5.配置类与yml配置

  1. druid的配置信息,读取配置文件中的数据,赋值给成员变量
package com.ruoyi.framework.config.properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;

/**
 * druid 配置属性
 */
@Configuration
public class DruidProperties
{
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;
    
    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.connectTimeout}")
    private int connectTimeout;

    @Value("${spring.datasource.druid.socketTimeout}")
    private int socketTimeout;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    //构建datasource
    public DruidDataSource dataSource(DruidDataSource datasource)
    {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);
        
        /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
        datasource.setConnectTimeout(connectTimeout);
        
        /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
        datasource.setSocketTimeout(socketTimeout);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
        return datasource;
    }
}

  1. 数据源配置
# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://192.168.2.183:3306/ruoyi_vue_single?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 074377wsd
            # 从库数据源
            slave:
                # 从数据源开关/默认关闭
                enabled: false
                url: 
                username: 
                password: 
            # 初始连接数
            initialSize: 5
            # 最小连接池数量
            minIdle: 10
            # 最大连接池数量
            maxActive: 20
            # 配置获取连接等待超时的时间
            maxWait: 60000
            # 配置连接超时时间
            connectTimeout: 30000
            # 配置网络超时时间
            socketTimeout: 60000
            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # 配置一个连接在池中最小生存的时间,单位是毫秒
            minEvictableIdleTimeMillis: 300000
            # 配置一个连接在池中最大生存的时间,单位是毫秒

三、使用方式

我们配置好了数据源之后,就可以通过注解进行使用了,可以将注解加在相关的controller/service方法上或者相关的controller/service层的类上,当程序执行该方法时,会自动的进行数据源的切换。
示例如下:

    /**
     * 查询参数配置信息
     * @param configId 参数配置ID
     * @return 参数配置信息
     */
    @Override
    @DataSource(DataSourceType.MASTER) //这样,执行该方法的时候就会选择MASTER数据源
    public SysConfig selectConfigById(Long configId)
    {
        SysConfig config = new SysConfig();
        config.setConfigId(configId);
        return configMapper.selectConfig(config);
    }

如果不直接在方法上指定,而是在;类上指定,那么该类里的所有方法只要执行都会切换到该类上指定的数据源,使用如下:

@Service
@DataSource(DataSourceType.SLAVE) //该类下的所有方法都会指定SLAVE数据源
public class SysConfigServiceImpl implements ISysConfigService{
//......
}

注意:
1.如果类上,方法上都加了,会以方法上的为基准,如果都不加,就是默认数据源(MASTER)。
2.该动他数据源只会在自己写的方法上生效,像父类的方法,子类不显示指定的时候是不生效的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值