springboot+mybatis plus 数据源真动态切换
动态数据源切换
在网上搜索springboot 动态数据源,出来的解决方案基本都比较统一,大概步骤是:1.使用dynamic-datasource-spring-boot-starter,2.配置多个数据源。3.使用@DS注解切换数据源。或类似做法。解决的都是多数据源支持问题,而非真正的动态数据源。经过几天的研究及调试,终于搞了一个自认为还可以的真正的动态切换数据源的方案,先分享出来,希望对大家有帮助,有更好的解决方案也欢迎指正。
业务需求背景
在做一个数据库管理的项目时,需要解决的一个问题就是,管理的数据库的数据源是通过页面维护进来的,所以在系统启动时,是无法确定有哪些数据源的。再加上还有不同类型的数据库,所以同一个管理功能,需要提供不同数据库类型的查询实现。
技术需求分析
从以上的需求可以分析出如下技术需求点:
- 数据源需要支持从数据库中动态读取
- 根据查询数据库所属的数据类型需要自动使用对应的数据库类型的查询实现类
解决方案
要实现以上两个技术需求点,简单的使用dynamic-datasource-spring-boot-starter是无法实现的,我们还需要在此基础上做些更改造。
-
dynamic-datasource-spring-boot-starter的配置不在叙述,参见网上的资料。只是不在需要使用@Ds注解
-
接口方法需要有一个参数来指定数据源的代号,假设为:“dsName”,即需要支持动态数据源切换的接口方法中必须要有dsName这个参数,前端调用的时候,使用的数据源就用这个字段标识。如下:
@GetMapping("getDbNameList") public Result getDbNameList(String dsName) { .... }
-
提供一个注解@DynamicDataBase,用于指定数据源的code的变量名称,即上面的“dsName”,同时也缩小这个功能后面切面的影响范围,如下:
@DynamicDataBase("dsName") public class DbInfoController implements DynamicDatabaseController { ... }
-
提供一套接口支持:DynamicDatabaseController、DynamicDatabaseService、DynamicDatabaseMapper,作用见第5步代码,接口代码如下:
public interface DynamicDatabaseController { DynamicDatabaseService getDynamicDatabaseService(); } public interface DynamicDatabaseService { void setDynamicDatabaseMapper(DynamicDatabaseMapper dynamicDatabaseMapper); } public interface DynamicDatabaseMapper { }
-
最关键的一步:利用切面来实现动态切换数据源及数据库操作的实现。
@Aspect @Component @Slf4j public class DynamicDatabaseSelector { @Resource private DataSourceHelper dataSourceHelper; @Resource private CommonAPI commonAPI; @Resource private ApplicationContext context; private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); @Pointcut("(@within(com.snail.dm.common.tool.aspect.DynamicDataBase)) " + "&& execution(public org.jeecg.common.api.vo.Result com.snail.dm..*.*(..))") public void collectorPoint(){ } @Around("collectorPoint()") public Object methodAroundPorcess(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ log.info("开始执行数据源切换"); Object[] args = proceedingJoinPoint.getArgs(); Object target = proceedingJoinPoint.getTarget(); if(!(target instanceof DynamicDatabaseController)){ log.error("指定的对象不是DynamicDatabaseController,不自动切换数据源"); return proceedingJoinPoint.proceed(args); } // 1.从DynamicDataBase注解上获取到数据源的变量名 String dataSourceCodeFileName = getDataSourceCodeFileName(proceedingJoinPoint); if(StrUtil.isBlank(dataSourceCodeFileName)){ log.error("指定的数据源变量名称为空,不自动切换数据源"); return proceedingJoinPoint.proceed(args); } // 2.从方法参数中获取到数据源的代号 MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); Method method = signature.getMethod(); // 获取方法参数名 String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); int dataSourceCodeArgIndex = parameterNames == null?-1:ArrayUtil.indexOf(parameterNames, dataSourceCodeFileName); if(dataSourceCodeArgIndex<0){ log.error("指定的数据源变量名称{}在方法参数中不存在,不自动切换数据源",dataSourceCodeFileName); return proceedingJoinPoint.proceed(args); } String dataSourceCode = (String) args[dataSourceCodeArgIndex]; if(StrUtil.isBlank(dataSourceCode)){ log.error("指定的数据源变量值为空,不自动切换数据源"); return proceedingJoinPoint.proceed(args); } // 3.从数据库中获取到数据源的配置信息 DynamicDataSourceModel dynamicDataSourceModel = commonAPI.getDynamicDbSourceByCode(dataSourceCode); if(dynamicDataSourceModel == null){ log.error("指定的数据源变量值{}不在定义的数据源中不存在,不自动切换数据源",dataSourceCode); return proceedingJoinPoint.proceed(args); } // 4.切换数据源 dataSourceHelper.changeDataSource(dataSourceCode); log.info("切换数据源为:{}",dataSourceCode); // 5.获取数据源的数据库类型,并根据自定义的信息返回其别名. Integer dbType = Integer.valueOf(dynamicDataSourceModel.getDbType()); DbTypeEnum dbTypeEnum = DbTypeEnum.fromCode(dbType); if (dbTypeEnum == null){ log.error("数据源类型{}没有定义,不设置处理类",dbType); return proceedingJoinPoint.proceed(args); } // 6.根据别名获取对应的mapper的实现. String beanName = String.format("dbInfoMapperOf%s", StrUtil.upperFirst(dbTypeEnum.getInternalName())); DynamicDatabaseMapper dynamicDatabaseMapper = context.getBean(beanName, DynamicDatabaseMapper.class); if(dynamicDatabaseMapper == null){ log.error("数据源类型{}没有对应的处理类",dbType); return proceedingJoinPoint.proceed(args); } log.info("设置数据源处理类为:{}",dynamicDatabaseMapper.getClass().getName()); // 7.切换service中mapper的实现 DynamicDatabaseService dynamicDatabaseService = ((DynamicDatabaseController) target).getDynamicDatabaseService(); dynamicDatabaseService.setDynamicDatabaseMapper(dynamicDatabaseMapper); Object proceed = proceedingJoinPoint.proceed(args); dynamicDatabaseService.setDynamicDatabaseMapper(null); return proceed; } private String getDataSourceCodeFileName(ProceedingJoinPoint proceedingJoinPoint) { Class<?> beanClass = AopProxyUtils.ultimateTargetClass( proceedingJoinPoint.getTarget()); DynamicDataBase annotation = beanClass.getAnnotation(DynamicDataBase.class); String annotationValue = annotation.value(); log.info("Class Annotation Value: " + annotationValue); return annotationValue; } }
详细解释都在代码里面。
-
具体实现实例如下:
// 1.Controller @RestController @RequestMapping("/dds/dbInfo/") @Slf4j @DynamicDataBase("dsName") public class DbInfoController implements DynamicDatabaseController { @Resource private IDbInfoService dbInfoService; @GetMapping("getDbNameList") public Result getDbNameList(String dsName) { Result<List<String>> result = new Result<>(); try { List<String> allDataBaseName = dbInfoService.getAllDbName(dsName); result.setSuccess(true); result.setResult(allDataBaseName); return result; } catch (Exception e) { log.error(e.getMessage(), e); result.setSuccess(false); result.setMessage(String.format("查询失败:%s", e.getLocalizedMessage())); } return result; } } // 2.service public interface IDbInfoService extends DynamicDatabaseService { List<String> getAllDbName(String dsName); } @Service public class DbInfoServiceImpl implements IDbInfoService { private DbInfoMapper dbInfoMapper; @Override public void setDynamicDatabaseMapper(DynamicDatabaseMapper dynamicDatabaseMapper) { if(dynamicDatabaseMapper instanceof DbInfoMapper){ this.dbInfoMapper = (DbInfoMapper) dynamicDatabaseMapper; } } @Override public List<String> getAllDbName(String dsName) { List<String> dbNameList = dbInfoMapper.queryAllDbName(dsName); return dbNameList; } } // 3.mapper public interface DbInfoMapper extends DynamicDatabaseMapper { List<String> queryAllDbName(String dsName); } // 3.1 sqlserver的实现 @Mapper @Component("dbInfoMapperOfSqlServer") public interface SqlServerDbInfoMapper extends DbInfoMapper{ } // 3.2 oracle的实现 @Mapper @Component("dbInfoMapperOfOracle") public interface OracleDbInfoMapper extends DbInfoMapper { } // 3.3 mysql的实现 @Mapper @Component("dbInfoMapperOfMysql") public interface MysqlDbInfoMapper extends DbInfoMapper { }
实现效果展示
请求1: http://localhost:3100/dbmanager/dds/dbInfo/getDbNameList?dsName=oracleTest&_t=1728652167062
请求2:http://localhost:3100/dbmanager/dds/dbInfo/getDbNameList?dsName=dbManagerdb&_t=1728652172814
后台日志:
c.snail.dm.common.tool.aspect.DbManangerDictAspect:30 - DbManangerDictAspect doAround
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:58 - 开始执行数据源切换
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:119 - Class Annotation Value: dsName
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:93 - 切换数据源为:oracleTest
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:107 - 设置数据源处理类为:jdk.proxy2.$Proxy294
c.s.d.c.tool.aspect.MehtodPerformanceCollector:33 - 方法(com.snail.dm.controller.DbInfoController.getDbNameList)执行共计耗时:36 ms
c.snail.dm.common.tool.aspect.DbManangerDictAspect:30 - DbManangerDictAspect doAround
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:58 - 开始执行数据源切换
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:119 - Class Annotation Value: dsName
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:93 - 切换数据源为:dbManagerdb
c.s.dm.common.tool.aspect.DynamicDatabaseSelector:107 - 设置数据源处理类为:jdk.proxy2.$Proxy289
c.s.d.c.tool.aspect.MehtodPerformanceCollector:33 - 方法(com.snail.dm.controller.DbInfoController.getDbNameList)执行共计耗时:52 ms
第一个请求从oracle数据库中查询数据,第二个请求从mysql数据库中查询数据.