系列文章目录
文章目录
前言
本文是对之前的MyBatis原理源码一文的补充,工作中都是Mybatis不会单独使用而是整合Spring一起使用,整合以后我们都感觉不到SqlSessionFactory
和SqlSession
的存在,需要用到哪个Mapper时,直接使用@Autowired
注入就行,而且 SqlSession
默认的实现DefaultSqlSession
是线程不安全的,别问我怎么知道的,DefaultSqlSession
的类注释告诉我的
到这里大家肯定有几个问题想问
- 什么原因导致了DefaultSqlSession线程不安全?
- MyBatis和Spring整合是如何解决SqlSession的线程安全问题?
- Mapper接口是怎么注入到Spring容器的?
我们就带着这两个问题去看源码,看完源码后,这两个问题自然就懂了。现在的项目基本都是基于SpringBoot
搭建的,所以这里我们直接看MyBatis
和SpringBoot
的整合源码,因为讲的是和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
方法获取连接对象的话,肯定拿到的是同一个连接对象,多线程使用一个连接并发查询的话,肯定会存在数据覆盖的问题,这就是为啥有线程安全问题。
先说一下解决线程安全的核心思想:方法局部变量存放在线程栈的栈帧中,线程栈是线程独占的
MyBatis在Spring整合中就是利用了这个思想来,如果SqlSession对象作为成员变量的话(成员变量存在JVM的堆,而堆是线程共享的)必然会有线程安全问题,如果将SqlSession作为方法的局部变量的话,那就是线程私有的,就不存在线程安全问题。具体解决方案我们去看源码。
我们可以看到MyBatis的自动装配类中,注入了SqlSessionTemplate
对象,而该类实现自SqlSession
,重写了所有的方法,将SqlSession作为了成员变量(这是个代理对象),后续的增删改查方法都是委托给SqlSession的代理对象去实现,
自动装配类中创建SqlSessionTemplate对象会走重载然后走到下面这个构造方法中,从这里可以看出来成员变量SqlSession就是SqlSessionTemplate实例化的时候创建的代理对象,代理对象的InvocationHandler为SqlSessionInterceptor,因为增删改查都是通过该代理对象来操作的,所以操作一定会执行到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中是如何注入到容器中的
MyBatis和SpringBoot使用时都会在配置类上加一个@MapperScan注解,告诉MyBatis去哪里找Mapper接口,我直接放在了启动类上面
文章前面不是说扫描工作是MapperScannerRegistrar来完成的嘛,怎么没看到该类。别急我们打开这个注解看看里面有啥
我们看下MapperScannerRegistrar
的类图,请记住它实现了ImportBeanDefinitionRegistrar
接口
Spring在解析配置类的时候,会去解析类上的注解,其中一个比较重要的就是@Import注解,然后判断导入的类是否实现自 ImportSelector
和ImportBeanDefinitionRegistrar
(这两个接口都是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
方法具体实现,乍一看,这不是Mybatis
为Mapper创建代理对象的代码嘛。
到这里我们已经知道了,Mapper对象是如何注入到容器中的,我简单总结一下
小结
- 启动类被
@MapperScan
注解修饰,@MapperScan
注解上又被@Import
注解修饰,导入了MapperScannerRegistrar
MapperScannerRegistrar
类只做了一件事情那就是将MapperScannerConfigurer
的定义信息注入到了容器(该类的postProcessBeanDefinitionRegistry
方法会在用户自定义Bean创建之前取扫描@MapperScan
注解给定的mapper包目录)- 将扫描到的Mapper都封装成
BeanDefintionHolder
对象,只注册之前,会将所有的Mapper的BeanDefintion
类型替换成MapperFactoryBean
- 容器创建
Mapper
对象成功后,存放在一级缓存的是MapperFactoryBean
(实现自FactoryBean
) - 由于
FactoryBean
的特性,用户获取Mapper时,会通过一级缓存中的MapperFactoryBean
对象调用getObject
方法去创建Mapper的代理对象 getObject
方法的逻辑就是原生的Mybatis
为Mapper创建代理对象的代码
总结
Mybatis和Spring的整合还是比较简单的,利用了Spring提供给第三方框架整合的扩展类FactoryBean,将SqlSessionFactory注入到容器中。
然后就是对SqlSession的处理,由于默认的DefaultSqlSession有线程安全问题,为了解决安全性问题,引入了SqlSessionTemplate这个类,在它实例化的时候,为SqlSession生成了一个代理类作为了它的成员变量,所有的增删改查操作都委托给SqlSession代理类去完成,代理类的InvokerHanlder的invoke方法中为每次操作都新建了一个SqlSession对象,用完了立马关闭。(主要利用了方法局部变量是线程安全的原理)
最后就是Mapper是如何注入到容器的,看过大家应该也知道了,通过Spring扫描mapper包,拿到所有的mapper接口然后给mapper生成BeanDefintion,立马将BeanDefintion的类型替换成了MapperFactoryBean类型,然后向容器注册,后续容器会生成mapper的Bean,由于之前类型被替换了,生成的Bean为MapperFactoryBean,由于FactoryBean的机制,在getBean的时候,先拿到mapper生成的MapperFactoryBean对象,然后调用getObject方法为mapper生成代理对象。
如果能仔细看完这篇文章,相信大家对Mybatis和Spring的整合有了一个新的认识,文章前言中的那些问题应该也有了答案。