在作項目的時候,幾乎都會用到數據庫,不少時候就只連一個數據庫,可是有時候咱們須要一個項目操做多個數據庫,不一樣的業務功能產生的數據存到不一樣的數據庫,那怎麼來實現數據源的動態、靈活的切換呢?今天咱們就來實現這個功能。java
前期準備工做
咱們須要有一臺聯網的電腦(用於maven自動下載依賴),而且電腦安裝JDK 八、IDEA、MySQL數據庫、maven,首先建立一個springboot項目(SSM也行)。springboot版本和SSM版本的代碼都已經放到碼雲託管。感興趣的能夠去下載https://gitee.com/itwalking/springboot-dynamic-datasource,https://gitee.com/itwalking/ssm-dynamic-datasourcemysql
實現思路
首先講一下咱們的實現思路,平時咱們作項目,都會用到spring
來集成咱們的數據源,鏈接mysql
或Oracle
數據庫,經過暴露出DataSource
相關的接口,而後不一樣的數據庫均可以集成過來,咱們只須要配置數據源的四大參數便可,這是咱們往常的作法。而若是使用動態數據源的話,Spring
也爲咱們提供了相應的擴展點,那就是AbstractRoutingDataSource
抽象類,它一樣是jdbc
的DataSource
接口的實現類。git
代碼
廢話很少說,咱們直接上代碼。
建立咱們本身的數據源DynamicDataSource
繼承AbstractRoutingDataSource
,實現它的抽象方法determineCurrentLookupKey
,這個方法其實就是實現動態選擇數據源的關鍵,經過這個方法返回的對象關聯到咱們的數據源。web
package com.walking.db;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author walking
* 公衆號:編程大道
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* ThreadLocal 用於提供線程局部變量,在多線程環境能夠保證各個線程裏的變量獨立於其它線程裏的變量。
* 也就是說 ThreadLocal 能夠爲每一個線程建立一個【單獨的變量副本】,至關於線程的 private static 類型變量。
*/
private static final ThreadLocal<DataSourceName> dataSourceName = new ThreadLocal<DataSourceName>();
/**
* 支持以包名的粒度選擇數據源
*/
private static final Map<String,DataSourceName> packageDataSource = new HashMap<>();
public DynamicDataSource(DataSource firstDataSource, Map<Object, Object> targetDataSources) {
setDefaultTargetDataSource(firstDataSource);
setTargetDataSources(targetDataSources);
afterPropertiesSet();
}
/**
* 獲取與線程上下文綁定的數據源名稱(存儲在ThreadLocal中)
* @return 返回數據源名稱
*/
@Override
protected Object determineCurrentLookupKey() {
DataSourceName dsName = dataSourceName.get();
dataSourceName.remove();
return dsName;
}
public static void setDataSourceName(DataSourceName dataSource){
dataSourceName.set(dataSource);
}
public static void usePackageDatasourceKey(String pkName) {
dataSourceName.set(packageDataSource.get(pkName));
}
public Map<String,DataSourceName> getPackageDatasource(){
return packageDataSource;
}
public void setPackageDatasource(Map<String,DataSourceName> packageDatasource){
this.packageDataSource.putAll(packageDatasource);
}
}
DynamicDataSource
中有一個ThreadLocal
用來保存咱們當前選擇的數據源名稱,代碼中的註釋寫的很清楚了。其中ThreadLocal
的泛型是DataSourceName
,DataSourceName
是咱們本身定義的一個枚舉類,用於定義咱們的數據源名稱,我這裏拿兩個數據源作演示,並命名爲FIRST
, SECOND
spring
package com.walking.db;
/**
* @author walking
* 公衆號:編程大道
*/
public enum DataSourceName {
FIRST, SECOND;
}
而後自定義一個註解,用於標註咱們操做數據庫時選擇哪一個數據源,很簡單隻有一個name屬性,默認是 DataSourceName.FIRST
sql
@Target({ElementType.PACKAGE,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurDataSource {
/**
* name of DataSource
* @return
*/
DataSourceName value() default DataSourceName.FIRST;
}
有了這些還不夠,咱們還須要根據咱們的註解裏的name屬性動態的去修改 DynamicDataSource
中 ThreadLocal
中保存的數據庫名稱,每次執行SQL前都要修改數據源,這樣才能達到修改數據源的目的。那很顯然咱們就須要spring AOP
來完成這個操做了。數據庫
以下,DynamicDataSourceAspect
是咱們定義的一個切面類,同時也定了三個切點,分別去切方法上帶@CurDataSource
註解的方法,類上帶@CurDataSource
註解的類,以及按包名去切。這樣,咱們的動態數據源就支持方法級別的、類級別的、包級別的動態配置了。編程
package com.walking.aaspect;
import com.walking.db.CurDataSource;
import com.walking.db.DataSourceName;
import com.walking.db.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* 動態數據源切面類
* 被切中的,則先判斷方法上是否有CurDataSource註解
* 而後判斷方法所屬類上是否有CurDataSource註解
* 其次判斷是否配置了包級別的數據源
*
* 優先級爲方法、類、包
* 若同時配置則優先按方法上的
*
* @author walking
* 公衆號:編程大道
*/
@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {
// pointCut
@Pointcut("@annotation(com.walking.db.CurDataSource)")
public void choseDatasourceByAnnotation() {
}
@Pointcut("@within(com.walking.db.CurDataSource)")
public void choseDatasourceByClass() {
}
@Pointcut("execution(* com.walking.service3..*(..))")
public void choseDatasourceByPackage() {
}
@Around("choseDatasourceByAnnotation() || choseDatasourceByClass() || choseDatasourceByPackage()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("進入AOP環繞通知");
Signature signature = joinPoint.getSignature();
DataSourceName datasourceName = getDatasourceKey(signature);
if (!Objects.isNull(datasourceName)) {
DynamicDataSource.setDataSourceName(datasourceName);
}
return joinPoint.proceed();
}
private DataSourceName getDatasourceKey(Signature signature) {
if (signature == null) {
return null;
} else {
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(CurDataSource.class)) {
return this.dsSettingInMethod(method);
}
Class<?> declaringClass = method.getDeclaringClass();
if (declaringClass.isAnnotationPresent(CurDataSource.class)) {
return this.dsSettingInConstructor(declaringClass);
}
Package aPackage = declaringClass.getPackage();
this.dsSettingInPackage(aPackage);
}
return null;
}
}
private DataSourceName dsSettingInConstructor(Class<?> declaringClass) {
CurDataSource dataSource = declaringClass.getAnnotation(CurDataSource.class);
return dataSource.value();
}
private DataSourceName dsSettingInMethod(Method method) {
CurDataSource dataSource = method.getAnnotation(CurDataSource.class);
return dataSource.value();
}
private void dsSettingInPackage(Package pkg) {
DynamicDataSource.usePackageDatasourceKey(pkg.getName());
}
}
仔細看一下這個切面類的環繞通知這個方法的邏輯,能夠發現,咱們首先看的是方法上的註解,而後再看類上的註解,最後看是否配置了包級別數據源。
基本上,該有的類咱們都寫完了,剩下就是驗證。springboot
驗證以前咱們還須要進行一些配置。微信
配置多數據源
這裏,咱們使用的是阿里的Druid數據源,用springboot自帶的也行。咱們能夠看到在Druid:
配置下,本來直接就配置url、name這些參數,咱們新增了一級分別是first和second,用於配置多個數據源
server:
port: 9966
servlet:
context-path: /walking
spring:
mvc:
log-request-details: false
application:
name: walking
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
first:
url: jdbc:mysql://localhost:3306/walking_mybatis?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456ppzy,
second:
url: jdbc:mysql://localhost:3306/walking_mybatis2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456ppzy,
mybatis:
mapper-locations: classpath:mapper/*.xml
而後javaconfig配置類。
配置了三個bean,前兩個是數據源的bean,使用@ConfigurationProperties
註解,讓springboot幫咱們去配置文件讀取指定前綴的配置,這樣咱們剛纔配的兩個數據源參數就區分開了。
而後第三個bean是咱們配置的叫作dataSource的bean,用於覆蓋spring默認的DataSource
,在這個bean中,咱們把全部的數據源注入進去,這裏咱們有兩個,命名爲FIRST和SECOND,以及咱們要配置的包級別的數據源,而後調用構造函數建立DynamicDataSource
咱們的動態數據源。並指明瞭默認的數據源。
package com.walking.configuration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.walking.db.DataSourceName;
import com.walking.db.DynamicDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author walking
* 公衆號:編程大道
*/
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.first")
public DataSource firstDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.second")
public DataSource secondDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DataSourceName.FIRST, firstDataSource);
targetDataSources.put(DataSourceName.SECOND, secondDataSource);
//配置包級別的數據源
Map<String, DataSourceName> packageDataSource = new HashMap<>();
packageDataSource.put("com.walking.service3", DataSourceName.SECOND);
DynamicDataSource dynamicDataSource = new DynamicDataSource(firstDataSource, targetDataSources);
dynamicDataSource.setPackageDatasource(packageDataSource);
dynamicDataSource.afterPropertiesSet();
return dynamicDataSource;
}
}
而後就是咱們的啓動類了,咱們須要禁用掉spring的自動配置數據源,和Druid的自動配置數據源,使用咱們自定義的動態數據源。
@EnableAspectJAutoProxy
//關掉數據源自動配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
DruidDataSourceAutoConfigure.class})
//導入咱們本身的數據源配置
@Import({DynamicDataSourceConfig.class})
@MapperScan(basePackages = "com.walking.dao")
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
操做Mybatis
我就很少說了,這裏我在兩個數據庫(walking_mybatis和walking_mybatis2)裏建立了相同的user表,咱們測試的時候觀察插入到哪一個表就OK了。
項目總體結構
測試
咱們在save_1
上添加註解指明使用SECOND,在save_2
則沒有,UserService1
類上也沒用註解,一樣的,在配置類裏也沒配置UserService1
的包名,那麼save_2
將會使用默認的數據源那就是FIRST
controller
運行,訪問http://localhost:9966/walking/test01
日誌輸出
查看數據庫則第二個數據庫新增一條數據。
完整代碼我已上傳gitee碼雲,詳細的測試都在這三個service包下和test包下,感興趣的能夠去下載代碼看看。
實現動態數據源切換就是這麼簡單。下次咱們看一下動態數據源的原理。
總結一下
一、繼承AbstractRoutingDataSource實現多數據源及默認數據源的配置
二、註解+AOP,實現動態修改數據源的邏輯
三、排除spring和Druid(若是引入了第三方數據庫鏈接池)默認的自動配置數據源
動手操做下一下,SQL和項目都已上傳。
本文分享自微信公衆號 - 編程大道(learn_code)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。