开始配置多数据源
首先我们需要先移除自动配置数据源,SpringBoot 的默认行为
DataSourceAutoConfiguration 是 SpringBoot 提供的自动配置类
@SpringBootApplication(scanBasePackages = "com", exclude = {DataSourceAutoConfiguration.class})
做完这一步,我们要保存数据源的存储位置
这个ThreadLocal是在一个线程中存储数据源,getDataSourceKey 用于获取当前线程存储的数据源名称,setDataSourceKey 是设置当前线程存储的数据源名称。clearDataSourceKey 用于清除,这样做的好处是避免线程安全问题。
/**
* 数据源存储位置
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> db = new ThreadLocal<>();
public static String getDataSourceKey() {
return db.get();
}
public static void setDataSourceKey(String key) {
db.set(key);
}
public static void clearDataSourceKey() {
db.remove();
}
}
下面首先读取了默认数据源的信息,通过 getProperties() 封装成一个 Map 对象
定义了一个名为 dynamicDataSource 的 bean 是数据源路由器,它继承了 AbstractRoutingDataSource 类,我们可以根据规则选择要使用的数据源。
dataSource 方法接受一个 Map,并返回一个数据源,于是我们就创建了一个数据源
这段代码通过设置,配置了一个支持动态切换的数据源,默认使用 master,用于运行时动态切换数据源。
@Configuration
@Slf4j
public class DataSourceConfigurer {
@Value("${spring.datasource.druid.master.url}")
private String url;
@Value("${spring.datasource.druid.master.username}")
private String username;
@Value("${spring.datasource.druid.master.password}")
private String password;
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
/**
* 获取数据源配置信息
*
* @return 数据源配置信息
*/
private Map<String, Object> getProperties() {
Map<String, Object> map = new HashMap<>();
map.put("driverClassName", driverClassName);
map.put("url", url);
map.put("username", username);
map.put("password", password);
return map;
}
/**
* 配置动态数据源
*
* @return 动态数据源
*/
@Bean("dynamicDataSource")
public DynamicRoutingDataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
// 创建数据源
DataSource dataSource = dynamicRoutingDataSource.dataSource(getProperties());
// 设置数据源映射
Map<Object, Object> dataSourceMap = new HashMap<>(1);
dataSourceMap.put("default_db", dataSource);
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
// 设置默认数据源
dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource);
return dynamicRoutingDataSource;
}
}
具体切换多数据源的逻辑 DynamicRoutingDataSource
多数据源切换的实现的关键是 AbstractRoutingDataSource
我们这里创建了一个 targetTargetDataSources 是一个 Map 存储数据源的数据,key 数据源的标识,value 是具体的数据源。通过 setTargetDataSources 可以设置数据源,在运行时,通过该方法切换。
通过 addDataSource 添加数据源,并通过 setTargetDataSources 使数据源生效。
existDataSource 是判断数据源是否存在
dataSource 这个是重要的方法,它是为数据源在druid 连接池中创建了资源,配置了数据源。
/**
* 动态数据源
*/
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private static Map<Object, Object> targetTargetDataSources = new ConcurrentHashMap<>();
@Override
protected Object determineCurrentLookupKey() {
// 每次连接数据库,都会去设置数据源
return DynamicDataSourceContextHolder.getDataSourceKey();
}
// 设置targetDataSources并记录数据源(这里可以记录每个数据源的最近使用时间,可以做删除不经常使用的数据源)
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
targetTargetDataSources = targetDataSources;
}
// 更新数据源
public void addDataSource(String tenant, Map<String, Object> dataSourceProperties) {
targetTargetDataSources.put(tenant, dataSource(dataSourceProperties));
super.setTargetDataSources(targetTargetDataSources);
afterPropertiesSet();
}
// 判断是否存在数据源,存在直接取
public boolean existDataSource(String tenant) {
return targetTargetDataSources.containsKey(tenant);
}
// 组装数据源
public DataSource dataSource(Map<String, Object> dataSourceProperties) {
DataSource dataSource;
try {
dataSource = DruidDataSourceFactory.createDataSource(dataSourceProperties);
} catch (Exception e) {
log.error("dataSource: {}", e.getMessage());
throw new RuntimeException();
}
return dataSource;
}
}
实践切换
准备一组数据,存入数据源的信息
Map<String, Object> map = new HashMap<>();
map.put("driverClassName", "com.mysql.cj.jdbc.Driver");
map.put("url", "jdbc:mysql://localhost:3306/ec_e3356vva?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8");
map.put("username", "ec_e3356vva");
map.put("password", "1234");
首先判断数据源中是否存在,如果不存在,就不加入新的数据源,一般在登录的时候就加入了
// 设置数据源信息
dynamicRoutingDataSource.addDataSource(tenant, map);
切面切换数据源
- 配置切面
- 首先我们定义一个注解
- 获取注解进行切换数据源
/**
* 自定义多数据源切换注解
*/
@Target({ElementType.METHOD, ElementType.TYPE}) // 修饰方法,修饰类,修饰接口
@Retention(RetentionPolicy.RUNTIME) // 在运行时可以处理
@Documented
@Inherited // 允许继承
public @interface DataSource {
/**
* 切换数据源名称
*/
DataSourceType value() default DataSourceType.MASTER;
}
我们在下面类中,开始处理数据源切换,首先要加切面注解
/**
* 多数据源处理
*/
@Aspect
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {
}
定义切点,首先 @annotation(com.ec.annotation.DataSource) 表示匹配带有注解的方法,而@within(com.ec.common.annotation.DataSource) 表示匹配带有注解的类上的所有方法。
@Pointcut("@annotation(com.ec.annotation.DataSource)"
+ "|| @within(com.ec.common.annotation.DataSource)")
public void dsPointCut() {
}
环绕通知,这里表示切到的方法,首先我们判断有没有 DataSource 注解,如果有执行切换数据源
首先我们通过自定义方法 getDataSource 获取注解(方法信息在下面),通过 (MethodSignature) point.getSignature() 获取到当前方法的签名信息,通过 getMethod 方法获取当前方法信息。将方法提交到 Spring 提供的注解工具类中,使用AnnotationUtils.findAnnotation 方法找到指定的注解。
如果这个时候找到了,直接返回,没找到,说明可能这个注解是在类上的,所以我们通过 signature.getDeclaringType() 获取当前类,再使用 AnnotationUtils.findAnnotation 在类上查找。
如果 该注解存在,执行切换数据源,最后销毁数据源即可。
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (StringUtils.isNotNull(dataSource)) {
DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().name());
}
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}
/**
* 获取需要切换的数据源
*/
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);
}