前言
項目中經常會有集成其他數據庫的情況,我們項目是使用spring Boot+Druid+Mybatis Plus開發,本文簡述在項目通過AOP的方式動態的切換數據庫。
版本號
框架 | 版本號 |
---|---|
druid | 1.1.10 |
spring boot | 2.2.2.RELEASE |
mybatis plus | 3.2.0 |
實現思路
- 配置文件中配置多個數據源
- 將多個數據源注入到AbstractRoutingDataSource類的一個Map結構中,
- 通過AOP的切面來動態的從Map中獲取數據源
實現過程
1. application.yml中配置多數據源
spring:
datasource:
druid:
xuyang:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/clouddb_xy?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
username: hebao-dev
password: <your password>
initialSize: 5
minIdle: 5
maxActive: 20
ppe:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://localhost:1433; DatabaseName=PowerPPE
username: sa
password: <your password>
initialSize: 5
minIdle: 5
maxActive: 20
2. 配置mybatis plus 這一步主要是配置多個數據源和mybatis的sqlSessionFactory
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.powerpms.risun.datasource.DBTypeEnum;
import com.powerpms.risun.datasource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 配置多數據源
* @Author: 俞兆鵬
* @Date: 2020/2/26 15:52
* @Email: [email protected]
* @Version 1.0
*/
@EnableTransactionManagement
@Configuration
@MapperScan("com.powerpms.dao.**")
public class MybatisPlusConfig {
@Bean(name = "xuyang-db")
@ConfigurationProperties(prefix = "spring.datasource.druid.xuyang")
public DataSource db1() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "ppe-db")
@ConfigurationProperties(prefix = "spring.datasource.druid.ppe")
public DataSource db2() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource multipleDataSource(@Qualifier("xuyang-db") DataSource xuyangDb,
@Qualifier("ppe-db") DataSource ppeDb) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.xuyang.getValue(), xuyangDb);
targetDataSources.put(DBTypeEnum.ppe.getValue(), ppeDb);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(xuyangDb);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(db1(), db2()));
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
}
3. 實現AbstractRoutingDataSource類,其實切換數據源的核心邏輯都在這個類裏,把這個類看一下就大致明白原理了,後面會專門的說一下這個類
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Author: 俞兆鵬
* @Date: 2020/2/26 16:14
* @Email: [email protected]
* @Version 1.0
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 返回要使用的數據源的key
return DbContextHolder.getDbType();
}
}
4. 實現一個線程安全的容器,用來保存當前線程要使用的數據源的key,這個容器裏的key的設置,就是通過AOP切面來設置的,以mapper爲切點,在執行mapper的時候動態設置當前mapper要使用的數據源
/**
* @Author: 俞兆鵬
* @Date: 2020/2/26 16:15
* @Email: [email protected]
* @Version 1.0
*/
public class DbContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
/**
* 設置數據源
* @param dbTypeEnum
*/
public static void setDbType(DBTypeEnum dbTypeEnum) {
contextHolder.set(dbTypeEnum.getValue());
}
/**
* 取得當前數據源
* @return
*/
public static String getDbType() {
return (String) contextHolder.get();
}
/**
* 清除上下文數據
*/
public static void clearDbType() {
contextHolder.remove();
}
}
5. 實現一個枚舉類用來當作多個數據源的key
/**
* @Author: 俞兆鵬
* @Date: 2020/2/26 16:17
* @Email: [email protected]
* @Version 1.0
*/
public enum DBTypeEnum {
xuyang("xuyang-db"),
ppe("ppe-db");
private String value;
DBTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
6. 通過AOP切面動態設置數據源
在使用AOP的時候需要依賴aop的jar包如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.5.3</version>
</dependency>
切面類
import com.powerpms.risun.datasource.DBTypeEnum;
import com.powerpms.risun.datasource.DbContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @Author: 俞兆鵬
* @Date: 2020/2/26 16:34
* @Email: [email protected]
* @Version 1.0
*/
@Component
@Order(value = -100)
@Slf4j
@Aspect
public class DataSourceSwitchAspect {
@Pointcut("execution(* com.powerpms.dao.xy..*.*(..))")
private void db1Aspect() {
}
@Pointcut("execution(* com.powerpms.dao.ppe..*.*(..))")
private void db2Aspect() {
}
@Before("db1Aspect()")
public void db1() {
log.info("切換到旭陽主數據源...");
DbContextHolder.setDbType(DBTypeEnum.xuyang);
}
@Before("db2Aspect()")
public void db2() {
log.info("切換到PPE數據源...");
DbContextHolder.setDbType(DBTypeEnum.ppe);
}
}
原理
AbstractRoutingDataSource類詳解
- 這是一個抽象類,這個類只有一個抽象方法,也是我們實現的這個方法。
/**
* 這個方法返回的是當前數據源的key,通過這個key來決定使用哪個數據源
*
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
該方法的調用如下:
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 在這裏調用,獲取數據源的key
Object lookupKey = determineCurrentLookupKey();
// 根據key來獲取數據源
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
// 如果上一步拿到的數據源是空的,就使用默認的數據源
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
我們完成了這一步之後,將這裏獲取到的dataSource對象注入到mybatis的sqlSessionFactory中,如下
@Bean
@Primary
public DataSource multipleDataSource(@Qualifier("xuyang-db") DataSource xuyangDb,
@Qualifier("ppe-db") DataSource ppeDb) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.xuyang.getValue(), xuyangDb);
targetDataSources.put(DBTypeEnum.ppe.getValue(), ppeDb);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(xuyangDb);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
// 在這裏注入sqlSessionFactory要使用的數據源,而這裏獲取到的數據源正是我們實現的AbstractRoutingDataSource類所返回的數據源
sqlSessionFactory.setDataSource(multipleDataSource(db1(), db2()));
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
參考
[小塵哥的博客](springboot+druid+mybatis plus的多數據源配置)