背景: 公司有多套环境,每个环境执行数据库脚本的时机也不一样,久而久之,不同环境相同表的结构就有了差异,需要做一个工具进行对比。
分析: 同一套环境下有很多数据库,不同环境的数据库连接肯定也是不一样的,那么如何做到查询指定环境下的某一个数据库,需要动态的去切换数据源,根据当前的查询条件路由对应的数据库。可以使用AOP在执行SQL前切换数据源。
实现逻辑:
- 用 AbstractRoutingDataSource 实现动态切换数据源,向容器中注册自定义的 AbstractRoutingDataSource 实现类(AbstractRoutingDataSource的相关介绍,我这篇文章有介绍,点我)。
- 用 ThreadLocal 线程级别变量存放当前数据源key。
- 利用 AOP 在执行SQL时切换数据源。
我这里因为公司使用的是阿里的DRDS,数据库是按照用户名进行隔离的,一个用户名对应一个数据库,所以多套环境下数据库的url连接都是一样的,只是连接的账号密码不一样,所以只需要把账号和密码配置在 application.xml 中。
spring:
datasource:
default:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxxxx/xxx?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: xxx
password: xxx
databaselist:
- database: dev_xx1
username: dev_xx1
password: dev_123
- database: dev_xx2
username: dev_xx2
password: dev_123
- database: test_xx1
username: test_xx1
password: test_123
- database: test_xx2
username: test_xx2
password: test_123
mybatis:
mapper-locations: classpath:/mapper/*.xml
DataSourceContextHolder
/**
* 数据源上下文
*/
@Slf4j
@UtilityClass
public class DataSourceContextHolder {
/**
* 线程级别的私有变量
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public String getDataSourceName() {
return CONTEXT_HOLDER.get();
}
public void setDataSourceName(String name) {
log.info("切换到:{}数据源", name);
CONTEXT_HOLDER.set(name);
}
public void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
RoutingDataSource
/**
* 数据源动态切换路由
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceName();
}
}
RoutingDataSource 实现 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法,从 DataSourceContextHolder 获取当前数据源 key,实现切换数据源。
DataSourceRegister
/**
* 数据源注册实现类
* ImportBeanDefinitionRegistrar 用来注册bean实例。
* EnvironmentAware 用来读取配置。
*/
@Slf4j
public class DataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private static final Map<String, DataSource> DATA_SOURCE_MAP = new HashMap<>();
/**
* 数据库连接url
*/
private static final String URL = "jdbc:mysql://xxx/%s?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true";
/**
* 数据库驱动
*/
private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";
/**
* 参数绑定工具
*/
private Binder binder;
@Override
public void setEnvironment(Environment environment) {
this.binder = Binder.get(environment);
}
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
// 注册默认的DataSource(这里是取我们yml里面配置的默认数据源相关配置,注意读取的key)
Map<String, String> defaultDataSourceProperties = binder.bind("spring.datasource.default", Map.class).get();
DataSource defaultDataSource = buildDataSource(defaultDataSourceProperties.get("DRIVER_CLASS_NAME"), defaultDataSourceProperties.get("URL"), defaultDataSourceProperties.get("username"), defaultDataSourceProperties.get("PASSWORD"));
// 自定义的DataSource
initDataSource();
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(RoutingDataSource.class);
// 为 RoutingDataSource 添加属性
MutablePropertyValues values = beanDefinition.getPropertyValues();
values.add("defaultTargetDataSource", defaultDataSource);
values.add("targetDataSources", DATA_SOURCE_MAP);
// 注册到 IOC 容器
beanDefinitionRegistry.registerBeanDefinition("datasource", beanDefinition);
}
/**
* 初始化自定义DataSource
*/
private void initDataSource() {
// 获取配置的数据库连接名
List<Map> databaseMapList = binder.bind("spring.datasource.databaselist", Bindable.listOf(Map.class)).get();
for (Map databaseMap : databaseMapList) {
log.info("开始注册数据源:{}", databaseMap.get("database"));
// 因为我的数据库连接URL都是一样,不同的是连接的数据库名,所以这里取到数据库名直接format
String jdbcUrl = String.format(URL, databaseMap.get("database"));
String username = databaseMap.get("username").toString();
String password = databaseMap.get("password").toString();
DATA_SOURCE_MAP.put(username, buildDataSource(DRIVER_CLASS_NAME, jdbcUrl, username, password));
}
}
/**
* 构建DataSource
*
* @param driverClassName 数据库驱动名称
* @param url URL
* @param username 用户名
* @param password 密码
* @return DataSource对象
*/
private DataSource buildDataSource(String driverClassName, String url, String username, String password) {
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(HikariDataSource.class);
return factory.build();
}
}
DataSourceRegister 主要是将我们自定义的 RoutingDataSource 注册到 IOC 容器中(这步注册的操作有多种方式)。
- DataSourceRegister 实现了 ImportBeanDefinitionRegistrar 接口,重写 registerBeanDefinitions() 向容器中注册bean。
- DataSourceRegister 实现了 EnvironmentAware 接口,重写 setEnvironment() 获取参数绑定对象,读取配置。
- 注册 RoutingDataSource 需要设置 defaultTargetDataSource 和 targetDataSources 两个属性。
ChangeDataSourceAspect
/**
* 数据源动态切换切面类
*
*/
@Aspect
@Component
public class ChangeDataSourceAspect {
/**
* 切点,dao 层下 comparetable 包下所有的接口
*/
@Pointcut("execution(public * com.xxx.dao.comparetable.*.*(..))")
public void point(){}
/**
* 在执行SQL前执行
*/
@Before("point()")
public void changeDataSource(JoinPoint point) {
Object[] args = point.getArgs();
// 从切点中获取参数
BaseQueryCriteria criteria = (BaseQueryCriteria) args[0];
// 从参数中获取数据源key
DataSourceContextHolder.setDataSourceName(criteria.getSchemaName());
}
/**
* 在执行SQL后,清除上一次的数据源key
*/
@After("point()")
public void clear() {
DataSourceContextHolder.clearDataSource();
}
}
ChangeDataSourceAspect 切面类会对dao层的请求做一个预处理,在执行SQL之前,执行 DataSourceContextHolder 的 setDataSourceName() 接口,根据参数将当前的数据源key保存下来。RoutingDataSource 会从 DataSourceContextHolder 获取到这个key。
RoutingDataSource 的父类 AbstractRoutingDataSource 调用基类已实现的 determineCurrentLookupKey() 获取数据源key,从 resolvedDataSources 中获取这个key对应的数据源对象。
然后调用这个数据源的 getConnection()方法获取数据库连接。
这样就完成了动态切换数据源,实现对指定的数据源进行写读操作,不用重启服务。
经过以上配置,在启动类上需要加上 @Import(DataSourceRegister.class) 才能开始使用。
@SpringBootApplication
@Import(DataSourceRegister.class)
@MapperScan("com.xxx.dao")
public class BigBangApplication {
public static void main(String[] args) {
SpringApplication.run(BigBangApplication.class, args);
}
}