@Auto-Annotation自定义注解——动态数据源篇
自定义通用注解连更系列—连载中…
首页介绍:点这里
前言
通常一个系统只需要连接一个数据库就可以了。但是在企业应用的开发中往往会和其他子系统交互,特别是对于一些数据实时性要求比较高的数据,我们就需要做实时连接查询,而不是做同步。这个时候就需要用到多数据源。
举个例子,在主从数据库的业务场景中,一个库用来读,一个库用来写,那么在进行数据库读写操作时就需要进行数据库的切换。
所需依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
动态数据源注解@DynamicDb
定义一个动态数据源注解做为标识符
/** 动态数据源注解
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DynamicDb {
/**
* 切换数据库源
*/
DbType value() default DbType.PRIMARY_DB;
}
定义主从库枚举
定义主从库枚举类,可增加多个,则对应配置多个数据源
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Getter
@AllArgsConstructor
public enum DbType {
/**
* 主库
*/
PRIMARY_DB("PRIMARY_DB","主库"),
/**
* 从库
*/
SECOND_DB("SECOND_DB","从库"),
;
private final String value;
private final String desc;
}
动态数据源配置类
程序运行时初始化数据源配置,默认加载主数据源,按条件加载从数据源,动态添加数据源到这个spring底层的AbstractRoutingDataSource数据源切换路由中,从而实现数据源的切换。
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Configuration
public class DruidConfig {
/**
* 加载主数据源
*
* @return 主数据源
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 加载从数据源(按条件加载)
*
* @return 从数据源
*/
@Bean("slaveDataSource")
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置动态数据源
*
* @param masterDataSource 主数据源
* @return 动态数据源切换配置类
*/
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSourceHandler dataSource(DataSource masterDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DbType.PRIMARY_DB, masterDataSource);
try {
Object slaveDataSource = SpringUtil.getBean("slaveDataSource");
targetDataSources.put(DbType.SECOND_DB, slaveDataSource);
} catch (Exception e) {
//未开启从库则会找不到该对象,非异常,这里进行空捕获
}
return new DynamicDataSourceHandler(masterDataSource, targetDataSources);
}
}
动态数据源切换配置
具体实现原理:
- 1、获取数据库连接getConnection()方法时,调用的是determineTargetDataSource()方法,来创建连接,而determineTargetDataSource()方法是决定spring容器连接哪个数据源。
- 2、哪个数据源又是由determineCurrentLookupKey()方法来决定的,此方法是抽象方法,需要我们继承AbstractRoutingDataSource抽象类来重写此方法。
- 3、该方法返回一个key,该key是数据源对象中的beanName,并赋值给lookupKey,由此key可以通过resolvedDataSources属性的键来获取对应的DataSource值,从而达到数据源切换的功能
/** 动态数据源切换配置
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class DynamicDataSourceHandler extends AbstractRoutingDataSource {
public DynamicDataSourceHandler(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
{
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
/**
* 操作数据库前会先获取本地线程的主从库枚举,再根据主从库枚举获取指定数据源
* @return 主从库枚举
*/
@Override
protected Object determineCurrentLookupKey() {
return DbThreadLocal.getType();
}
}
本地数据源线程
用于存储每个线程需切换的数据源
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class DbThreadLocal {
/**
* 本地数据源线程,每个线程单独拥有自己的数据源配置(默认为主数据源)
*/
private static ThreadLocal<DbType> THREAD_LOCAL = new ThreadLocal<>();
public static void setType(DbType dbType){
THREAD_LOCAL.set(dbType);
}
public static DbType getType(){
return THREAD_LOCAL.get() == null ? DbType.PRIMARY_DB: THREAD_LOCAL.get();
}
public static void cleanType(){
THREAD_LOCAL.remove();
}
}
定义数据源切面类
在接口执行前切换指定数据源,存入本地线程中,在创建数据库连接时会执行DynamicDataSourceHandler
类中的determineCurrentLookupKey
方法,获取本地线程的主从库枚举,再根据主从库枚举获取指定数据源。
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Slf4j
@Aspect
@Component
public class DynamicDataSourceAop {
@Around(value = "@annotation(dynamicDb)")
public Object around(ProceedingJoinPoint jp, DynamicDb dynamicDb) throws ServerException {
try {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
DynamicDb annotation = method.getAnnotation(DynamicDb.class);
DbType dbType = annotation.value();
DbThreadLocal.setType(dbType);
return jp.proceed();
} catch (Throwable e) {
log.error("处理异常", e);
throw new ServerException("程序运行异常", e);
} finally {
//清理本地线程数据源,避免上下文数据源逻辑混乱
DbThreadLocal.cleanType();
}
}
}
数据源配置
# 数据源配置
spring:
datasource:
type:
driverClassName:
druid:
# 主库数据源
master:
url:
username:
password:
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled:
url:
username:
password:
标记动态数据源接口
@DynamicDb(DbType.SECOND_DB)
@PostMapping("saveUser")
private void saveUser(User user){
System.out.println("保存用户信息逻辑...");
}
总结
使用自定义注解来切换动态数据源非常方便,只需添加依赖,并在yaml中配置数据源的名称和地址,并在接口上使用注解来指定实现切换即可。