业务场景:项目中因为环境的迁移以及使用通用mapper等,我们会在配置文件中指定默认数据源链接。但是在为了实现单个业务跨库查询,现在使用配置动态数据源来完成。
效果展示:
在不同库建立相同的表格,然后使用postman测试结果
主数据源:
从属数据源:
未使用注解切换数据源的测试效果:
使用数据源注解切换数据源的测试效果:
如上效果,该方法成功切换到所需的数据库并查询出数据。
接下来讲解具体配置和代码:
TargetDataSource:自定义注解,直接作用于所需修改数据源的方法上。
package com.changrong.common.datasource.annotation;
import java.lang.annotation.*;
/**
* @Author wanxu
* @Date 2021-08-27
* @Description 作用于类、接口或者方法上
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name();
}
DynamicDataSource:每次和数据库交互时,动态获取数据源信息
package com.changrong.common.datasource.config;
import com.changrong.common.datasource.support.DynamicDataSourceContextHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Author wanxu
* @Date 2021-08-27
* @Description 动态数据源
* AbstractRoutingDataSource(每执行一次数据库,动态获取DataSource)
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
DynamicDataSourceContextHolder:动态数据源上下文管理,将上下文信息可视化读取出来。
package com.changrong.common.datasource.support;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* @Author wanxu
* @Date 2021-08-27
* @Description 动态数据源上下文管理
*/
public class DynamicDataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
//存放当前线程使用的数据源类型信息
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
//存放数据源id
public static List<String> dataSourceIds = new ArrayList<String>();
//设置数据源
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
//获取数据源
public static String getDataSourceType() {
return contextHolder.get();
}
//清除数据源
public static void clearDataSourceType() {
contextHolder.remove();
}
//判断当前数据源是否存在
public static boolean isContainsDataSource(String dataSourceId) {
return dataSourceIds.contains(dataSourceId);
}
}
DynamicDataSourceRegister:根据配置的数据源信息对应修改上下文中的配置文件。
package com.changrong.common.datasource.support;
import com.changrong.common.datasource.config.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Author wanxu
* @Date 2021-08-27
* @Description 注册动态数据源
* 初始化数据源和提供了执行动态切换数据源的工具类
* EnvironmentAware(获取配置文件配置的属性值)
*/
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);
//指定默认数据源(springboot2.0默认数据源是hikari如何想使用其他数据源可以自己配置)
private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
//默认数据源
private DataSource defaultDataSource;
//用户自定义数据源
private Map<String, DataSource> slaveDataSources = new HashMap<>();
@Override
public void setEnvironment(Environment environment) {
initDefaultDataSource(environment);
initslaveDataSources(environment);
}
private void initDefaultDataSource(Environment env) {
// 读取主数据源
Map<String, Object> dsMap = new HashMap<>();
dsMap.put("driver", env.getProperty("spring.datasource.driver"));
dsMap.put("url", env.getProperty("spring.datasource.url"));
dsMap.put("username", env.getProperty("spring.datasource.username"));
dsMap.put("password", env.getProperty("spring.datasource.password"));
defaultDataSource = buildDataSource(dsMap);
}
private void initslaveDataSources(Environment env) {
// 读取配置文件获取更多数据源
String dsPrefixs = env.getProperty("slave.datasource.names");
for (String dsPrefix : dsPrefixs.split(",")) {
// 多个数据源
Map<String, Object> dsMap = new HashMap<>();
dsMap.put("driver", env.getProperty("slave.datasource." + dsPrefix + ".driver"));
dsMap.put("url", env.getProperty("slave.datasource." + dsPrefix + ".url"));
dsMap.put("username", env.getProperty("slave.datasource." + dsPrefix + ".username"));
dsMap.put("password", env.getProperty("slave.datasource." + dsPrefix + ".password"));
DataSource ds = buildDataSource(dsMap);
slaveDataSources.put(dsPrefix, ds);
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//添加默认数据源
targetDataSources.put("dataSource", this.defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
//添加其他数据源
targetDataSources.putAll(slaveDataSources);
for (String key : slaveDataSources.keySet()) {
DynamicDataSourceContextHolder.dataSourceIds.add(key);
}
//创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
mpv.addPropertyValue("targetDataSources", targetDataSources);
//注册 - BeanDefinitionRegistry
beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
logger.info("Dynamic DataSource Registry");
}
public DataSource buildDataSource(Map<String, Object> dataSourceMap) {
try {
Object type = dataSourceMap.get("type");
if (type == null) {
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource
}
Class<? extends DataSource> dataSourceType;
dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
String driverClassName = dataSourceMap.get("driver").toString();
String url = dataSourceMap.get("url").toString();
String username = dataSourceMap.get("username").toString();
String password = dataSourceMap.get("password").toString();
// 自定义DataSource配置
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
DynamicDattaSourceAspect:切面文件,配合自定义注解完成对方法的前置切换数据源操作
package com.changrong.common.datasource.support;
import com.changrong.common.datasource.annotation.TargetDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @Author wanxu
* @Date 2021-08-27
* @Description 动态数据源通知
*/
@Aspect
@Order(-1)//保证在@Transactional之前执行
@Component
@Slf4j
public class DynamicDattaSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDattaSourceAspect.class);
//改变数据源
@Before("@annotation(targetDataSource)")
public void changeDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
String dbid = targetDataSource.name();
if (!DynamicDataSourceContextHolder.isContainsDataSource(dbid)) {
//joinPoint.getSignature() :获取连接点的方法签名对象
logger.error("数据源 " + dbid + " 不存在使用默认的数据源 -> " + joinPoint.getSignature());
} else {
logger.debug("使用数据源:" + dbid);
DynamicDataSourceContextHolder.setDataSourceType(dbid);
}
}
@After("@annotation(targetDataSource)")
public void clearDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
logger.debug("清除数据源 " + targetDataSource.name() + " !");
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
详细使用步骤
-
首先找到需要配置动态数据源的服务,引入刚才配置好的common服务
-
在对应服务的启动类上导入common服务中support的三个所需的文件
如果配置文件和业务代码在统一项目服务路径下,可以忽略以上两步
3.找到需要动态切换数据源的服务的配置文件,将自己的所需的从属数据源加上,格式如下:
spring:
redis:
database: 0
host: 192.168.1.138
port: 6379
password: unknown
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.138:3306/tianji?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: unknown
password: unknown
mybatis-plus:
configuration:
# 驼峰下划线转换
map-underscore-to-camel-case: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局参数设置
ribbon:
ReadTimeout: 120000
ConnectTimeout: 10000
SocketTimeout: 10000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
slave:
datasource:
names: tianjiboot
tianjiboot:
type: com.alibaba.druid.pool.DruidDataSource
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.138:3306/tianjiboot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: unknown
password: unknown
注意不要覆盖原本的主数据源配置。其中slave标识这是从属数据源,之前的配置文件会读取slave下面的配置。names标识每一个从属数据源的名字,是可以配置多个的,以逗号分割。从属数据源下面具体配置就和主数据源一样,type表示使用的数据源,springboot2.0以上默认使用hikari,driver表示数据库驱动,url就是你需要访问的数据源链接,username和password就是对应链接的用户名和密码。
4.找到所需修改数据源的方法(一般是dao层实现类)
打上之前写的自定义注解,并将name指向从属数据源配置下的某个name
最后测试结果如文章开头所示
简单原理介绍
- 首先加入注解后,代码执行到指定地方,会触发springAOP的切面通知
会根据注解传来的name值取yaml文件找是否有对应的数据源,没有的话就使用默认数据源。
2. 找到数据源后,读取对应配置并将其写入到数据源上下文配置中,完成对应属性的赋值
3.完成以上切换数据源的步骤后,等待追加自定义注解的方法执行完毕,同时触发springAOP中的后置通知。
目的是为了不影响其他应用在读取数据源配置的时候错误读取到了刚才注入的动态数据源配置