系列文章目录
文章目录
前言
本文是对之前的MyBatis原理源码一文的补充,工作中都是Mybatis不会单独使用而是整合Spring一起使用,整合以后我们都感觉不到SqlSessionFactory
和SqlSession
的存在,需要用到哪个Mapper时,直接使用@Autowired
注入就行,而且 SqlSession
默认的实现DefaultSqlSession
是线程不安全的,别问我怎么知道的,DefaultSqlSession
的类注释告诉我的
到这里大家肯定有几个问题想问
- 什么原因导致了
DefaultSqlSession
线程不安全?
2.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
的整合有了一个新的认识,文章前言中的那些问题应该也有了答案。