背景:
公司有多套環境,每個環境執行數據庫腳本的時機也不一樣,久而久之,不同環境相同表的結構就有了差異,需要做一個工具進行對比。
分析:
同一套環境下有很多數據庫,不同環境的數據庫連接肯定也是不一樣的,那麼如何做到查詢指定環境下的某一個數據庫,需要動態的去切換數據源,根據當前的查詢條件路由對應的數據庫。可以使用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);
}
}