前言
项目中经常会有集成其他数据库的情况,我们项目是使用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
# 如果使用了pageHelper分页,数据库方言设置问自动识别,以防止不同数据库的分页方式不同
pagehelper:
reasonable: true
support-methods-arguments: true
params: count=countSql
row-bounds-with-count: true
auto-dialect: true
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();
}
注意
当我使用了这种动态切换数据源的方式之后,就不能在使用默认的事务管理器了,默认的事务管理器还是使用的我们标记为primary的数据源。
因此需要我们自己实现事务管理器
自己实现数据源的博客后面会写到
参考
[小尘哥的博客](springboot+druid+mybatis plus的多数据源配置)