一、引言
本文主要介紹一種優雅、安全、易用,支援事務管理的Spring Boot整合多資料來源的方式,本文中不針對多資料來源是什麼、為什麼用、什麼時候用做介紹,小夥伴可根據自身情況酌情采納
溫馨提示:
基於以下知識有一定應用與實踐後,能更好地理解本篇文章
- Lambda、ThreadLocal、棧、佇列、自定義註解
- IoC、AOP、Druid、Maven、Spring Boot
由於本文主要講解程式碼的具體實現,程式碼與註釋較多,若感到閱讀體驗不佳,可配合開原始碼,使用程式碼編輯器進行閱讀
多資料來源Gitee地址
對應專案模組為hei-dynamic-datasource
二、大致思路
- 通過配置類與yml配置檔案先裝配好預設資料來源與多資料來源
- 再通過自定義註解與AOP,找到目標類或方法,並指定其使用的資料來源Key值
- 最後通過繼承AbstractRoutingDataSource類,返回經AOP處理後的資料來源Key值,從第一步裝配好的資料來源中找到對應配置並應用
三、測試用例
在類或方法上加上@DataSource("value")就可以指定不同資料來源
@Service
// 方法上的註解比類上註解優先順序更高
@DataSource("slave2")
public class DynamicDataSourceTestService {
@Autowired
private SysUserDao sysUserDao;
@Transactional
public void updateUser(Long id){
SysUserEntity user = new SysUserEntity();
user.setUserId(id);
user.setMobile("13500000002");
sysUserDao.updateById(user);
}
@Transactional
@DataSource("slave1")
public void updateUserBySlave1(Long id){
SysUserEntity user = new SysUserEntity();
user.setUserId(id);
user.setMobile("13500000001");
sysUserDao.updateById(user);
}
@DataSource("slave2")
@Transactional
public void updateUserBySlave2(Long id){
SysUserEntity user = new SysUserEntity();
user.setUserId(id);
user.setMobile("13500000003");
sysUserDao.updateById(user);
// 測試事務
int i = 1/0;
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceTest {
@Autowired
private DynamicDataSourceTestService dynamicDataSourceTestService;
@Test
public void test(){
Long id = 1L;
dynamicDataSourceTestService.updateUser(id);
dynamicDataSourceTestService.updateUserBySlave1(id);
dynamicDataSourceTestService.updateUserBySlave2(id);
}
}
四、專案結構
五、程式碼示例及解析
5.1、maven相關依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.2、yml配置
dynamic:
datasource:
slave1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hei?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
slave2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hei?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
5.3、自定義註解(DataSource)
// 定義作用範圍為(方法、介面、類、列舉、註解)
@Target({ElementType.METHOD, ElementType.TYPE})
// 保證執行時能被JVM或使用反射的程式碼使用
@Retention(RetentionPolicy.RUNTIME)
// 生成Javadoc時讓使用了@DataSource這個註解的地方輸出@DataSource這個註解或不同內容
@Documented
// 類繼承中讓子類繼承父類@DataSource註解
@Inherited
public @interface DataSource {
// @DataSource註解裡傳的參,這裡主要傳配置檔案中不同資料來源的標識,如@DataSource("slave1")
String value() default "";
}
5.4、切面類(DataSourceAspect)
// 宣告、定義切面類
@Aspect
@Component
/**
* 讓該bean的執行順序優先順序最高,並不能控制載入入IoC的順序
* 如果一個方法被多個 @Around 增強,那就可以使用該註解指定順序
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
// 指明通知在使用@DataSource註解標註下才觸發
@Pointcut("@annotation(io.renren.commons.dynamic.datasource.annotation.DataSource) " +
"|| @within(io.renren.commons.dynamic.datasource.annotation.DataSource)")
public void dataSourcePointCut() {
}
// 對通知方法的具體實現並採用環繞通知設定方法與切面的執行順序,即在方法執行前和後觸發
@Around("dataSourcePointCut()")
/**
* ProceedingJoinPoint繼承了JoinPoint,相較於JoinPoint暴露了proceed方法,該類僅配合實現around通知
* JoinPoint類,用來獲取代理類和被代理類的資訊
* 呼叫proceed方法,表示繼續執行目標方法(即加了@DataSource註解的方法)
*/
public Object around(ProceedingJoinPoint point) throws Throwable {
// 通過反射獲得被代理類(目標物件)
Class targetClass = point.getTarget().getClass();
System.out.println("targetClass:" + targetClass);
/**
* 獲得被代理類(目標物件)的方法簽名
* signature加簽是一種簡單、 低成本、保障資料安全的方式
*/
MethodSignature signature = (MethodSignature) point.getSignature();
/**
* 獲得被代理類(目標物件)的方法
* 這裡獲得方法也可以通過反射和getTarget(),但步驟更多更復雜
*/
Method method = signature.getMethod();
System.out.println("method:" + method);
// 獲得被代理類(目標物件)的註解物件
DataSource targetDataSource = (DataSource) targetClass.getAnnotation(DataSource.class);
System.out.println("targetDataSource:" + targetDataSource);
// 獲得被代理類(目標物件)的方法的註解物件
DataSource methodDataSource = method.getAnnotation(DataSource.class);
System.out.println("methodDataSource:" + methodDataSource);
// 判斷被代理類(目標物件)的註解物件或者被代理類(目標物件)的方法的註解物件不為空
if (targetDataSource != null || methodDataSource != null) {
String value;
// 優先用被代理類(目標物件)的方法的註解物件的值進行後續賦值
if (methodDataSource != null) {
value = methodDataSource.value();
} else {
value = targetDataSource.value();
}
/**
* DynamicContextHolder是自己實現的棧資料結構
* 將註解物件的值入棧
*/
DynamicContextHolder.push(value);
logger.debug("set datasource is {}", value);
}
try {
// 繼續執行被代理類(目標物件)的方法
return point.proceed();
} finally {
// 清空棧中資料
DynamicContextHolder.poll();
logger.debug("clean datasource");
}
}
}
5.5、多資料來源上下文操作支援類(DynamicContextHolder)
public class DynamicContextHolder {
/**
* Lambda構造 本地執行緒變數
* 用於避免多次建立資料庫連線或者多執行緒使用同一個資料庫連線
* 減少資料庫連線建立關閉對程式執行效率的影響與伺服器壓力
*
* 這裡使用陣列佇列實現棧資料結構
* 主要為了解決呼叫鏈執行順序問題,比如這裡有ABC三個方法,它們分別使用的是不同資料來源
* A呼叫B,B呼叫C,這裡資料來源切換時就要保證,C方法資料來源要最先被清除,再是B,最後A,即後進先出LIFO
*/
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = ThreadLocal.withInitial(ArrayDeque::new);
/**
* 獲得當前執行緒資料來源
*
* @return 資料來源名稱
*/
public static String peek() {
return CONTEXT_HOLDER.get().peek();
}
/**
* 設定當前執行緒資料來源
*
* @param dataSource 資料來源名稱
*/
public static void push(String dataSource) {
CONTEXT_HOLDER.get().push(dataSource);
}
/**
* 清空當前執行緒資料來源
*/
public static void poll() {
Deque<String> deque = CONTEXT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
CONTEXT_HOLDER.remove();
}
}
}
5.6、多資料來源類(DynamicDataSource)
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 返回當前上下文環境的資料來源key
* 後續會根據這個key去找到對應的資料來源屬性
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicContextHolder.peek();
}
}
5.7、多資料來源配置類(DynamicDataSourceConfig)
/**
* 通過@EnableConfigurationProperties(DynamicDataSourceProperties.class)
* 將DynamicDataSourceProperties.class注入到Spring容器中
*/
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
// 這裡properties已經包含了yml配置中所對應的多資料來源的屬性了
@Autowired
private DynamicDataSourceProperties properties;
/**
* 通過@ConfigurationProperties與@Bean,將yml配置檔案關於druid中的屬性配置,轉化成bean,並將bean注入到容器中
* 這裡作用是通過autowire作為引數應用到下面的dynamicDataSource()方法中
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
/**
* 通過@Bean告知Spring容器,該方法會返回DynamicDataSource物件
* 通過dynamicDataSource()配置多資料來源選擇邏輯,主要配置目標資料來源和預設資料來源
*/
@Bean
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
// 例項化自己實現的多資料來源,其中實現了獲取當前執行緒資料來源名稱的方法
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 設定多資料來源屬性
dynamicDataSource.setTargetDataSources(getDynamicDataSource());
// 工廠方法建立Druid資料來源
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
// 設定預設資料來源屬性
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
private Map<Object, Object> getDynamicDataSource(){
Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
});
return targetDataSources;
}
}
5.8、多資料來源工廠類(DynamicDataSourceFactory)
// 這裡訪問許可權是包私有
class DynamicDataSourceFactory {
static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(properties.getValidationQuery());
druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
try {
druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
}
}
5.9、資料來源屬性類(DataSourceProperties)
public class DataSourceProperties {
/**
* 可動態配置的資料庫連線屬性
*/
private String driverClassName;
private String url;
private String username;
private String password;
/**
* Druid預設引數
*/
private int initialSize = 2;
private int maxActive = 10;
private int minIdle = -1;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
private String validationQuery = "select 1";
private int validationQueryTimeout = -1;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = true;
private boolean poolPreparedStatements = false;
private int maxOpenPreparedStatements = -1;
private boolean sharePreparedStatements = false;
private String filters = "stat,wall";
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getInitialSize() {
return initialSize;
}
public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}
public int getMaxActive() {
return maxActive;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public long getMaxWait() {
return maxWait;
}
public void setMaxWait(long maxWait) {
this.maxWait = maxWait;
}
public long getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public long getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public long getMaxEvictableIdleTimeMillis() {
return maxEvictableIdleTimeMillis;
}
public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
}
public String getValidationQuery() {
return validationQuery;
}
public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}
public int getValidationQueryTimeout() {
return validationQueryTimeout;
}
public void setValidationQueryTimeout(int validationQueryTimeout) {
this.validationQueryTimeout = validationQueryTimeout;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public boolean isTestOnReturn() {
return testOnReturn;
}
public void setTestOnReturn(boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}
public boolean isTestWhileIdle() {
return testWhileIdle;
}
public void setTestWhileIdle(boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}
public boolean isPoolPreparedStatements() {
return poolPreparedStatements;
}
public void setPoolPreparedStatements(boolean poolPreparedStatements) {
this.poolPreparedStatements = poolPreparedStatements;
}
public int getMaxOpenPreparedStatements() {
return maxOpenPreparedStatements;
}
public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) {
this.maxOpenPreparedStatements = maxOpenPreparedStatements;
}
public boolean isSharePreparedStatements() {
return sharePreparedStatements;
}
public void setSharePreparedStatements(boolean sharePreparedStatements) {
this.sharePreparedStatements = sharePreparedStatements;
}
public String getFilters() {
return filters;
}
public void setFilters(String filters) {
this.filters = filters;
}
}
5.10、多資料來源屬性類(DynamicDataSourceProperties)
/**
* 通過@ConfigurationProperties指定讀取yml的字首關鍵字
* 配合setDatasource(),即讀取dynamic.datasource下的配置,將配置屬性轉化成bean
* 容器執行順序是,在bean被例項化後,會呼叫後置處理,遞迴的查詢屬性,通過反射注入值
*
* 由於該類只在DynamicDataSourceConfig類中使用,沒有其它地方用到,所以沒有使用@Component
* 而是在DynamicDataSourceConfig類中用@EnableConfigurationProperties定義為bean
*/
@ConfigurationProperties(prefix = "dynamic")
public class DynamicDataSourceProperties {
private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>();
public Map<String, DataSourceProperties> getDatasource() {
return datasource;
}
public void setDatasource(Map<String, DataSourceProperties> datasource) {
this.datasource = datasource;
}
}
六、最後
以上程式碼均已提交到開源專案中,對應專案模組為hei-dynamic-datasource
有需要的小夥伴可點選下方連結,clone程式碼到本地
多資料來源Gitee地址