完整整合多数据源切换demo实例,通过自定义注解实现
引入springboot相关依赖这里选用的2.5.3版本:
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.1
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
5.1.34
runtime
com.baomidou
mybatis-plus
3.2.0
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-test
test
com.alibaba
druid
1.1.10
org.projectlombok
lombok
true
commons-lang
commons-lang
2.6
com.alibaba
fastjson
1.2.72
commons-codec
commons-codec
1.15
本案例选用的是双mysql所以需要使用Oracle的需要在pom引入Oracle依赖然后更改驱动:
配置如下:
spring:
datasource:
appdb:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123
type: com.alibaba.druid.pool.DruidDataSource
druid:
max-active: 30 # 最大连接数
min-idle: 5 # 最小连接量
max-wait: 10000 # 最大等待时间 10s
validation-query: SELECT 'WTUCLOUD' # 8小时空闲查询一次 WTUCloud 避免连接关闭
time-between-eviction-runs-millis: 60000 # 空闲连接检查间隔
min-evictable-idle-time-millis: 300000 # 空闲阈值
sysdb:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.0.5:3306/mytest?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123
type: com.alibaba.druid.pool.DruidDataSource
druid:
max-active: 30 # 最大连接数
min-idle: 5 # 最小连接量
max-wait: 10000 # 最大等待时间 10s
validation-query: SELECT 'WTUCLOUD' # 8小时空闲查询一次 WTUCloud 避免连接关闭
time-between-eviction-runs-millis: 60000 # 空闲连接检查间隔
min-evictable-idle-time-millis: 300000 # 空闲阈值
这里使用的配置一个名为appdb的数据库和一个sysdb的数据库,如果有更多可以依次类推在增加相关命名和配置即可
这里使用了lombook插件生成构造方法;
实现代码如下:
package com.package.package.emenu;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Description:
* 数据库枚举类
*
*
* @author Editor MartinZac
* @date 2021年07月27日 11:14
*/
@AllArgsConstructor
@Getter
public enum DataSourceEnum {
DEFAULT("sysdb"),
APPDB("appdb"),
SYSDB("sysdb");
private String name;
}
编写将数据服务注入容器的配置实现代码如下:
package com.package.package.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.sbm.productionmodelmap.emenu.DataSourceEnum;
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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* Description:
*
* @author Editor MartinZac
* @date 2021年07月27日 11:14
*/
@Configuration
public class DataSourceConfig {
/**
* APP数据库配置
* @return
*/
@Bean("appDb")
@ConfigurationProperties("spring.datasource.appdb")
public DataSource appDb(){
return new DruidDataSource();
}
/**
* 后台系统数据库
* @return
*/
@Bean("sysDb")
@ConfigurationProperties("spring.datasource.sysdb")
public DataSource sysDb(){
return new DruidDataSource();
}
@Bean("dynamicDataSource")
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setDefaultTargetDataSource(appDb());
Map
package com.package.package.config;
import com.sbm.productionmodelmap.emenu.DataSourceEnum;
/**
* Description:
* 动态数据源切换配置
*
*
* @author Editor MaYongHui
* @date 2021年07月27日 11:28
*/
public class DataSourceHolder {
private static final ThreadLocal DS_HOLDER = new ThreadLocal<>();
public static void setDataSource(DataSourceEnum dataSource) {
DS_HOLDER.set(dataSource.getName());
}
public static String getDataSource() {
return DS_HOLDER.get();
}
public static void clearDataSource() {
DS_HOLDER.remove();
}
}
package com.package .package .config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* Description:
*
* @author Editor MarticZac
* @date 2021年07月27日 11:26
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//实现数据源切换
log.info("dynamic datasource :" + DataSourceHolder.getDataSource());
return DataSourceHolder.getDataSource();
}
}
自定义注解实现代码如下:
package com.package .package .target;
import com.package .package .emenu.DataSourceEnum;
import java.lang.annotation.*;
/**
* Description:
*
* @author Editor MartinZac
* @date 2021年07月27日 11:14
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSource {
//获取枚举类的值作为value
DataSourceEnum value();
}
切面类通过注解作为切点获取注解参数调用handler实现数据源切换:
package com.package .package .asepct;
import com.package .package .target.DataSource;
import com.package .package .emenu.DataSourceEnum;
import com.package .package .config.DataSourceHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Description:
动态切换数据源切面类
@Order(1) 注解为开启切点顺序,由于还有其他切点处理日志需要开启顺序
*
* @author Editor MartinZac
* @date 2021年07月27日 11:34
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class DataSourceAsepct{
@Pointcut("@annotation(com.sbm.productionmodelmap.target.DataSource)")
public void pointCut() {
}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
DataSourceEnum dataSourceEnum = DataSourceEnum.DEFAULT;
try {
Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
if (method.isAnnotationPresent(DataSource.class)) {
DataSource annotation = method.getAnnotation(DataSource.class);
dataSourceEnum = annotation.value();
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
log.info("切换数据源为: " + dataSourceEnum.getName());
//切换数据源
DataSourceHolder.setDataSource(dataSourceEnum);
}
@After("pointCut()")
public void after() {
DataSourceHolder.clearDataSource();
}
}
以上6个步骤已经完全实现了数据源切换的所有逻辑编写,接下来的实现只是使用注解在业务方法上实现数据源切换处理不通的业务逻辑:
package com.sbm.productionmodelmap.service.impl;
import com.sbm.productionmodelmap.emenu.DataSourceEnum;
import com.sbm.productionmodelmap.entity.OperLog;
import com.sbm.productionmodelmap.mapper.OperLogMapper;
import com.sbm.productionmodelmap.service.OperLogService;
import com.sbm.productionmodelmap.target.DataSource;
import com.sbm.productionmodelmap.vo.OperLogVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Description: 通过 @DataSource(DataSourceEnum.APPDB) 注解声明哪个方法应该使用哪个数据库来实现具体的调用,切面类通过切点获取注解进行自动切换
*
* @author Editor MaYongHui
* @date 2021年08月23日 15:31
*/
@Service
public class TestServiceImpl implements TestService {
@Autowired
privateTestAppMapper testAppMapper;
@Autowired
privateTestSysMapper testSysMapper;
@DataSource(DataSourceEnum.APPDB)
@Override
public int deleteByPrimaryKey(Long operId) {
return testAppMapper.deleteByPrimaryKey(operId);
}
@DataSource(DataSourceEnum.SYSDB)
@Override
public int insert(OperLog record) {
return testSysMapper.insert(record);
}
}
以上所有步骤为实现多数据源动态切换的方式,可以根据自己业务选择数据源的切换实现主从读写业务或其他复杂业务,但是目前存在一个使用环境暂时没有更好的解决方案,当你的项目中还有其他切面类处理日志等其他业务需要切换数据源时会出现动态切换失败一直为枚举类中的默认数据源,我查看过一些失效的解决办法网上大部分都为通过使用@Order注解来定义切点的执行顺序将数据源的执行顺序放到最高级(@Order(1)这个数据越大执行顺序越低。数值越小执行顺序越高),这样的方式从AOP思想上来看是并没有问题的,但是使用后还是不能解决切换失败的问题,所以我自己是将需要处理日志的数据源设置为默认,但并没有解决根本问题。所以欢迎大佬遇到的时候能评论区给个指点!