MyBatis-Spring中间件逻辑分析(怎么把Mapper接口注册到Spring中)
1. 文档介绍
1.1. 为什么要写这个文档
接触Spring和MyBatis也挺久的了,但是一直还停留在使用的层面上,导致很多时候光知道怎么用,而不知道其具体原理,这样就很难做一些针对性的优化工作,Spring和MyBatis都已经是很庞大的框架了,分析起来会需要很多的时间,所以我先从两者之间的中间件MyBatis-Spring开始,一步一步开始学习两个框架的原理和精髓
1.2. MyBatis-Spring是什么
当我们在使用MyBatis时,一般是编写一个Mapper接口和一个Mapper.xml文件,我们都知道接口是不能直接被实例化的,然而我们一般在service层中编写的注入属性都是Mapper接口,那么Spring是如何对该接口进行实例化的呢?
一般而言,如果我们使用Spring和MyBatis作为我们的开发框架时,在搭建开发环境的时候,都会做一个Spring与MyBatis的整合,使用到的就是MyBatis-Spring这个中间件,MyBatis-Spring中间件帮我们把mapper接口和mapper.xml文件对应的代理类注册到Spring中,因此,我们在service层中就能根据类型注入,将对应mapper接口的代理类注入到service层中,我们才能够调用到对应的方法
1.3 整体原理
在Spring开发中,我们通常是在service层中通过依赖注入Dao层的实例,在MyBatis中,Mapper接口即对应着Dao实例,MyBatis-Spring中间件就是把MyBatis中的mapper.xml和mapper.java对应的Mapper接口注册到Spring容器中,使得service层可以直接通过以来注入获取到Mapper接口
1.3.1 注册
在Spring中所有的Mapper接口都会被注册为MapperFactoryBean,所有的MapperFactoryBean会共享一个SqlSessionFactory,该SqlSessionFactory由SqlSessionFactoryBean创建,而在sqlSessionFactory的configuration属性中存的是一个Configuration对象,configuratiao对象中的mapperRegistry属性中存储了一个MapperRegistry对象,MapperRegistry对象中的knownMappers属性是一个key为mapper.java文件对应接口的类型,value为MapperProxyFactory的对象。
1.3.2 获取
当从Spring中获取Mapper接口时,将会调用对应的MapperFactoryBean的getObjects方法,该方法返回值即为对应的MapperProxyFactory创建的MapperProxy动态代理
2 扫描需要注册到Spring中的Mapper接口
在Spring中注册MapperFactoryBean的流程
2.1 整体流程图
2.2 配置MapperScannerConfigurer
还是来看一下在文档最开头Spring整合MyBatis时的配置
在这里,我只配置了basePackage和sqlSessionFactoryBeanName两个属性,还有一些其他配置,这里我们就不一一解析了,只分析注册MapperFactoryBean最主要的流程。
2.3 扫描basePackage下的所有候选对象
先来看看MapperScannerConfigurer类的定义
首先实现的是postProcessBeanDefinitionRegistry接口,实现了这个接口的类会再Spring初始化Bean定义的时候被调用postProcessBeanDefinitionRegistry方法,用来自定义注册Bean的定义逻辑,先来看看这个方法做了些什么事情
在一开始先是调用了本类中的processPropertyPlaceHolders方法,用来设置自己本身的Bean属性,接下来定义ClassPathMapperScanner即Mapper扫描器来扫描对应的Mapper文件,该扫描器继承了Spring中的ClassPathBeanDefinitionScanner,该扫描器是扫描需要实例化的Bean并把它们加载到BeanDefinitionHolder集合中,以便后续的初始化Bean,这里我们直接来看其中的扫描逻辑
这里先调用了父类的doScan方法,我们需要先看看父类的doScan方法做了什么事情,
核心方法即findCandidateCompents,即从指定的包中找到需要初始化的候选人(即需要初始化的Bean)的定义并把他们放进Set<BeandDefinitionHolder>中,
这两个方法都是在获取候选者,上面的是当设置了ResourceLoader时的调用,其实两个方法在本质上是一样的,我们直接来看下面这个方法
可以看到,在寻找候选人的过程中,直接获取对应basePackage下的所有资源,然后会对资源进行判断,是否满足候选人的条件
2.4 候选人的条件
上节中提到了对候选人进行筛选的方法,idCandilateComponent这个方法实际上是由ClassPathMapperScanner覆盖的,因此,当调用这个方法时,将会执行ClassPathMapperScanner#isCandidateComponent方法
即对应扫描的应该为basePackage下独立的接口(即非内部接口),至此,找到了需要实例化的接口,
2.5 将所有候选对象定义为Spring中的Bean
再获取完候选对象之后,即父类的doScan扫描完毕之后,Spring会将所有满足条件的对象存储到beanDefinition,即Spring中的bean定义对象,这也是Spring初始化Bean的基础,
2.6 设置候选对象的Bean属性
在Spring中获取到了这些Bean的定义之后,MyBatis-Spring中间件还需要对这些Bean做一些属性上的设置,让其能满足使用的条件,我们接下来看看都有哪些属性的配置
继续来看ClassPathMapperScanner#doScan方法,
ClassPathMapperScanner#processBeanDefinitions就是在Spring处理完Bean定义后由MyBatis-Spring来处理的逻辑
2.6.1 设置bean定义的Class类型
上图中最重要的逻辑即先设置构造函数参数为原本读取的BeanDefinition中的类名(即Mapper接口的名称),把所有的Mapper接口定义BeanClass类型设置为MapperFactoryBean,并设置其构造器参数为对应的Mapper接口类型
上图为MapperFactoryBean的构造函数
2.6.2 设置SqlSessionFactoryBean
在processBeanDefinitions()方法中还有一段比较重要的逻辑
在这里,是需要配置MapperFactoryBean的父类SqlSessionDaoSupport中的sqlSessionTemplate属性,当配置的时sqlSessionFactory属性时,MapperFactoryBean的在初始化时,会使用sqlSessionFactory属性来构建sqlSessionTemplate
从2.6节可知,设置到MapperFactoryBean中sqlSessionFactory或者是sqlSessionTemplate都是从spring中获取的,这就意味着,
当设置的是sqlSessionFactory时,每个MapperFactoryBean中的sqlSessionTemplate是不同的,但是最终的sqlSessionFactory是相同的,
当设置的是sqlSessionTemplate时,每个MapperFactoryBean中sqlSessionTemplate就是同一个
3. SqlSessionFactory初始化
构建SqlSessionFactory并存储MapperProxyFactory流程
3.1 整体流程图
3.2 配置SqlSessionFactoryBean
首先看段Spring整合MyBatis时的经典配置
在Spring使用MyBatis-Spring中间件来整合MyBatis必须配置这两个Bean,首先我们从sqlSessionFactoryBean开始,SqlSessionFactoryBean,见名思意,实际上是一个factoryBean,即用来生产对象的工厂Bean,SqlSessionFactoryBean是用来创建上文中提到的SqlSessionFactory的
3.3 构建SqlSessionFactoryBean
先来看看SqlSessionFactoryBean类的定义
实现了InitializingBean的类会在Spring初始化完该Bean后的时候会执行afterPropertiesSet方法,我们来看看这个方法里做了写什么
先做了一些数据校验,即必须配置dataSource,sqlSessionFactoryBuilder会在创建对象的时候初始化,不用关心,而对于configuration或configLocation而言,两者不能同时配置,只能配置其中一个,或者一个都不配置,这样在构建sqlSessionFactory时就会采用默认配置,然后开始构建sqlSessionFactory实例
3.4 配置了configuration或configLocations的处理
在这个方法中,首先会判断是否SqlSessionFactoryBean的Spring配置里针对是否配置了configuration属性或configLocation属性进行针对处理,没有的话就新建一个新的configuration即采用默认配置,接下来,我们先对配置了configLocation的情况进行一次分析,先来看下MyBatis-configuration.xml的内容,先来看一段经典配置
在这个配置里面,我们配置了一个Mapper接口,其接口类型为UserMapper,
再回到构建sqlSessionFactory的过程中
在该方法中,使用XMLconfigBuilder初始化了对改配置文件的读取,然后对该配置进行了解析,我们先进去看看解析的过程
这里我们跳过对其他配置的解析,直接来看对<mappers>节点的解析,对<mappers>节点下的所有<mapper>节点进行循环遍历,并调用了configuration#addMapper方法
3.5 配置了mapperLocations的情况处理
我们再来看下配置了mapperLocations的情况
当我们配置了mapperLocations后,在buildSqlSessionFactory中也会对这一属性进行处理
这里循环遍历mapperLocations的配置,并使用XMLMapperBuilder来进行读取并解析,我们直接来看对应解析过程
获取到的<mapper>节点就是mapperLocation下所有的mapper.xml文件下的mapper节点,再对这些文件进行初始化过程,这里我们也只看注册对应Mapper接口的逻辑
跟之前一样,这里也是使用了configuration.addMapper方法,
3.6 configuration.addMapper
上文中提到了无论是配置了configuration或configLocation还是配置了mapperLocations,都会调用Configuration#addMapper方法,下面我们就来看下这个方法,下图为Configuration#addMapper方法
这里委托给mapperRegistry来添加对应的代理,再进去MapperRegistry#addMapper方法,这个方法的工作就是在初始化mapper接口对应的动态代理类了
先校验此Mapper的类型是否是接口类型,因为只有MyBatis只处理接口,再校验是否已存在此mapper,可以看到这里初始化了一个MapperProxyFactory并放到已知的Mapper类型Map中,这个MapperProxyFactory即Mapper动态代理的工厂,使用工厂模式来创建Mapper动态代理
4 MapperFactoryBean初始化
4.1 整体流程图
4.2 afterPropertiesSet
在设置完MapperFactoryBean的beanDefinition信息之后,Spring在初始化Bean时就会初始化这个Bean,再来看看这个Bean的定义
MapperFactoryBean继承了SqlSessionDaoSupport,而SqlSessionDaoSupport又继承了DaoSupport类,DaoSupport类又实现了InitializingBean接口,意味着当MapperFactoryBean初始化完毕后会调用DaoSupport的afterPropertiesSet方法
来看DaoSupport类中的afterPropertiesSet方法
这里调用了checkDaoConfig方法,最终会调用到MapperFactoryBean中的checkDaoConfig方法
4.3 checkDaoConfig
接下来来看MapperFactoryBean的checkDaoConfig方法
可以看到,在MapperFactoryBean属性设置完毕之后,会调用这个checkDaoConfig来检查mapperRegistry中是否存在对应的MapperProxyFactory,如果没有,将会把对应的MapperProxyFactory添加进去,
4.4 解决的问题
不知道看过上文的读者发现没有,如果在SqlSessionFactoryBean中未配置configuration,configLocation和mapperLocations时,在MyBatis-Spring初始化的第一步完成后(即章节3.1中所提及的内容),SqlSessionFactory中的mapperRegistry中是没有与章节3.2中所注册的Mapper接口的对应关系的,那么,本节中的内容,就是确保mapperRegistry中一定存在两者之间的对应关系,这也是从Spring中获取到Mapper接口的基础
5 从Spring中获取Mapper接口
5.1 整体流程图
5.2 从Spring中获取Bean的流程介绍
在上一节中,我们扫描到的Mapper接口在Spring中都设置成了MapperFactoryBean,那这么设置有什么用呢?
这里就需要对从Spring中获取Bean有一些了解了,这里先简单介绍一下从Spring中获取Bean的流程
5.2.1 从Spring中获取Bean的流程图
5.2.2 从Spring中获取Bean具体逻辑
从Spring获取Bean时,都是从BeanFactory即Bean工厂中构建并获取,我们先来看下BeanFactory的源码,当获取对应的Bean时,会调用对应的BeanFactory#getBean方法
里面有几个getBean方法的重载
以及判断Singleton或Prototype的方法,
我们直接看它的默认实现AbstractBeanFactory#getBean方法
这里我们直接以单例模式举例了,该方法主要是判断需要的Bean是Singleton还是prototype的,然后调用对应的方法,我们这里直接看单例模式下的获取Bean
这里我们可以看到,对于Bean的类型有一个区别判断,如果是普通Bean的话会直接返回实例,而对于FactoryBean而言,会执行另外一段逻辑
其实不用管其他的一些判断逻辑,里面的核心逻辑就一个,调用对应的FactoryBean的getObject方法,
5.3 MapperFactoryBean#getObject()
那对于我们的MapperFactoryBean而言会怎样呢?
很明显,MapperFactoryBean实际上是一个FactoryBean,那我们再进去看看它获取Bean的逻辑
对于MapperFactoryBean即为调用其getObjeact方法,而在MapperFactoryBean对该方法有一个重写
在这个方法中,getSqlSession为其父类的方法,
在sqlSessionDaoSupport中
这个sqlSessionTemplate即为在doScan中设置Beandifinition时设置的,
然后再调用SqlSessionTemplate中的getMapper方法
再进去SqlSessionTemplate的getConfiguration方法,实际上获取的是sqlSessionFactory的configuration,
5.4 Configuration#getMapper
然后再调用Configuration#getMapper方法
可以看到,实际上还是转交给mapperRegistry中的getMapper方法
5.5 MapperRegistry#getMapper
这里我们可以看到,最终是从章节2,章节3中存储的knownMappers中获取到对应Mapper接口的MapperProxyFactory,并最终调用到MapperProxyFactory#newInstance方法获取到对应的MapperProxy
这里就是使用newInstance来构建一个新的MapperProxy,即Mapper接口的动态代理类,这里使用的是java自带的动态代理,这里的泛型T就是对应的Mapper接口的类型
6 总结及验证
6.1 总结
从上面的分析中可以看出,由SqlSessionFactoryBean来扫描mapper接口并配置对应的MapperProxyFactory到mapperRegistry中的knownMappers Map中,由MapperScannerConfigurer来扫描对应的Mapper接口并生成对应的MapperFactoryBean,从Spring中获取mapper接口动态代理类时会调用MapperFactoryBean的getObjects方法,并最终调用到mapperRegistry中的getMapper方法调用mapperProxyFactory的newInstance生成对应的MapperProxy即Mapper接口的动态代理,
可以看到,这里就调用了MapperProxyFactory的newInstance方法获取到对应Mapper接口的动态代理类了,至此,我们基本完成了将Mapper接口注册到Spring的过程
6.2 验证
接下来我们来看一个简单的验证
Spring配置如下
对应的markerInterface如下,如果设置了该属性,只有继承了该接口的Mapper接口会被注册
FooMapper接口如下
测试类如下
其对应的运行结果为
即我们获取到的fooMapper实际上是一个动态代理,其在Spring中的类型为FooMapper接口类型,创建它的工厂Bean为MapperFactoryBean类型,与我们分析源代码的结果是一致的
至此,我们初步分析了MyBatis-Spring中间件的原理,对于Mapper接口是如何注册到Spring中的原理有了一个不错的了解
7 写在最后
任何问题请联系hei12138@outlook.com