一、SpringBoot切换数据源
前言
记得刚工作的,看到别人写了一个切换数据源的,觉得很厉害,当时一直想模仿写一个,但是一直没有行动起来,后面也就逐渐忘记了。下面开始记录下实现过程。
实现原理也非常简单,主要包括了3个步骤:
- 继承
AbstractRoutingDataSource - 创建数据源
- 创建注解和切面逻辑
下面按照这3个主要步骤来实现
1. 继承 AbstractRoutingDataSource
为什么要继承这个类?
Springboot内置了一个AbstractRoutingDataSource,将所有数据源装入map,然后可以根据不同的key返回不同的数据源。当springboot开始执行连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去map中查找相应的数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Value("${mini-db-router.jdbc.datasource.default}")
private String defaultDataSource;
@Override
protected Object determineCurrentLookupKey() {
if (DBContextHolder.getDbkey() == null) {
return defaultDataSource;
} else {
return DBContextHolder.getDbkey();
}
}
}
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.keller</groupId>
<artifactId>copy-project</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>
配置文件 application.yml
server:
port: 8081
# 多数据源路由配置
mini-db-router:
jdbc:
datasource:
dbCount: 2
tbCount: 4
default: db00
routerKey: uId
list: db01,db02
db00:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/lottery?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
db01:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/lottery_01?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
db02:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/lottery_02?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
mybatis:
mapper-locations: classpath:/mybatis/mapper/*.xml
config-location: classpath:/mybatis/config/mybatis-config.xml
2. 创建数据源
从配置文件中读取数据库信息创建数据源,通过实现EnvironmentAware接口,获取yml中的配置。
@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {
/**
* 数据源配置组
*/
private Map<String, Map<String, String>> dataSourceMap = new HashMap<>();
@Value("${mini-db-router.jdbc.datasource.default}")
private String defaultDataSouceName;
/**
* 创建数据源
*/
@Bean
public DataSource createDataSouce() {
Map<Object, Object> targetDataSouce = new HashMap<>();
for (String dbInfo : dataSourceMap.keySet()) {
DataSource ds = createDataSource(dataSourceMap.get(dbInfo));
targetDataSouce.put(dbInfo, ds);
}
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSouce);
dynamicDataSource.setDefaultTargetDataSource(createDataSource(dataSourceMap.get(defaultDataSouceName)));
return dynamicDataSource;
}
private DataSource createDataSource(Map<String, String> attributes) {
try {
DataSourceProperties dataSourceProperties = new DataSourceProperties();
dataSourceProperties.setUrl(attributes.get("url").toString());
dataSourceProperties.setUsername(attributes.get("username").toString());
dataSourceProperties.setPassword(attributes.get("password").toString());
String driverClassName = attributes.get("driver-class-name") == null ? "com.zaxxer.hikari.HikariDataSource" : attributes.get("driver-class-name").toString();
dataSourceProperties.setDriverClassName(driverClassName);
String typeClassName = attributes.get("type-class-name") == null ? "com.zaxxer.hikari.HikariDataSource" : attributes.get("type-class-name").toString();
DataSourceBuilder<?> dataSourceBuilder = dataSourceProperties.initializeDataSourceBuilder();
DataSource ds = dataSourceProperties.initializeDataSourceBuilder().type((Class<DataSource>) Class.forName(typeClassName)).build();
return ds;
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("can not find datasource type class by class name", e);
}
}
/**
* 获取数据库配置信息
* @param environment environment
*/
@Override
public void setEnvironment(Environment environment) {
String prefix = "mini-db-router.jdbc.datasource.";
String dataSources = environment.getProperty(prefix + "list");
List<String> dataSourceList = Arrays.stream(dataSources.split(",")).collect(Collectors.toList());
dataSourceList.add("db00");
for (String source : dataSourceList) {
Map<String, String> dbConfig = new HashMap<>();
String driverClassName = environment.getProperty(prefix + source + ".driver-class-name");
String url = environment.getProperty(prefix + source + ".url");
String username = environment.getProperty(prefix + source + ".username");
String password = environment.getProperty(prefix + source + ".password");
dbConfig.put("driver-class-name", driverClassName);
dbConfig.put("url", url);
dbConfig.put("username", username);
dbConfig.put("password", password);
dataSourceMap.put(source, dbConfig);
}
}
}
3. 创建注解和切面逻辑
自定义一个注解和切面,在切面内获取到执行的数据库,然后执行。
注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SwitchDS {
String key() default "";
}
切面:记得在上面加@Component
@Aspect
@Component
public class DBAspect {
@Pointcut("@annotation(com.keller.lottery.route.annotation.SwitchDS)")
public void aopPoint(){
}
@Around("aopPoint() && @annotation(switchDS)")
public Object doSwitch(ProceedingJoinPoint jp, SwitchDS switchDS) throws Throwable {
try {
// 获取数据库类型,如果注解上有从注解上获取,没有从参数的最后一个获取
String dbType;
if (!"".equals(switchDS.key())) {
dbType = switchDS.key();
} else {
Object[] args = jp.getArgs();
dbType = (String)args[args.length - 1];
}
DBContextHolder.setDbkey(dbType);
return jp.proceed();
} finally {
DBContextHolder.clear();
}
}
}
4. 测试
@Mapper
public interface RuleTreeDao {
@SwitchDS
RuleTree queryRuleTreeByTreeId(Long id, String dbType);
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SwitchTest {
@Resource
RuleTreeDao ruleTreeDao;
@Test
public void test1() {
RuleTree ruleTree = ruleTreeDao.queryRuleTreeByTreeId(2110081903l, "db01");
System.out.println(ruleTree);
}
}
二、整合成SpringBoot starter
在实际开发中,很多工程都有切换数据源的需求,为了方便,可以将其弄成SpringBoot starter,其他地方需要引入依赖即可。
项目结构:
1. 编写spring.factories
在 resources下的 META-INF下面新建 spring.factories文件,里面写上要自动配置的类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.keller.route.config.DataSourceAutoConfig
然后maven clean install打包,其他项目需要引入这个依赖即可。
注意点:
需要在自动配置类 DataSourceAutoConfig中将切面的 bean注入,不然引入的时候找不到切面这个bean
@Bean
public DBAspect pointBean() {
return new DBAspect();
}