MyBatis多数据源配置与使用,基于ThreadLocal+AOP

导读

  • MyBatis多数据源配置与使用
    • 其一
    • 其二
      • 1. 引依赖
      • 2. 配置文件
      • 3. 编写测试代码
      • 4. 自定义DynamicDataSource类
      • 5. DataSourceConfig配置类
      • 6. AOP与ThreadLocal结合
      • 7. 引入AOP依赖
      • 8. DataSourceContextHolder
      • 9. 自定义注解@UseDB
      • 10. 创建切面类UseDBAspect
      • 11. 修改DynamicDataSource
      • 12. 简单测试一下
      • 13. 未完
      • 14. 结合栈的使用
      • 15. 修改DataSourceContextHolder
      • 16. 最后小坑

MyBatis多数据源配置与使用

前言:MyBatis默认情况下只能在application配置文件中配置单数据源,但有一些开发场景可能有多数据源的需求,这需要做一些额外的配置。

查了一下Mybatis多数据源的解决方案,主要有两种方式:

其一

利用MyBatis的@MapperScan注解,该注解除了标注扫描路径外,还能给扫描到的mapper文件的dao操作指定sqlSessionFactoryRef属性指定使用的SqlSessionFactory,此时我们就可以构建不同源的SqlSessionFactory,从而实现不同的mapper文件对应不同的数据源操作。

这种方式简单易懂,创建对应的SqlSessionFactory即可,缺点是需要为每个数据源维护对应的mapper文件。这里不详细描述这种方式。

其二

第二种方式是利用springboot自身的AbstractRoutingDataSource,AbstractRoutingDataSource是一个抽象类,其中维护了一个Map属性,该Map是用于存储多个数据源,通过不同的key获取对应的数据源。另外提供determineCurrentLookupKey抽象方法,供给用户自定义获取键的方式。例如我们两个数据库,db1和db2,当我们想用db1时,只需要让determineCurrentLookupKey方法获取到db1的key就行,db2同理。下面说下详细编码过程:

1. 引依赖

无需额外依赖,springboot,mybatis,mysql驱动即可,注意的是如果springboot版本过高,则可能需要升级其中的mybatis-spring版本,否则报错

    <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- springboot版本过高,需要升级其中的mybatis-spring版本,否则报错 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.2</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>3.0.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.19</version></dependency></dependencies>

2. 配置文件

配置文件中定义数据源的信息,需要注意的是,在单数据源中,连接数据库参数时,使用的key是url,但在多数据源中,默认使用的是jdbc-url。(实际上我们也可以随便定义,但需要我们自己读取配置封装DataSource,后面会讲到)

spring:application:name: MultiSourceMyBatis# datasource配置文件如下datasource:# 数据源1db1:username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://127.0.0.1/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai# 数据源2db2:username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://111.111.111.111/inote?userUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai

3. 编写测试代码

测试代码的部分省略,就是controller,service,dao常规流程

在这里插入图片描述

4. 自定义DynamicDataSource类

创建DynamicDataSource类,继承AbstractRoutingDataSource类,实现determineCurrentLookupKey抽象方法,determineCurrentLookupKey方法就是如何获取DataSource的key的方法。通过不同的key获取对应的数据源。该方法的具体实现我们暂时留白,下面会再做修改

public class DynamicDataSource extends AbstractRoutingDataSource {/*** 获取数据源key的方式,要使用哪个数据源,是通过数据源key选择的,这个key是数据源map中的key*/@Overrideprotected Object determineCurrentLookupKey() {return "db1";}
}

5. DataSourceConfig配置类

DataSourceConfig这个类的主要作用是将我们自定义DynamicDataSource类的实例对象交由spring bean管理,由容器装配与调用。而在这之前,我们还需要给DynamicDataSource设置DataSource的map(也就是将多个DataSource添加到DynamicDataSource中)。

@Configuration
public class DataSourceConfig {@AutowiredEnvironment environment;	// 用于读取application.yml文件配置/*** 构建两个数据库源,交由spring管理,但其实直接创建也无妨,注意保证创建相同配置的DataSource只有一个就行*/@Beanpublic DataSource db1(){HikariDataSource dataSource = new HikariDataSource();dataSource.setDriverClassName(environment.getProperty("spring.datasource.db1.driver-class-name"));dataSource.setJdbcUrl(environment.getProperty("spring.datasource.db1.jdbc-url"));dataSource.setUsername(environment.getProperty("spring.datasource.db1.username"));dataSource.setPassword(environment.getProperty("spring.datasource.db1.password"));return dataSource;}@Beanpublic DataSource db2(){HikariDataSource dataSource = new HikariDataSource();dataSource.setDriverClassName(environment.getProperty("spring.datasource.db2.driver-class-name"));dataSource.setJdbcUrl(environment.getProperty("spring.datasource.db2.jdbc-url"));dataSource.setUsername(environment.getProperty("spring.datasource.db2.username"));dataSource.setPassword(environment.getProperty("spring.datasource.db2.password"));return dataSource;}
//    /**
//     * 实际上创建DataSource的方式可以用以下代码替代,但是需要注意的是配置文件中的数据库连接参数要改为jdbc-url
//     */
//    @ConfigurationProperties(prefix = "spring.datasource.db1")
//    @Bean
//    public DataSource db1(){
//        return DataSourceBuilder.create().build();
//    }/*** 创建DynamicDataSource,并将db1,db2添加进去。*/@Bean("dynamicDataSource")@Primary  // 该注解表示如果有多个相同bean,首选这个public DataSource dynamicDataSource(@Qualifier("db1") DataSource db1,@Qualifier("db2") DataSource db2){DynamicDataSource dynamicDataSource = new DynamicDataSource();//默认数据源,如果determineCurrentLookupKey方法获取到的key不在列表中,则走默认的datasourcedynamicDataSource.setDefaultTargetDataSource(db1);Map<Object,Object> map = new HashMap<>();map.put("db1",db1);map.put("db2",db2);dynamicDataSource.setTargetDataSources(map);return dynamicDataSource;}
}

至此,配置就完成了,此时我们可以通过上面的determineCurrentLookupKey方法指定我们想使用的数据源。

这时候就会有人问了,这也没完成啊,determineCurrentLookupKey方法中写死了数据库的key,怎么做到数据库切换?

刚才说了,determineCurrentLookupKey方法留白了,关键就是怎么动态切换要使用的数据库的key,就的改写determineCurrentLookupKey方法。下面就展开说说。

6. AOP与ThreadLocal结合

我们想实现多数据源,目的肯定是希望不同用户,或者不同操作同时进行时能够使用不同的数据库,而不是同一时刻只有一个数据源起作用,因而多线程下,相同操作对不同资源进行访问,首先想到的是ThreadLocal。如果在用户请求进来后,我们为其配置对应数据库源的key,然后在determineCurrentLookupKey中通过ThreadLocal获取到key,OK,万事大吉。

但……,我们给一个线程创建同一个数据源,我们需要怎么去创建,创建的时机是怎样的?基于编码习惯,我们肯定希望的是通过注解的方式做方法增强。

“对啊,AOP,ThreadLocal+AOP,在service层方法执行前捕获方法,然后通过ThreadLocal设置数据源,后续就能使用该数据源源进行sql操作了,你真聪明”。

7. 引入AOP依赖

        <!-- aop依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

8. DataSourceContextHolder

创建一个线程上下文工具类DataSourceContextHolder,该类主要作用是给线程创建ThreadLocal,然后实现ThreadLocal的getter,setter以及清除工作。

public class DataSourceContextHolder {private static ThreadLocal<String> dataSourceKey = new ThreadLocal<>();public static void setDataSourceKey(String key){dataSourceKey.set(key);}public static String getDataSourceKey(){return dataSourceKey.get();}public static void clear(){dataSourceKey.remove();}}

9. 自定义注解@UseDB

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDB {/*** 要使用的数据源的key*/String value();
}

10. 创建切面类UseDBAspect

在代理方法执行前设置数据库源,方法执行后移除数据库源

@Aspect
@Component
public class UseDBAspect {/*** 定义切面*/@Pointcut(value = "@annotation(com.example.multisourcemybatis.announce.UseDB)")private void getAnnounce(){}/*** 环绕通知* @param joinPoint 切点,就是被注解的目标方法*/@Around("getAnnounce()")public Object logPostMapping(ProceedingJoinPoint joinPoint) throws Throwable {// 获取自定义注解中的value值MethodSignature signature = (MethodSignature) joinPoint.getSignature();UseDB annotation = signature.getMethod().getAnnotation(UseDB.class);String dataSourceKey = annotation.value();// 将dataSource的key设置到ThreadLocalDataSourceContextHolder.setDataSourceKey(dataSourceKey);// 执行目标方法,也就是service方法Object result = joinPoint.proceed();// 执行方法后,记得清除ThreadLocal,避免内存泄漏DataSourceContextHolder.clear();// 返回方法返回值return result;}}

11. 修改DynamicDataSource

补充DynamicDataSource的determineCurrentLookupKey方法,也就是如何获得key的方法,改为从ThreadLocal中获取即可

public class DynamicDataSource extends AbstractRoutingDataSource {/*** 获取数据源key的方式,要使用哪个数据源,是通过数据源key选择的,这个key是数据源map中的key*/@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSourceKey();}}

12. 简单测试一下

service方法

    @UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}

controller方法

    @PostMapping("add")public Result add(UserInfo userInfo) throws Exception {userInfoService.addInDB1(userInfo);userInfoService.addInDB2(userInfo);return ResultUtils.success();}

测试结果:

两个数据库分别插入一条数据,符合预期

在这里插入图片描述

在这里插入图片描述

13. 未完

“你这例子确实实现了通过注解方式实现数据源的切换,但是好像有点问题,你测试的例子是从controller中分别执行两个service方法(被自定义注解@UseDB标注的方法),但在实际开发中,我不确保总是从controller中调用,万一我在一个service中调用另一个service,而且在调用完另一个service后还需要进行数据库操作,这样的话就出问题了,在调用内层service的时候,我的ThreadLocal值已经被覆盖,并且内层service执行完后还进行了清除ThreadLocal,也就是说外层service设置的数据源已经没了,等到后面再执行dao操作时,会走默认的数据源,而不是@UseDB标注的数据源。这……是bug啊”

是的,理想状态下我们认为一个service不调用另一个service,但如果确实调用了,就可能出现bug,但也不是不能解决,那我们就针对性修改下吧

14. 结合栈的使用

我们要实现的效果是,外层方法使用外层数据源,内层方法使用内层方法数据源,如果还有内层的内层方法,使用内层的内层的数据源。然后方法执行完后一步一步弹出,但不影响相对外层的数据源。

有没有很熟悉,这就是栈啊,先进后出,我们使用栈来存储数据源的key,当调用内层方法后pop掉就行了,这样外层方法依旧能获取到外层的数据源key。

15. 修改DataSourceContextHolder

只修改DataSourceContextHolder,修改setter,getter以及clear方法,适配stack。

public class DataSourceContextHolder {private static ThreadLocal<Stack<String>> dataSourceKey = new ThreadLocal<>();/*** 将DataSource的key添加到ThreadLocal的Stack中,效果等同直接交给ThreadLocal* @param key DataSource的key*/public static void setDataSourceKey(String key){// 判断stack是否为空,在初始状态下stack == nullif (dataSourceKey.get()==null){dataSourceKey.set(new Stack<String>());}// 将DataSource的key添加到stack中dataSourceKey.get().push(key);}/*** 获取ThreadLocal中Stack最后添加进的key,效果等同获取当前DataSource的key* @return DataSource的key*/public static String getDataSourceKey(){// 注意,我们获取DataSource时不能采用pop方法,因为我们不能保证一个方法中只有一个数据库操作,// 如果直接pop,则会导致同一个方法后续数据库操作使用错误的数据源return dataSourceKey.get().peek();}/*** 将DataSource的key删除,但是不一定删除ThreadLocal,只有最后一个key配Stack踢出后才删除ThreadLocal*/public static void clear(){dataSourceKey.get().pop();// 如果此时栈中没有数据了,则将ThreadLocal清除if (dataSourceKey.get().empty()) {dataSourceKey.remove();}}/*** 额外再写个方法,无论如何都清除ThreadLocal,避免异常问题,没有将栈全部踢出,导致ThreadLocal内存泄漏* 建议在servlet拦截器中调用清除,afterCompletion中调用。*/public static void clearWhatever(){dataSourceKey.remove();}}

16. 最后小坑

这个不是上面代码的坑,而是AOP实现代理时,类的内部调用默认不走代理方法,也就是说,上面service的addInDB1和addInDB2方法,如果在addInDB1中直接调用或通过this调用addInDB2,如下

    @UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);// 直接调用addInDB2this.addInDB2(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}

上述代码中this.addInDB2(userInfo);默认不走AOP动态代理,也就会导致addDB2方法用的依然是db1数据源这是不符合我们预期的,要解决这个问题,也就是走动态代理,我们要:

  1. 开启exposeProxy=true的配置,将类内部引用也走AOP代理

在启动类上标注

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)		// 允许类内获取当前实例的代理
public class MultiSourceMyBatisApplication {public static void main(String[] args) {SpringApplication.run(MultiSourceMyBatisApplication.class, args);}}
  1. 获取代理对象,通过代理对象调用
    @UseDB("db1")public void addInDB1(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);// 通过AopContext获取当前实例的代理对象UserInfoService userInfoService = (UserInfoService) AopContext.currentProxy();userInfoService.addInDB2(userInfo);}@UseDB("db2")public void addInDB2(UserInfo userInfo) {String stringId = SnowFlakeUtils.getStringId();userInfo.setId(stringId);userInfoMapper.insert(userInfo);}

至此全篇完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/16949.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PTA 计算矩阵两个对角线之和

计算一个nn矩阵两个对角线之和。 输入格式: 第一行输入一个整数n(0<n≤10)&#xff0c;第二行至第n1行&#xff0c;每行输入n个整数&#xff0c;每行第一个数前没有空格&#xff0c;每行的每个数之间各有一个空格。 输出格式: 两条对角线元素和&#xff0c;输出格式见样例…

Android存储系统成长记

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章 本文概要 您一定使用过Context的getFileStreamPath方法或者Environment的getExternalStoragePublicDirectory方法&#xff0c;甚至还有别的方法把数据存储到文件中&#xff0c;这些都是存储系统提供的服务&#x…

PTA 判断两个矩阵相等

Peter得到两个n行m列矩阵&#xff0c;她想知道两个矩阵是否相等&#xff0c;请你用“Yes”&#xff0c;“No”回答她&#xff08;两个矩阵相等指的是两个矩阵对应元素都相等&#xff09;。 输入格式: 第一行输入整数n和m&#xff0c;表示两个矩阵的行与列&#xff0c;用空格隔…

修改元组元素

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 场景模拟&#xff1a;伊米咖啡馆&#xff0c;由于麝香猫咖啡需求量较大&#xff0c;库存不足&#xff0c;店长想把它换成拿铁咖啡。 实例08 将麝香猫…

chrome浏览器驱动下载

跑自动化的时候&#xff0c;需要打开谷歌浏览器&#xff0c;这个时候提示浏览器驱动找不到咋办呢&#xff1f; 1、网上搜索找到了这篇文章&#xff1a;https://www.cnblogs.com/laoluoits/p/17710501.html&#xff1b;按照文章介绍&#xff0c; 首先找到&#xff1a;CNPM Bin…

D - Permutation Subsequence(AtCoder Beginner Contest 352)

题目链接: D - Permutation Subsequence (atcoder.jp) 题目大意&#xff1a; 分析&#xff1a; 相对于是记录一下每个数的位置 然后再长度为k的区间进行移动 然后看最大的pos和最小的pos的最小值是多少 有点类似于滑动窗口 用到了java里面的 TreeSet和Map TreeSet存的是数…

解决 Spring Boot 应用启动失败的问题:Unexpected end of file from server

解决 Spring Boot 应用启动失败的问题&#xff1a;Unexpected end of file from server 博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的…

Spring AOP失效的场景事务失效的场景

场景一&#xff1a;使用this调用被增强的方法 下面是一个类里面的一个增强方法 Service public class MyService implements CommandLineRunner {private MyService myService;public void performTask(int x) {System.out.println("Executing performTask method&quo…

爬虫学习--15.进程与线程(2)

线程锁 当多个线程几乎同时修改某一个共享数据的时候&#xff0c;需要进行同步控制 某个线程要更改共享数据时&#xff0c;先将其锁定&#xff0c;此时资源的状态为"锁定",其他线程不能改变&#xff0c;只到该线程释放资源&#xff0c;将资源的状态变成"非锁定…

Linux如何设置共享文件夹

打开虚拟机->菜单->虚拟机设置->选项->共享文件夹->总是启用。点击添加按钮->弹出添加向导->点击浏览按钮&#xff0c;从windows中选择一个文件夹&#xff0c;确定即可。

[Windows] GIF动画、动图制作神器 ScreenToGif(免费)

ScreenToGif 是开源免费的 Gif 动画录制工具&#xff0c;小巧原生单文件&#xff0c;功能很实用。它有录制屏幕、录制摄像头、录制画板、图像编辑器等功能&#xff0c;可以将屏幕任何区域及操作过程录制成 GIF 格式的动态图像。保存前还可对 GIF 图像编辑优化&#xff0c;支持自…

研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?

一、写在开头 今天和一个之前研二的学妹聊天&#xff0c;聊及她上周面试字节的情况&#xff0c;着实感受到了Java后端现在找工作的压力啊&#xff0c;记得在18&#xff0c;19年的时候&#xff0c;研究生计算机专业的学生&#xff0c;背背八股文找个Java开发工作毫无问题&#x…

抖音里卖什么最赚钱?4个冷门的高利润商品,还有谁不知道!

哈喽~我的电商月月 做抖音小店的新手朋友&#xff0c;一定很想知道&#xff0c;在抖音里卖什么最赚钱&#xff1f; 很多人都会推荐&#xff0c;日常百货&#xff0c;小风扇&#xff0c;女装&#xff0c;宠物用品等等&#xff0c;这些商品确实很好做&#xff0c;你们可以试试 …

备忘录可以统计字数吗?备忘录里在哪查看字数?

在这个信息爆炸的时代&#xff0c;很多人喜欢使用备忘录app来记录生活中的点点滴滴。备忘录不仅可以帮助我们记事、安排日程&#xff0c;还能提醒我们完成各种任务&#xff0c;是我们日常生活中不可或缺的小助手。 然而&#xff0c;在使用备忘录时&#xff0c;有时我们会遇到需…

不用BookStack的企业都在用什么知识库软件

现如今&#xff0c;越来越多的企业使用知识库软件对企业内部知识进行管理。BookStack作为一款功能强大的开源知识库软件&#xff0c;成为很多企业的首选。但是还是有一部分人群认为BookStack不适合他们的企业那么他们都是在用什么别的知识库软件呢&#xff1f;LookLook同学今天…

《python本机环境多版本切换》-两种方式以及具体使用--venv/pyenv+pycharm测试

阿丹&#xff1a; source myenv/bin/activate 在开发使用rasa的时候发现自己安装的python环境是3.12的&#xff0c;和rasa不兼容&#xff0c;所以实践一下更换多python环境。 使用虚拟环境 在Python中使用虚拟环境来切换Python版本是一个常见的做法&#xff0c;这可以帮助你…

实用篇| huggingface网络不通

之前文章《Transformer原理》中介绍过,Transformers 是由 Hugging Face 开发的一个包&#xff0c;支持加载目前绝大部分的预训练模型。随着 BERT、GPT 等大规模语言模型的兴起&#xff0c;越来越多的公司和研究者采用 Transformers 库来构建应用。 Hugging Face是一家美国公司…

Easy IP + DNAT(服务器NAT转换)

第一章 Easy IP 1.1 一般家庭和企业使用的地址转换方式 直接使用出接口的地址做转换Easy IP适用于小规模居于网中的主机访问Internet的场景如&#xff1a;家庭、小型网吧、小型办公室中&#xff0c;这些地方内部主机不多&#xff0c;出接口可以通过拨号方式获取一个临时公网I…

视频监控汇聚平台LntonCVS通过GB/T28181国标协议实现视频监控平台的级联方案

近年来&#xff0c;随着网络视频监控应用范围的拓展&#xff0c;越来越多的政府部门和跨区域行业单位对视频监控的需求已经不局限于本地联网监控。他们正在探索在原有的本地联网监控基础上&#xff0c;建设省级乃至全国范围内的跨区域监控联网&#xff0c;以全面打造数据共享平…

BUUCTF靶场[Reverse]内涵的文件、新年快乐

[reverse]内涵的文件 文件运行看一下 老规矩&#xff0c;拿到文件先用DIE查有没有壳 没有壳&#xff0c;且是一个32位的文件&#xff0c;用相对应的IDA打开 &#xff0c;有主函数&#xff08;mian&#xff09;&#xff0c;先点开 这里点开&#xff08;mian_0&#xff09;,发现…