AbstractRoutingDataSource 介绍
AbstractRoutingDataSource 是 Spring 提供的一个抽象类,它可以让我们动态切换数据源。在使用 AbstractRoutingDataSource 时,我们需要实现它的 determineCurrentLookupKey() 方法,该方法返回当前线程使用的数据源的名称或标识符。
AbstractRoutingDataSource 两个重要的成员变量:
- defaultTargetDataSource:默认数据源,即当无法确定使用哪个数据源时,将使用该默认数据源。在AbstractRoutingDataSource中,我们可以通过调用setDefaultTargetDataSource()方法来设置默认数据源。
- targetDataSources:一个Map类型的成员变量,其中保存了多个数据源实例。在AbstractRoutingDataSource中,我们可以通过调用setTargetDataSources()方法来设置多个数据源实例,其中Map的key为数据源标识符或名称,value为对应的数据源实例。
使用AbstractRoutingDataSource实现数据源切换的原理如下:
- 在应用启动时,我们需要配置多个数据源,并将它们注册到AbstractRoutingDataSource中。
- 当应用发起数据库操作时,AbstractRoutingDataSource会根据determineCurrentLookupKey()方法返回的值,查找对应的数据源。
- 在determineCurrentLookupKey()方法中,我们可以根据不同的策略来决定使用哪个数据源。例如,可以根据当前请求的用户信息、请求的URL、当前时间等来选择不同的数据源。
简单示例
有两个数据库datasource01、datasource02,分别有一张表user,
在 yml 中配置两个数据源 datasource01、datasource02
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://localhost:3306/datasource01?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
datasource2:
url: jdbc:mysql://localhost:3306/datasource02?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
将这两个 DataSource 加载到 Spring IOC容器中
package cc.oolo.imgcdn.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource.datasource1中的配置,创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
return DruidDataSourceBuilder.create().build();
}
}
定义 DynamicDataSource 并且添加 @Component和@Primary注解,并且继承 AbstractRoutingDataSource,afterPropertiesSet为初始化方法,在 Spring Bean 生命周期初始化阶段就会执行该方法,在这个方法里面,将数据源加载到 targetDataSources 和 defaultTargetDataSource
package cc.oolo.imgcdn.config;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
@Resource
private DataSource dataSource1;
@Resource
private DataSource dataSource2;
@Override
protected Object determineCurrentLookupKey() {
return "d1";
}
@Override
public void afterPropertiesSet() {
// 为targetDataSource初始化所有数据源
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put("d1",dataSource1);
targetDataSources.put("d2",dataSource2);
super.setTargetDataSources(targetDataSources);
// 为defaultTargetDatasource设置默认数据源
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
然后当需要获取连接的时候,就会调用 getConnection() 方法,在这个方法里面会调用 determineTargetDataSourcea().getConnection(),获取连接然后返回,determineTargetDataSource() 方法根据我们返回的值,从 resolvedDataSources 获取对应的数据源返回。
至于以上为什么不直接从 targetDataSources 获取,因为在初始化的时候,AbstractRoutingDataSource 会将 targetDataSources 进行处理和转换后,赋值给 resolvedDataSources
以上我们添加了"d1"和"d2"数据源,在选择数据源的时候,只需要在 determineCurrentLookupKey() 方法返回该数据源所对应的key即可
使用ThreadLocal动态控制
上面这个例子需要我们手动去修改 determineCurrentLookupKey 方法的返回值,没有办法根据我们的业务,动态切换数据源。
在这里可以引入ThreadLocal,使用ThreadLocal存储需要使用的数据源的key,需要修改的时候直接修改ThreadLocal即可,只需要在 DynamicDataSource 中创建一个 ThreadLocal,并且将determineCurrentLookupKey 方法的返回值修改为 threadLocal.get() 即可
package cc.oolo.imgcdn.config;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
public static ThreadLocal<String> name = new ThreadLocal<>();
@Resource
private DataSource dataSource1;
@Resource
private DataSource dataSource2;
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
@Override
public void afterPropertiesSet() {
// 为targetDataSource初始化所有数据源
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put("d1",dataSource1);
targetDataSources.put("d2",dataSource2);
super.setTargetDataSources(targetDataSources);
// 为defaultTargetDatasource设置默认数据源
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
测试:
@Test
public void test1(){
DynamicDataSource.name.set("d1");
System.out.println(userService.getUser());
DynamicDataSource.name.set("d2");
System.out.println(userService.getUser());
}
结果:
2023-07-20 17:56:04.353 INFO 21600 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
UserEntity(id=1, name=数据源1, age=18, gender=男, email=1@qq.com)
2023-07-20 17:56:04.529 INFO 21600 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-2} inited
UserEntity(id=1, name=数据源2, age=120, gender=女, email=2@qq.com)
使用 自定义注解 + AOP + ThreadLocal 切换数据源
自定义注解 DataSourceSwitch,注解可以标识在类或者方法上面
package cc.oolo.imgcdn.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSwitch {
String value() default "d1";
}
定义 Aspect,拦截 DataSourceSwitch 注解和 cc.oolo.imgcdn.controller、cc.oolo.imgcdn.service.impl
包下的所有类和方法
package cc.oolo.imgcdn.aspect;
import cc.oolo.imgcdn.annotation.DataSourceSwitch;
import cc.oolo.imgcdn.config.DynamicDataSource;
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.lang.reflect.Method;
@Component
@Aspect
public class DynamicDataSourceAspect {
@Pointcut("within(cc.oolo.imgcdn.controller..*) || within(cc.oolo.imgcdn.service.impl..*) || (@annotation(cc.oolo.imgcdn.annotation.DataSourceSwitch))")
public void pointCut(){
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取目标类
Class<?> targetClass = joinPoint.getTarget().getClass();
// 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取类上的注解
DataSourceSwitch classAnnotation = targetClass.getAnnotation(DataSourceSwitch.class);
// 获取方法上的注解
DataSourceSwitch methodAnnotation = method.getAnnotation(DataSourceSwitch.class);
// 如果类上有注解,以类上的注解为准
if (classAnnotation != null){
// 根据注解的value设置ThreadLocal的值
DynamicDataSource.name.set(classAnnotation.value());
System.out.println("选择的数据源:"+ classAnnotation.value());
}
// 如果方法上有注解,以方法上的注解为准
if (methodAnnotation != null){
// 根据注解的value设置ThreadLocal的值
DynamicDataSource.name.set(methodAnnotation.value());
System.out.println("选择的数据源:"+ methodAnnotation.value());
}
return joinPoint.proceed();
}
}
测试:
package cc.oolo.imgcdn.controller;
import cc.oolo.imgcdn.annotation.DataSourceSwitch;
import cc.oolo.imgcdn.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class UserController {
@Resource
private UserService userService;
@GetMapping("getUser")
@DataSourceSwitch("d2")
public Object getUser(){
return userService.getUser();
}
}
结果:
{
"id": "1",
"name": "数据源2",
"age": 120,
"gender": "女",
"email": "2@qq.com"
}
注意:由于我们是通过AOP拦截,如果标注在类上,该类必须被Spring托管,如果标注在方法上,如果实现了接口,那么只能标注在实现了接口的抽象方法的方法上。
其他应用
- 在实现读写分离的时候,可以使用mybatis拦截器,拦截query和update方法,然后再根据自己的规则选择不同的数据源
- 如果是mysql集群,数据是分散到不同的数据库中,则可以使用AOP,根据数据的唯一值对数据库的数量取模,然后选择不同的数据源