我正在参加「掘金·启航计划」
前言
在上文中,研究了MyBatis
针对配置文件的解析,但节省篇幅没有编写对mapper
文件的解析。本文便来探究下MyBatis
如何将Mapper
文件与对应接口关联起来,主要涉及以下代码:
mapperElement(root.evalNode("mappers"));
建议配合官方文档食用更佳。有些部分可能并没太细,写多了又占篇幅,也容易绕进去。更多的是希望读者通过给出的大体思路,去debug
验证这些流程,而不是味同嚼蜡般的阅读。
1. 概述
MyBatis
提供以下三种方式配置Mapper
文件:
<mappers>
<!-- 使用相对于类路径的资源引用 -->
<mapper resource="org/mybatis/builder/xxx.xml"/>
<!-- 使用完全限定资源定位符(URL) -->
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mapper class="org.mybatis.builder.BlogMapper"/>
<!-- 将包内的映射器接口全部注册为映射器 -->
<package name="org.mybatis.builder"/>
</mappers>
注意resource
,url
,class
不能共存,但他们其中之一可以和<package>
共存。对于mapperElement()
源码,节省篇幅此处就不贴出来了。
resource
和url
处理逻辑一致,而class
又和package
内部相似,并增加了注解处理,所以本文以<package>
为主线路。
2. addMapper()
MyBatis
内部使用了MapperRegistry
来保存和注册mapper
。
knownMappers
是一个Map
对象,用于存放已注册的mapper
,key
是对应接口的类对象,value
是MapperProxyFactory
。现在无需知道MapperProxyFactory
是个啥东东,只要知道里面保存了当前接口的类对象即可。
解析文件的核心方法交给了MapperAnnotationBuilder
,别被名称迷惑了,这个类不仅会解析注解,也会解析xml
。
MapperAnnotationBuilder
内部还包含一个MapperBuilderAssistant
对象。从名字上看该类就是一个构建mapper
的助手。其内部有一个字符串resource
属性被赋值为如下内容:com/demo/xxx.java (best guess)
,xxx
表示当前处理的接口。
以上内容便是在parser.parse()
之前所做的处理,接下来看看parse()
干了啥事。
3. MapperAnnotationBuilder#parse()
方法一览:
已给出部分方法说明。我们通过上述流程一步一个脚印,先看loadXmlResource()
3.1 loadXmlResource()
先看一眼整个方法流程
在第一步
if
判断中加上namespace
进行判断的原因是可能在与Spring
整合中,已经由Spring
调用 XMLMapperBuilder#bindMapperForNamespace
加载过了,这部分内容在研究MyBatis-spring
时会再提及(可能要点时间😣)。
后续内容就是查找和接口同目录或者在资源路径中是否包含同名的.xml
文件,有就使用XMLMapperBuilder
进行解析。这就是前面为啥选择<package>为主流程的原因以及前面所说的不要被"迷惑"。
3.1.1 XMLMapperBuilder#parse()
什么这个代码就是处理
XML
格式的mapper
的。不过核心解析方法在configurationElement()
中。此处有个细节点需要注意,在MapperAnnotationBuilder#parse()
中的resource
表示的值是interface xxx.xxx
,而到了此处resource
的值为xxx/xxx/xxx.xml
,代表接口和xml
都被加载,这不难理解吧!
3.1.2 XMLMapperBuilder#configurationElement()
整个方法就是解析
mapper
文件中的各个标签,转换成对应对象。
3.1.2.1 cacheRefElement()
解析<cache-ref>
标签。这部分代码比较简单,我就不贴了,大家可自行参照代码阅读。
- 将当前
namespace
为key
和引用的namespace
为value
保存至Configuration
的cacheRefMap
中。 - 从
Configuration
中获取其他namespace
中的缓存组件,如果指定namespace
没有缓存组件,则会抛出异常,待整个流程结束后,会进行重试。
3.1.2.2 cacheElement()
解析<cache/>
,标签。此处假设各位对缓存的使用都非常了解。
核心方法在
MapperBuilderAssistant#useNewCache()
中,显然是通过建造者模式创建Cache
对象。在出build()
方法外,前面的方法都是在给建造者设置参数。
真核心方法,还是在build()
中:
上述流程做个说明吧:
setDefaultImplementations()
处理默认情况,默认实现为PerpetualCache
,添加默认装饰LruCache
。- 使用反射创建缓存实例,
id
为当前namespace
值。 setCacheProperties()
这个类通过反射设置Cache
中的属性。知识点:此处也用到了上一篇中提到的MetaObject
,而MetaObject
和ObjectWrapper
又有关系。这里阐述下:MetaObject
也是使用了MetaClass
所提供的方法,但为了将对象和MetaClass
关联起来,便使用了ObjectWrapper
这个结构。相较于MetaClass,MetaObject
新增了对复杂属性名称的处理(节省篇幅,各位自行了解吧)。- 对默认实现会添加装饰器。非默认同时不是
LoggingCache
子类则只会添加LoggingCche
。
并不会装饰每个装饰器,而是根据<cache/>
标签定义的属性进行装饰,默认情况会只会装饰LruCache
,SerializedCache
,LoggingCache
,SynchronizedCache
。执行时便是从后开始。
3.1.2.3 resultMapElements()
resultMap
标签解析。跳过parameterMapElement()
原因在该标签已经不推荐使用了,在将来可能废弃。
处理的核心方法有点长,我们一点一点来看。首先是下面这一段
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
type
代表标签中type
属性,如果没有则会从ofType
,resultType
,javaType
中获取一个不为null
的。inheritEnclosingType()
是处理association
和case
派上用场的,用于处理类型优先级,是从父级获取,还是标签中指定的属性。
有人可能会说了,resultMap
标签只能配置type属性啊,为啥还要解析其他属性呢?答案看这里
在继续往下之前,先了解下ResultMapping
。它表示<resultMap>
标签下的每个子标签。Discriminator
则对应<discriminator/>
标签。ok,我们继续:
for (XNode resultChild : resultChildren) {
// 解析constructor标签
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
//解析discriminator标签
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// 解析普通标签
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
看起来好像没啥,依次解析各个标签。解析普通标签和<constructor>
标签只是添加的ResultFlag
类型不同,普通标签只对<id>
标签添加ResultFlag.ID
,而构造器式每个子标签都会添加ResultFlag.CONSTRUCTOR
,idArg
标签还会多添加一个ResultFlag.ID
。
也就是说无论是解析<construtor>
标签还是普通标签都会使用buildResultMappingFromContext()
。
这个方法不是很难,大部分都是赋值操作,但在其中有个细节,在处理collection
,association
,case
时,如果没有定义resultMap
和select
属性,则会重新走一遍上述流程,认为你可能是在对应子标签配置映射,而不是使用外部resultMap
。
这方法最后使用builderAssistant.buildResultMapping()
完成创建resultMapping
,针对普通标签和构造器标签生成ResultMapping
就结束了。回到上文,我们看看<discriminator>
标签是如何处理的。
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String typeHandler = context.getStringAttribute("typeHandler");
Class<?> javaTypeClass = resolveClass(javaType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Map<String, String> discriminatorMap = new HashMap<>();
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute("value");
String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType));
discriminatorMap.put(value, resultMap);
}
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}
处理过程和<result>
标签类似,不过返回的不再是ResultMapping
,而是一个Discriminator
对象。内部保存了case
中的映射关系。由于使用较少此处便不再展开了。
这最后就是将resultMapping整合成一个resultMap
对象,对于继承的会进行resultMapping
替换。
3.1.2.4 sqlElement()
此方法是解析<spl>
标签。会先获取包含databaseId
的sql
片段(如果你忘了这是啥,回头看看第一篇吧),然后再获取和databaseId
无关的。
这个方法可以说是so easy
的,就是获取符合条件的<sql>
标签就添加进一个为Map
机构的sqlFragments
属性中。核心逻辑:先加载和配置文件中配置的databaseId
相同的片段,此时,相同会进行覆盖,再加载和databaseId
无关的片段,存在和上一步相同的id
则跳过该片段,但两个没有databaseId
的sql
片段,则以后面的为准。
总结:以符合配置文件的databaseId
优先,其次以定义顺序。
3.1.2.5 buildStatementFromContext()
这一步可以说是核心了,MyBatis
如何将配置和接口对应起来就在这里展现。也做了databaseId
判断,大体流程和上一小节相似。
核心方法是在XMLStatementBuilder#parseStatementNode()
中,一眼望去,全是属性设置。但还是有几个方法值得一看的。
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
这段代码是处理<include>
标签的,内部处理过程和jquery
处理节点有点像。大致流程是:
- 定位目标
<include>
标签位置。 - 用目标标签替换
<include>
内容(目标标签就是<sql>
标签)。 - 将
sql
标签内容替换到该节点前。 - 删除刚才替换的
<sql>
标签。
感兴趣的可自行debug
。接下来还有个需要注意的地方,就是SqlSource
的创建,里面就包含了我们的sql
资源。
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
对于SqlSource
是啥我们不做讲解,网上大把文章,这里注重流程。
- 通过
XmlScriptBuilder
解析sql
标签。 - 根据标签是否是动态(是否是包含
$
符号)的创建不同的SqlSource
实现。 - 如果是非动态就会创建
RawSqlSource
,在创建时就会将#{}
内容替换为?
。 - 最终封装成
StaticSqlSource
返回。
像<if>
,<where>
这些标签的解析在builder.parseScriptNode()
中的parseDynamicTags(context)
中就完成了,方法挺简单就是通过不同的sqlNode
解析最后组装。
再提一嘴,在使用${}
时,此处并没有解析,而是在使用时,而#{}
在构建xml
时就解析了,到时可以直接发送给数据库,性能相对较好。
最终将每一个<select>..
标签替换成对应的MappedStatement
。
3.1.3 bindMapperForNamespace()
此处会尝试解析接口,是的,你没看错。问题来了,我们就是从接口进来的啊,为什么还要加载接口?这个类是专门加载xml
的,我们扫描接口只是顺带使用的,原则上来说并没有什么问题(我解析xml
,顺带解析下接口注解啥的合理吧?)。
那怎么避免重复加载呢?接口避免重复解析xml
的方式是使用!configuration.isResourceLoaded("namespace:" + type.getName())
判断的,而xml避免重复扫描接口使用的就是!configuration.hasMapper(boundType)
。大家可以在上述流程中看看哪出现了对应语句。
由于我们在调用MapperAnnotationBuilder#parse()
之前已经填充了knownMappers
,所以此处并不会再次解析接口注解(knownMappers.put(type, new MapperProxyFactory<>(type))
)。
3.2 parseCache()
根据注解配置缓存。在前面解析xml的时候已经解析了缓存标签,如果两个共存则会报错(namespace值一致
)。
其实不止该方法,后续的所有方法都可以说是和xml
解析配置相似,只是解析的是注解。后续流程可自行了解。
4. 总结
解析mapper
文件的流程如果宏观来看就是依据配置的路径扫描解析文件中的每一个标签。但其中还包括了对同路径下的接口或者xml
过程,使得流程看起来略显复杂。这其中也能看出MyBatis
期望我们接口和mapper
文件放在同包下。
还有较为难的就是使用了比较多的XXXResolver
类来生成保存xxx
对象,导致流程有些许跳跃,不过也是职责分离的一种方式。
总的来说,这两篇文章就跟厨房的配菜师傅一样,将食材处理好,分类,对应成MyBatis
就是解析文件,封装成对应对象。