概述
根据上一篇文章,我们了解了 Mybatis 的配置文件是如何被加载的。在完成这一步之后,Mybatis 还需要根据配置文件中指定的路径去加载 Mapper 接口与写有 sql 的相应映射文件,最终完成两者的绑定。
在本篇文章,我们将结合源码阐述这个过程。
这是关于 Mybatis 的第二篇文章,前文:
一、加载接口
1.XMLConfigBuilder
见名知意,这个类的作用在于解析映射文件,根据上文,我们会了解到他里面有各种 标签名+Element
为名的解析方法,其中,针对 Mapper 映射文件解析方法就是mapperElement()
。
在开始之前,我们需要知道,在映射文件中有四种指定 Mybatis 接口的方式:
1 | <mappers> |
这决定了获取在XMLConfigBuilder
中解析 Mapper 映射文件的方法mapperElement()
会有四种不同处理逻辑:
1 | private void mapperElement(XNode parent) throws Exception { |
2.MapperRegistry
MapperRegistry
类在Configuration
中。当我们使用addMapper()
和 addMappers()
将一个映射文件或者多个映射文件接口添加到配置的时候,最终配置会添加到Configuration
中一个MapperRegistry
实例里。
他存放了接口与其对应的映射文件的各种信息,当接口和通过映射文件生产的实现类代理通常一起添加到注册表中时,也就完成了绑定。
MapperRegistry
内部维护了一个以 Mapper 接口的类作为 key ,接口实现类工厂——暂时先这么理解——作为的 value 的 HashMap 集合 knownMappers
,一个代理工厂中可以生产接口中的所有方法实现:
1 | private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); |
当我们想将一个 package 下的所有 Mapper 接口添加到 knownMappers
中时,会调用 MapperRegistry.addMappers()
:
1 | public void addMappers(String packageName, Class<?> superType) { |
通过ResolverUtil
工具类获取指定包下的所有接口类后,遍历并调用——实际上直接添加单个接口类也是调用这个——addMapper(Class<T> type)
方法将之一个一个的添加到knownMappers
:
1 | public <T> void addMapper(Class<T> type) { |
这里又涉及到了两个类:MapperProxyFactory<>
与MapperAnnotationBuilder
,这两个类都与 Mapper 接口生成实现类有关:
- 前者负责通过代理创建接口实现类;
- 后者扫描代码中的注解并且加载 XXXMapper.xml 的映射文件。
后者在下文就会讲到,前者将在实现类部分的文章细说。总而言之,至此所有 Xml 文件中指定的 Mapper 接口都加载完毕了。
二、映射文件的解析
1.标签与对象之间的关系
我们以最简单的映射文件配置为例:
1 | <mapper namespace="com.huang.read.base.PersonDao"> |
- 接口
Mapper
:一个映射文件对应,也就是<mapper></mapper>
对应一个 Mapper 接口类,以namespace
作为唯一标识; - 方法声明
MappedStatement
:一个完整的方法配置块——比如上面的<select></select>
——对应的 Mybatis 的一个MappedStatement
; - 绑定 SQL
BoundSql
:一段方法声明中的 sql 片段对应 Mybatis 的一个BoundSql
。
所有的映射文件最终都会被解析为上述三个对象。
2.MapperAnnotationBuilder
只有 package 或者 class 的配置方式才会进入这个类,否则像 url 或者 resources 会直接从 XMLMapperBuilder 开始。
MapperAnnotationBuilder
这个类虽然听名是解析注解的,其实由于 mybaits 支持@Select
这样注解式的 sql ,因此解析映射文件与绑定接口也被放在这里一起处理。
我们先看看他的构造方法:
1 | public MapperAnnotationBuilder(Configuration configuration, Class<?> type) { |
MapperBuilderAssistant
这个类主要作用是解析映射文件中的标签,并将创建为对应的对象,这里我们暂时不过多研究,了解作用即可。
接着是入口方法parse()
,这也是一个扫描配置项-初始化为配置类的一次性过程:
1 | // 加载所有映射文件及相关注解 |
完成初始化后,调用parse()
方法将配置信息存入配置类:
1 | public void parse() { |
由于 Mybatis 的缓存与注解不影响主体逻辑,因此这里暂时先忽略。
3.XMLMapperBuilder
元素文件与接口的绑定主要逻辑在于loadXmlResource()
方法:
1 | private void loadXmlResource() { |
这里的 XMLMapperBuilder
实际上跟 XMLConfigBuilder
作用类似,最后也是通过 XPathParser
工具类去解析 Xml 文件,只不过XMLMapperBuilder
解析的是映射文件而不是配置文件。
我们查看它的入口方法 parse()
:
1 | public void parse() { |
4.bindMapperForNamespace 方法
parse()
里调用了 bindMapperForNamespace()
方法,这里根据映射文件的 namespace 属性把映射文件与接口绑定起来了:
1 | private void bindMapperForNamespace() { |
这里的 addMapper()
方法其实还是要往 MapperRegister
里面注册接口。也就是说同一个接口实际上可能会被注册两次,在 XMLConfigBuilder
的 mapperElement()
里注册一次,在现在 XMLMapperBuilder
的bindMapperForNamespace()
里面再注册一次,这也是为什么第二次注册需要检查接口是否重复注册的原因了。
5.parsePendingStatements 方法
映射文件里的标签声明的 sql 方法在XMLMapperBuilder.parse()
中的parsePendingStatements()
方法被解析对应的 statement 对象:
1 | private void parsePendingStatements() { |
对应的转换方法为:
1 | public class XMLStatementBuilder extends BaseBuilder { |
关于方法声明标签中一些属性的具体作用,可以参考Mybatis源码之美:3.7.深入了解select元素。
接着,在 MapperBuilderAssistant.addMappedStatement()
中,经过一系列的处理:
1 | public MappedStatement addMappedStatement( |
最终生成的MappedStatement
会以方法声明的 id 作为标识,加入全局配置类中的 Map 集合mappedStatements
中:
1 | public void addMappedStatement(MappedStatement ms) { |
6.MappedStatement
MappedStatement
对应了映射文件中的一个方法声明,每个MappedStatement
对象都以 namespace.id
的形式区分,里面包含了映射文件中一个方法签名所有必要的数据,包括 sql 对象,映射文件路径,入参和返回值类型等等。
1 | public final class MappedStatement { |
三、关于接口与映射文件关系的思考
1.先加载接口还是映射文件?
关于这个问题,我们回头看看 XMLConfigBuilder
里的 mapperElement()
,其实就能理明白接口与映射文件的加载顺序。
首先,Myabtis 的四种配置方式,其实本质上对应着两种顺序:
- 先找接口,再根据接口全限定名找映射文件:
<package>
和<mapper class="xxx.xxx.xxx">
; - 先找映射文件,再根据 namespace 找接口:
<mapper url="xxx/xxx/xxx.xml">
和<mapper resource="xxx/xxx/xxx.xml">
先找接口
-
直接先在指定位置找到接口类,注册到
MapperRegister
里面; -
在此时创建
MapperAnnotationBuilder
,解析@Select
这样的 sql 标签; -
接着将接口类的全限定名
com.xxx.xxx.class
转成com/xxx/xxx/xml
作为映射文件路径,然后创建XMLMapperBuilder
去根据路径解析映射文件,表现出来的形式就是接口与映射文件需要同包同名; -
解析映射文件的过程中,再根据映射文件的 namespace 属性找接口,试着再注册一次,如果已经注册过就不注册。
另外需要注意的是,这里如果映射文件的 namespace 跟
MapperAnnotationBuilder
的全限定名不一样就会报错。
先找映射文件
- 直接根据配置文件路径找到映射文件;
- 根据映射文件 namespace 找到并且注册接口。
这里我们就完全明白了:
-
为什么
XMLMapperBuilder
解析映射文件注册接口的时候要做重复校验?因为根据配置方式的不同,可能在加载映射文件之前接口已经被注册一次了;
-
为什么 package 和 class 的配置方式需要接口和映射文件同包同名?
因为加载完接口会直接根据接口全限定名找对应的映射文件;
2.接口可不可以有多个映射文件?
答案是可以,根据上述理论,一个接口是可以有多个映射文件的。
也就是说,如果有一个接口叫 Person.class
,里面的 selectOne()
方法在 Person.xml
,而 selectList()
方法在Person2.xml
,而两个映射文件的 namespace 都指向了 Person.class
。
当我们在配置文件中以 <mapper url="xxx/xxx/xxx.xml">
或 <mapper resource="xxx/xxx/xxx.xml">
的方式配置它并运行程序,那么实际上 Person.class
所有的方法都可以正常调用。
从理论上来说,Person.xml
和 Person2.xml
的信息最终都会合并到一个 Statement
里头,这两个映射文件除了 <Mapper></Mapper>
跟标签外,其他内容都是互通的,在Person.xml
配置的 <Sql>
或者 <ResultMap>
标签在 Person2.xml
中一样可以正常使用。
总结
我们总结一下 Mapper 接口与映射文件的加载过程:
-
在
XmlConfigBuilder
解析<mappers></ mappers>
标签,获取要解析的 Mapper 集合如果 mapper 是以 package 标签或者 mapper 的 class 属性配置的,就带着配置文件路径直接跳到步骤 4
如果 mapper 是以 mapper 的 resource 或者 url 属性配置的,就继续步骤 2;
-
在
MapperRegister
中注册接口; -
对每一个 Mapper 接口创建一个
MapperAnnotationBuilder
,用于解析@Select
这样的 sql 标签将
xxx.xxx.xxx
的接口全限定名转为xxx/xxx/xxx.xml
作为步骤 4 的配置文件路径; -
创建一个
XMLMapperBUilder
,并根据配置文件路径找到映射文件并解析; -
调用
bindMapperForNamespace()
方法,解析映射文件并获取映射文件的 namespace 属性,找到并将接口注册到MapperRegister
中; -
调用
parsePendingStatements()
方法,解析映射文件的方法声明,然后通过XMLStatementBuilder
转为MappedStatement
。