【源码系列】MyBatis与Spring整合原理源码

系列文章目录

【源码系列】MyBatis原理源码


前言

本文是对之前的MyBatis原理源码一文的补充,工作中都是Mybatis不会单独使用而是整合Spring一起使用,整合以后我们都感觉不到SqlSessionFactorySqlSession的存在,需要用到哪个Mapper时,直接使用@Autowired注入就行,而且 SqlSession默认的实现DefaultSqlSession是线程不安全的,别问我怎么知道的,DefaultSqlSession的类注释告诉我的
在这里插入图片描述
到这里大家肯定有几个问题想问

  1. 什么原因导致了DefaultSqlSession线程不安全?
    2. MyBatisSpring整合是如何解决SqlSession的线程安全问题?
  2. Mapper接口是怎么注入到Spring容器的?

我们就带着这两个问题去看源码,看完源码后,这两个问题自然就懂了。现在的项目基本都是基于SpringBoot搭建的,所以这里我们直接看MyBatisSpringBoot的整合源码,因为讲的是和SpringBoot整合,所以大家最好懂SpringBoot的自动装配原理,不懂的请移步我写的另一篇文章 SpringBoot启动流程的原理源码,懂得可以直接跳过,本文中我也会简单提一下自动装配原理。


一、整合时一些关键类

老规矩,我们先看下一些关键的类,我会先告诉大家类的作用,具体细节去源码中扣

MapperScannerRegistrar

顾名思义,这是一个注册器,它向容器中注册了MapperScannerConfigurer类的定义信息

MapperScannerConfigurer

该类是mybatis-spring的类,它的作用就是扫描@MapperScan(basePackages = "com.*.*.mapper")注解指定的路径,将所有的Mapper接口都包装成BeanDefintionHolder对象然后向容器注册(该类的作用是向容器注册待创建的Bean的一些定义信息,后期容器就会拿着BeanDefintion依次去创建Bean),所有待工厂创建的对象都会在这两个集合中。
在这里插入图片描述

SqlSessionFactoryBean

该类SqlSessionFactory实现了Spring的一个扩展接口FactoryBean,用来创建SqlSessionFactory的,很多小伙伴会好奇,为啥不直接注入SqlSessionFactory对象,那样不更方便,为啥要搞一个新的类来做这个事情,首先我们要明白SqlSessionFactory属于Mybatis源码的,作者也不可能为了向Spring靠近,而使用@Compose来修饰,而且创建SqlSessionFactory的时候,中途还有很多业务逻辑要处理,最重要的是Spring为了第三方框架和自己整合,向他们提供了FactoryBean接口,FactoryBean可以简化创建Bean的流程,不用走Bean创建的生命周期。不清楚的请移步这篇Spring中FactoryBean原理解析

SqlSessionTemplate

该类就是用来解决DefaultSqlSession线程不安全的问题,它实现了SqlSession接口,我们在调用Mapper接口方法,首先会进入SqlSessionTemplate的方法,后续的具体查询会走到SqlSession

MapperFactoryBean

该类也是一个FactoryBean,从名字能看出该类应该跟Mapper接口有关,没错我们使用的Mapper实现类就是通过它来创建的。

二、源码分析

1.SqlSessionFactory创建流程

这里就涉及到SpringBoot的自动装配了,简单说一下SpringBoot在启动的时候会去扫描所有jar包资源目录下的spring.factory的文件,拿到所有以org.springframework.boot.autoconfigure.EnableAutoConfiguration为key的类,然后将他们作为一个配置类注入到容器中,Spring后面会去解析该配置类

我们看下Mybatis自动装配的配置文件,发现自动装配的类为MybatisAutoConfiguration,打开该类我们就能看到SqlSessionFactory就是在这里面注入的
在这里插入图片描述
前面就说过SqlSessionFactory是通过SqlSessionFactoryBean来创建的,至于原因上面也分析过啦,该方法中都是在为SqlSessionFactoryBean赋值,其实这些值都是为SqlSessionFactory准备的,比如插件、全局的Configunation对象等,如果用户没有配置的话,那么都会取默认值。在这里插入图片描述
接下来我们来看SqlSessionFactory#getObject方法,afterPropertiesSet方法会调到buildSqlSessionFactory方法,通过观察会发现又回到了MyBatis的代码中
在这里插入图片描述
在这里插入图片描述

2、SqlSession线程安全解决

先说一下为啥SqlSession是线程不安全的,在讲这个之前希望大家对这个知识点的认知没有问题:JVM的堆是线程共享的,对象的成员变量存在堆中,虚拟机栈是线程独占的,方法中的局部变量存放在线程栈的栈帧中,这个没问题后,我们来看下DefaultSqlSession是如何获取Connection对象的,通过代码发现是通过Executor(默认实现为SimpleExecutor)对象中的Transaction对象来获取的,我们再点进去看下
在这里插入图片描述
JdbcTransaction#getConnection方法中我们可以看到,该类将·connection作为了成员变量,然后获取连接的时候先判断成员变量connection是否为空,不为空就返回原来的Connetion对象,由于对象的成员变量存在堆,堆中数据是线程共享的,如果多线程同是调用SqlSession#getConnect方法获取连接对象的话,肯定拿到的是同一个连接对象,多线程使用一个连接并发查询的话,肯定会存在数据覆盖的问题,这就是为啥有线程安全问题。
在这里插入图片描述

先说一下解决线程安全的核心思想:方法局部变量存放在线程栈的栈帧中,线程栈是线程独占的

MyBatisSpring整合中就是利用了这个思想来,如果SqlSession对象作为成员变量的话(成员变量存在JVM的堆,而堆是线程共享的)必然会有线程安全问题,如果将SqlSession作为方法的局部变量的话,那就是线程私有的,就不存在线程安全问题。具体解决方案我们去看源码。

我们可以看到MyBatis的自动装配类中,注入了SqlSessionTemplate对象,而该类实现自SqlSession,重写了所有的方法,将SqlSession作为了成员变量(这是个代理对象),后续的增删改查方法都是委托给SqlSession的代理对象去实现,
在这里插入图片描述在这里插入图片描述
自动装配类中创建SqlSessionTemplate对象会走重载然后走到下面这个构造方法中,从这里可以看出来成员变量SqlSession就是SqlSessionTemplate实例化的时候创建的代理对象,代理对象的InvocationHandlerSqlSessionInterceptor,因为增删改查都是通过该代理对象来操作的,所以操作一定会执行到SqlSessionInterceptor#invoke方法,我们再看下invoke方法中具体的代码逻辑
在这里插入图片描述
我们可以看到每一次增删改查都会创建一个SqlSession对象,操作完成后就关闭了当前的SqlSession对象。
在这里插入图片描述
从该方法可以看到,先会从TransactionSynchronizationManager对象中获取SqlSession,如果有就返回,没有就通过SqlSessionFactory创建,创建以后向TransactionSynchronizationManager注册,TransactionSynchronizationManager隶属于Spring事务模块的类,原理就是ThreadLocal,让SqlSession对象和线程绑定,这里是为了实现同一个事务中,所有的操作对象都是同一个SqlSession,不用纠结事务这一块。
如果有兴趣想深入了解一下事务的请移步我的另一篇博客【源码系列】Spring事务执行原理源码
在这里插入图片描述

小结

MyBatis在和Spring整合解决SqlSession线程不安全问题是通过SqlSessionTemplate对象,通过为SqlSession创建代理对象,作为SqlSessionTemplate的成员变量,增删改查操作都是通过代理对象来执行的,具体的SqlSession对象是在执行代理对象invoke方法时去创建,用完了立马关闭。

3、MapperScannerConfigurer注入流程

Spring的对象注入用户只需要把类的定义信息注册到容器中,后期容器会对所有的定义信息去创建Bean,所以我们这里的注入也只需要注入MapperScannerConfigurer的定义信息

该类是mybatis-spring的类,前面也说了它负责了Mapper接口的扫描工作,并将扫描到的Mapper接口封装成BeanDefintionHolder向容器注册定义信息,我们看下该类在SpringBoot中是如何注入到容器中的
MyBatisSpringBoot使用时都会在配置类上加一个@MapperScan注解,告诉MyBatis去哪里找Mapper接口,我直接放在了启动类上面
在这里插入图片描述
文章前面不是说扫描工作是MapperScannerRegistrar来完成的嘛,怎么没看到该类。别急我们打开这个注解看看里面有啥
在这里插入图片描述
我们看下MapperScannerRegistrar的类图,请记住它实现了ImportBeanDefinitionRegistrar接口
在这里插入图片描述

Spring在解析配置类的时候,会去解析类上的注解,其中一个比较重要的就是@Import注解,然后判断导入的类是否实现自 ImportSelectorImportBeanDefinitionRegistrar(这两个接口都是spring提供的扩展点),因为我们的实现自ImportBeanDefinitionRegistrar,我们只说ImportBeanDefinitionRegistrar的处理,Spring在解析某个配置类(启动类就是一个配置类)的时候,会将Import实现自ImportBeanDefinitionRegistrar的类加到配置类的importBeanDefinitionRegistrars属性中,后续统一执行这些类的registerBeanDefinitions方法,我们打断点看一下
在这里插入图片描述
在这里插入图片描述
MapperScannerRegistrar#registerBeanDefinitions方法
在这里插入图片描述
这里是具体的注入MapperScannerConfigurer定义信息的方法,在封装定义信息的时候还将需要扫描的路径放到了定义信息里面,后面Spring在创建该对象的时候会主动给属性basePackages赋值
在这里插入图片描述
在这里插入图片描述
到这里,MapperScannerConfigurer的注入工作就完成了,流程还是比较复杂的,上面基本都是Spring的知识,看不懂的也没关系,记住做的事情就行

小结

@MapperScan注解导入了MapperScannerRegistrar类,该类可以导入Bean的定义信息并向容器注册,注册的就是我们的MapperScannerConfigurer

4、Mapper的定义信息扫描注册流程

前面我们已经说了扫描和注册都是MapperScannerConfigurer类完成的,我们先看下该类的类图,发现该类实现了Spring的两个扩展接口,InitializingBean做的事情就是验证了一下bean的属性basePackage不能为空,简单科普一下扩展接口BeanDefinitionRegistryPostProcessor的作用,该类在创建用户自定义的Bean之前执行postProcessBeanDefinitionRegistry方法,正好利用这个创建用户自定义的Bean创建之前执行时机,我们可以将mapper的所有接口扫描出来,让后封装成BeanDefintionHolder,然后向容器注册,后面容器就会给我们创建这些接口的实现(也许大家会有疑问,就算扫描mapper接口那BeanDefintion的类型也是对应mapper类型呀,怎么创建,提前透露一下,扫描并封装成BeanDefintionHolder后还做了一个事情,那就是替换了原本的Mapper类型,具体对象是由替换后的类来创建的)
在这里插入图片描述
下面我们看下postProcessBeanDefinitionRegistry该方法的具体代码
在这里插入图片描述
我们直接跳到干活的方法doScan,可以看到通过扫描将mapper封装成BeanDefintionHolder,然后对结果进行了再一次处理,这里就是对BeanDefintionHolder的类型进行替换了,我们看具体的替换代码
在这里插入图片描述
通过代码我们能看到,先拿到BeanDefintionHolder中的BeanDefintion,取出原本的定义类型,替换为了MapperFactoryBean类型,将原本的类型设置到MapperFactoryBean#mapperInterface属性中
在这里插入图片描述,到此所有的Mapper已经将定义信息注入到Spring容器中,只不过所有的Mapper的BeanDefintion的类型为MapperFactoryBean

5、Mapper代理类注入流程

到了这就简单了,当容器创建UserMapper的时候,由于之前替换了定义信息的类型,所以工厂的一级缓存中看到userMapper对应创建的对象为MapperFactoryBean也就不奇怪了。
在这里插入图片描述
由于FactoryBean的特性,我们获取调用getBean向容器获取对象时,如果传的是&userMapper的话,获取的才是容器一级缓存中的对象,如果通过userMapper获取的时候,获取的就是一级缓存存储对象调用getObject方法创建的对象,创建后会放到容器的factoryBeanObjectCache集合中,后续就不用重复创建啦。
在这里插入图片描述
我们来看下MapperFactoryBean#getObject方法具体实现,乍一看,这不是MybatisMapper创建代理对象的代码嘛。
在这里插入图片描述
到这里我们已经知道了,Mapper对象是如何注入到容器中的,我简单总结一下

小结

  1. 启动类被@MapperScan注解修饰,@MapperScan注解上又被@Import注解修饰,导入了MapperScannerRegistrar
  2. MapperScannerRegistrar类只做了一件事情那就是将MapperScannerConfigurer的定义信息注入到了容器(该类的postProcessBeanDefinitionRegistry方法会在用户自定义Bean创建之前取扫描@MapperScan注解给定的mapper包目录)
  3. 将扫描到的Mapper都封装成BeanDefintionHolder对象,只注册之前,会将所有的Mapper的BeanDefintion类型替换成MapperFactoryBean
  4. 容器创建Mapper对象成功后,存放在一级缓存的是MapperFactoryBean(实现自FactoryBean)
  5. 由于FactoryBean的特性,用户获取Mapper时,会通过一级缓存中的MapperFactoryBean对象调用getObject方法去创建Mapper的代理对象
  6. getObject方法的逻辑就是原生的Mybatis为Mapper创建代理对象的代码

总结

MybatisSpring的整合还是比较简单的,利用了Spring提供给第三方框架整合的扩展类FactoryBean,将SqlSessionFactory注入到容器中。

然后就是对SqlSession的处理,由于默认的DefaultSqlSession有线程安全问题,为了解决安全性问题,引入了SqlSessionTemplate这个类,在它实例化的时候,为SqlSession生成了一个代理类作为了它的成员变量,所有的增删改查操作都委托给SqlSession代理类去完成,代理类的InvokerHanlder的invoke方法中为每次操作都新建了一个SqlSession对象,用完了立马关闭。(主要利用了方法局部变量是线程安全的原理)

最后就是Mapper是如何注入到容器的,看过大家应该也知道了,通过Spring扫描mapper包,拿到所有的mapper接口然后给mapper生成BeanDefintion,立马将BeanDefintion的类型替换成了MapperFactoryBean类型,然后向容器注册,后续容器会生成mapperBean,由于之前类型被替换了,生成的Bean为MapperFactoryBean,由于FactoryBean的机制,在getBean的时候,先拿到mapper生成的MapperFactoryBean对象,然后调用getObject方法为mapper生成代理对象。

如果能仔细看完这篇文章,相信大家对MybatisSpring的整合有了一个新的认识,文章前言中的那些问题应该也有了答案。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值