SpringBoot、Mybatis配合AOP和註解實現動態數據源切換配置

點擊上方 "程序員小樂"關注公眾號, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約

每日英文

You’re human. Making mistakes is a part of life. It’s how you fix them that counts.

犯錯是人生的一部分,難以避免,重要的是,錯誤出現了之後,你怎樣去面對。

每日掏心話

人跟人之間的感情就像織毛衣,建立的時候一針一線,小心而漫長,拆除的時候只要輕輕一拉,就已經回不去。

鏈接:juejin.im/post/5d830944f265da03963bd153

SpringBoot、Mybatis配合AOP和註解實現動態數據源切換配置

程序員小樂(ID:study_tech)第 666 次推文 圖片來自網絡

往日回顧:編寫高性能Java代碼的最佳實踐

正文

0、前言

隨著應用用戶數量的增加,相應的併發請求的數量也會跟著不斷增加,慢慢地,單個數據庫已經沒有辦法滿足我們頻繁的數據庫操作請求了。

在某些場景下,我們可能會需要配置多個數據源,使用多個數據源(例如實現數據庫的讀寫分離)來緩解系統的壓力等,同樣的,Springboot官方提供了相應的實現來幫助開發者們配置多數據源,一般分為兩種方式(目前我所瞭解到的),分包和AOP。

而在Springboot +Mybatis實現多數據源配置中,我們實現了靜態多數據源的配置,但是這種方式怎麼說呢,在實際的使用中不夠靈活,為了解決這個問題,我們可以使用上文提到的第二種方法,即使用AOP面向切面編程的方式配合我們的自定義註解來實現在不同數據源之間動態切換的目的。

1、數據庫準備

數據庫準備仍然和之前的例子相同,具體建表sql語句則不再詳細說明,表格如下:

SpringBoot、Mybatis配合AOP和註解實現動態數據源切換配置

並分別插入兩條記錄,為了方便對比,其中testdatasource1為芳年25歲的張三, testdatasource2為芳年30歲的李四。

2、環境準備

首先新建一個Springboot項目,我這裡版本是2.1.7.RELEASE,並在pom文件中引入相關依賴,和上次相比,這次主要額外新增了aop相關的依賴,如下:

<dependency>
<groupid>mysql/<groupid>
<artifactid>mysql-connector-java/<artifactid>
/<dependency>

<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-jdbc/<artifactid>
/<dependency>


<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-aop/<artifactid>
<version>5.1.5.RELEASE/<version>
/<dependency>
<dependency>
<groupid>junit/<groupid>
<artifactid>junit/<artifactid>
/<dependency>
<dependency>
<groupid>org.aspectj/<groupid>
<artifactid>aspectjweaver/<artifactid>
<version>1.9.2/<version>
/<dependency>

3、代碼部分

首先呢,在我們Springboot的配置文件中配置我們的datasourse,和以往不一樣的是,因為我們有兩個數據源,所以要指定相關數據庫的名稱,其中主數據源為primary,次數據源為secondary如下:

#配置主數據庫
spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/testdatasource1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

##配置次數據庫
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/testdatasource2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver


spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true

需要我們注意的是,Springboot2.0 在配置數據庫連接的時候需要使用jdbc-url,如果只使用url的話會報jdbcUrl is required with driverClassName.錯誤。

新建一個配置文件,DynamicDataSourceConfig 用來配置我們相關的bean,代碼如下

@Configuration
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper", sqlSessionFactoryRef = "SqlSessionFactory") //basePackages 我們接口文件的地址
public class DynamicDataSourceConfig {

// 將這個對象放入Spring容器中
@Bean(name = "PrimaryDataSource")
// 表示這個數據源是默認數據源
@Primary
// 讀取application.properties中的配置參數映射成為一個對象
// prefix表示參數的前綴
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource getDateSource1() {
return DataSourceBuilder.create().build();
}



@Bean(name = "SecondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource getDateSource2() {
return DataSourceBuilder.create().build();
}


@Bean(name = "dynamicDataSource")
public DynamicDataSource DataSource(@Qualifier("PrimaryDataSource") DataSource primaryDataSource,
@Qualifier("SecondaryDataSource") DataSource secondaryDataSource) {

//這個地方是比較核心的targetDataSource 集合是我們數據庫和名字之間的映射
Map<object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceType.DataBaseType.Primary, primaryDataSource);
targetDataSource.put(DataSourceType.DataBaseType.Secondary, secondaryDataSource);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(primaryDataSource);//設置默認對象
return dataSource;
}


@Bean(name = "SqlSessionFactory")
public SqlSessionFactory SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*/*.xml"));//設置我們的xml文件路徑
return bean.getObject();
}
}
/<object>

而在這所有的配置中,最核心的地方就是DynamicDataSource這個類了,DynamicDataSource是我們自定義的動態切換數據源的類,該類繼承了AbstractRoutingDataSource 類並重寫了它的determineCurrentLookupKey()方法。

AbstractRoutingDataSource 類內部維護了一個名為targetDataSources的Map,並提供的setter方法用於設置數據源關鍵字與數據源的關係,實現類被要求實現其determineCurrentLookupKey()方法,由此方法的返回值決定具體從哪個數據源中獲取連接。同時AbstractRoutingDataSource類提供了程序運行時動態切換數據源的方法,在dao類或方法上標註需要訪問數據源的關鍵字,路由到指定數據源,獲取連接。

DynamicDataSource代碼如下:

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType();
return dataBaseType;
}

}

DataSourceType類的代碼如下:

public class DataSourceType {

//內部枚舉類,用於選擇特定的數據類型
public enum DataBaseType {
Primary, Secondary
}

// 使用ThreadLocal保證線程安全
private static final ThreadLocal<databasetype> TYPE = new ThreadLocal<databasetype>();

// 往當前線程裡設置數據源類型
public static void setDataBaseType(DataBaseType dataBaseType) {
if (dataBaseType == null) {
throw new NullPointerException();
}
TYPE.set(dataBaseType);
}

// 獲取數據源類型
public static DataBaseType getDataBaseType() {
DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.Primary : TYPE.get();
return dataBaseType;
}

// 清空數據類型
public static void clearDataBaseType() {
TYPE.remove();
}



}
/<databasetype>/<databasetype>

接下來編寫我們相關的Mapper和xml文件,代碼如下:

@Component
@Mapper
public interface PrimaryUserMapper {

List<user> findAll();
}


@Component
@Mapper
public interface SecondaryUserMapper {

List<user> findAll();
}
/<user>/<user>


br> PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper>

<select>
select * from sys_user;
/<select>

/<mapper>



br> PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper>

<select>
select * from sys_user2;
/<select>


/<mapper>

相關接口文件編寫好之後,就可以編寫我們的aop代碼了:

@Aspect
@Component
public class DataSourceAop {
//在primary方法前執行
@Before("execution(* com.jdkcb.mybatisstuday.controller.UserController.primary(..))")
public void setDataSource2test01() {
System.err.println("Primary業務");
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
}

//在secondary方法前執行
@Before("execution(* com.jdkcb.mybatisstuday.controller.UserController.secondary(..))")
public void setDataSource2test02() {
System.err.println("Secondary業務");
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary);
}
}

編寫我們的測試 UserController, 代碼如下:

@RestController
public class UserController {

@Autowired
private PrimaryUserMapper primaryUserMapper;
@Autowired
private SecondaryUserMapper secondaryUserMapper;


@RequestMapping("primary")
public Object primary(){
List<user> list = primaryUserMapper.findAll();
return list;
}
@RequestMapping("secondary")
public Object secondary(){
List<user> list = secondaryUserMapper.findAll();
return list;
}

}
/<user>/<user>

4、測試

啟動項目,在瀏覽器中分別輸入http://127.0.0.1:8080/primary 和http://127.0.0.1:8080/primary ,結果如下:

[{"user_id":1,"user_name":"張三","user_age":25}]

[{"user_id":1,"user_name":"李四","user_age":30}]

5、等等

等等,嘖嘖嘖,我看你這不行啊,還不夠靈活,幾個菜啊,喝成這樣,這就算靈活了?

那肯定不能的,aop也有aop的好處,比如兩個包下的代碼分別用兩個不同的數據源,就可以直接用aop表達式就可以完成了,但是,如果想本例中方法級別的攔截,就顯得優點不太靈活了,這個適合就需要我們的註解上場了。

6、配合註解實現

首先自定義我們的註解 @DataSource

/**
* 切換數據註解 可以用於類或者方法級別 方法級別優先級 > 類級別
*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "primary"; //該值即key值,默認使用默認數據庫


}

通過使用aop攔截,獲取註解的屬性value的值。如果value的值並沒有在我們DataBaseType裡面,則使用我們默認的數據源,如果有的話,則切換為相應的數據源。

@Aspect
@Component
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

@Before("@annotation(dataSource)")//攔截我們的註解
public void changeDataSource(JoinPoint point, DataSource dataSource) throws Throwable {
String value = dataSource.value();
if (value.equals("primary")){
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
}else if (value.equals("secondary")){
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary);
}else {
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);//默認使用主數據庫
}

}

@After("@annotation(dataSource)") //清除數據源的配置
public void restoreDataSource(JoinPoint point, DataSource dataSource) {
DataSourceType.clearDataBaseType();


}
}

7、測試

修改我們的mapper,添加我們的自定義的@DataSouse註解,並註解掉我們DataSourceAop類裡面的內容。如下:

@Component
@Mapper
public interface PrimaryUserMapper {



@DataSource
List<user> findAll();
}

@Component
@Mapper
public interface SecondaryUserMapper {

@DataSource("secondary")//指定數據源為:secondary
List<user> findAll();
}
/<user>/<user>

啟動項目,在瀏覽器中分別輸入http://127.0.0.1:8080/primary 和http://127.0.0.1:8080/primary ,結果如下:

[{"user_id":1,"user_name":"張三","user_age":25}]

[{"user_id":1,"user_name":"李四","user_age":30}]

到此,就算真正的大功告成啦。

8、源碼

github.com/hanshuaikang/HanShu-Note

歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。

歡迎各位讀者加入程序員小樂技術群,在公眾號後臺回覆“加群”或者“學習”即可。

猜你還想看

阿里、騰訊、百度、華為、京東最新面試題彙集

手動模擬JDK動態代理

JVM 發生內存溢出的 8 種原因、及解決辦法

Web 開發中,什麼級別才算是高併發

關注微信公眾號「程序員小樂」,收看更多精彩內容

嘿,你在看嗎?


分享到:


相關文章: