一、基于若依(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配置
- 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;
}
}
- 数据源配置
# 数据源配置
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.该动他数据源只会在自己写的方法上生效,像父类的方法,子类不显示指定的时候是不生效的。