概述
使用主要技术
使用Druid+myBatis+Spring AOP实现多数据源切换。
总体流程
图描述
![](https://img-blog.csdnimg.cn/img_convert/ec610976b8e645d2ac471f817cc1d4ba.png)
详细文字描述
我们在想要使用其它数据源的方法或者类上加上@DataSource注解并给对设置注解中value为数据源的名称。
执行该方法时加有该注解的方法或类就会被DataSourceAspect切面类拦截到,在这个AOP切面类中会拿到@DataSource中的value值,这个值就是我们想要切换的数据源的名称。
拿到这个值之后我们就将这个值存入到DynamicDataSourceContextHolder中的静态变量ThreadLocal中(这部分涉及到ThreadLocal的知识)。
mybatis去执SQL会去调AbstractRoutingDataSource中的getConnection( )方法,该方法中首先会获取要调用的数据源,我们自定义的DynamicDataSource类在此时就会被调用。
DynamicDataSource类中有重写的AbstractRoutingDataSource类中的方法所以能决定调用哪个数据源的规则,DynamicDataSource类回去调用当时切面放入DynamicDataSourceContextHolder中数据源的名称。
到此mybatis终于拿到要调用的数据源并获取到连接,可以进行SQL操作。
代码实现
Druid配置类
需要提前配置所有数据源信息
@Configuration
public class DruidConfig
{
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource)
{
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 设置数据源
*
* @param targetDataSources 备选数据源集合
* @param sourceName 数据源名称
* @param beanName bean名称
*/
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
{
try
{
DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
}
catch (Exception e)
{
}
}
}
配置文件中
druid:
# 主库数据源
master:
url: jdbc:mysql://192.168.5.132:3306/ccd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: abc123.
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: true
jdbc:mysql://192.168.5.215:3306/aaaad?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456.
切面相关类
枚举DataSourceType
public enum DataSourceType
{
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE
}
注解DataSource
/**
* 自定义多数据源切换注解
*
* 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
*
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
/**
* 切换数据源名称
*/
public DataSourceType value() default DataSourceType.MASTER;
}
切面DataSourceAspect
/**
* 多数据源处理
*
* @author ruoyi
*/
@Aspect
@Order(1)
@Component
public class DataSourceAspect
{
protected Logger logger = LoggerFactory.getLogger(getClass());
@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))
{
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try
{
return point.proceed();
}
finally
{
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
/**
* 获取需要切换的数据源
*/
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);
}
}
数据源切换处理DynamicDataSourceContextHolder
/**
* 数据源切换处理
*
*/
public class DynamicDataSourceContextHolder
{
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用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();
}
}
数据源切换自定义类
DynamicDataSource
/**
* 动态数据源
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource
{
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
{
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey()
{
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
使用方式
@DataSource(DataSourceType.SLAVE)
@Transactional(readOnly = false)
@Override
public List<User> findAll() {
return userDao.findAll();
}
总结
重点以及作用
AOP
这里使用AOP切面来实现对数据源的切换控制十分方便
切面中的切面处理类
@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ "|| @within(com.ruoyi.common.annotation.DataSource)")
public void dsPointCut()
{
}
/**
* 自定义多数据源切换注解
*
* 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
*
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
/**
* 切换数据源名称
*/
public DataSourceType value() default DataSourceType.MASTER;
}
其中@within:如果某个类上标注了注解这个,那么该类中的所有方法就会被匹配为切点,并且该类的子类中没有重写的父类方法也会被匹配为切点。
注解中又放置了优先级【方法优先级>类优先级】灵活性更高
@Aspect
@Order(1)
@Component
public class DataSourceAspect
@Order:Spring IOC中bean的执行顺序,其中的值越小优先级越高。代码中的是@Order(1)所有优先级是很高的,因为数据源切换一定是要在这个方法执行的最前面不然会出问题的。
ThreadLocal
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 清空数据源变量
*/
public static void clearDataSourceType()
{
CONTEXT_HOLDER.remove();
}
finally
{
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
这里使用ThradLocal是为了多线程下各线程都能拿到自己切换的那个数据源,使用完ThreadLocal并对其进行清理防止内存泄露。