暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

mybatis源码分析02-映射文件解析

易林的博客 2019-12-11
290

1.简介

我们在上一篇文章说了mybatis得到配置文件解析,这篇我们来分析mybatis的映射文件的解析,MyBatis 的真正强大在于它的映射语句,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 为聚焦于 SQL 而构建,以尽可能地为你减少麻烦。

SQL 映射文件只有很少的几个顶级元素

cache – 对给定命名空间的缓存配置。

cache-ref – 对其他命名空间缓存配置的引用。

resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。

sql – 可被其他语句引用的可重用语句块。

我们本篇文章主要就是分析上面几个标签的解析过程

2.解析流程分析

在上一篇文章我们就说了配置文件的解析入口,这里也就包括了我们的映射文件,我们直接去看相关代码

2.1.入口分析

我们先看来下四种方式:mapper在配置文件中的配置

<mappers>  
<!-- 通过package元素将会把指定包下面的所有Mapper接口进行注册 -->
<package name="com.yilin.mybatis.mappers"/>
<!-- 通过mapper元素的resource属性可以指定一个相对于类路径的Mapper.xml文件 -->
<mapper resource="com/yilin/mybatis/mapper/UserMapper.xml"/>
<!-- 通过mapper元素的url属性可以指定一个通过URL请求道的Mapper.xml文件 -->
<mapper url="file:///E:/UserMapper.xml"/>
<!-- 通过mapper元素的class属性可以指定一个Mapper接口进行注册 -->
<mapper class="com.yilin.mybatis.mappers.UserMapper"/>
</mappers>

当使用mapper元素进行Mapper定义的时候需要注意:mapper的三个属性resource、url和class对于每个mapper元素只能指定一个,要么指定resource属性,要么指定url属性,要么指定class属性,不能都指定,也不能都不指定

接下来我们继续看配置文件的解析入口

XMLConfigBuilder

 private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
//循环遍历
for (XNode child : parent.getChildren()) {
//对于package类型的
if ("package".equals(child.getName())) {
//获取packageName
String mapperPackage = child.getStringAttribute("name");
/*
做了几件事:
1:从指定的包中查找mapper接口
2:根据mapper接口解析映射配置
3:把解好的mapper放入configuration
*/

configuration.addMappers(mapperPackage);
} else {
//分别获取resourcurlclass属性
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");

//resource不为空,其它两个是空的。从resource指定的路径中加载我们的mapper配置
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//解析器
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//进行mapper文件的解析
mapperParser.parse();
}// url不为空,其它两个是空的。从url指定的路径中加载我们的mapper配置
else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}//mapperClass不为空,其它两个是空的。从mapperClass指定的路径中加载我们的mapper配置
else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
}
//否则报异常
else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}


上面的过程也比较简单:

1:遍历子节点

2:根据不同的类型创建解析器

3:进行配置文件的解析

下面我们开始分析解析的入口

 public void parse() {
//映射文件是否被解析过
if (!configuration.isResourceLoaded(resource)) {
//解析mapper节点
configurationElement(parser.evalNode("/mapper"));
//添加资源的路径到:已解析的资源集合中,为了避免再次解析
configuration.addLoadedResource(resource);
//绑定mapper接口通过命名空间
bindMapperForNamespace();
}
//处理一些未完成的解析
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}

上面就是映射文件的解析流程,分为3步

1.解析mapper节点

2:绑定mapper接口通过命名空间

3:处理一些未完成的解析流程

接下来我们会对上面3点过程进行分析

2.2.解析前的背景知识

首先我们看下一个mybatis官方提供的一个模板配置

select的模板配置

<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">

元素解析

属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句
parameterType将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置(unset)
parameterMap这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性
resultType从这条语句中返回的期望类型的类的完全限定名或别名。注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同时使用。
resultMap外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同时使用
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false
useCache将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)
fetchSize这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。默认值为未设置(unset)(依赖驱动)。
statementTypeSTATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
resultSetTypeFORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖驱动)
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略
resultOrdered这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
resultSets这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的

delete.update,insert的模板配置

<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">

<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">

<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">

元素解析

属性描述
id命名空间中的唯一标识符,可被用来代表这条语句。
parameterType将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:true(对于 insert、update 和 delete 语句)
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。
statementTypeSTATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
useGeneratedKeys(仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。
keyProperty(仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认值:未设置(unset)。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn(仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望使用多个生成的列,也可以设置为逗号分隔的属性名称列表。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。

下面我们看一个简单实例代码

<mapper namespace="com.yilin.dao.example">
<sql id="sometable">
${prefix}Table
</sql>

<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select>
</mapper>

上面文件的解析步骤是放在了XMLMapperBuilder的configurationElement中,下面我们开始看相关代码

private void configurationElement(XNode context) {
try {
//解析命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//设置命名空间到 builderAssistant
builderAssistant.setCurrentNamespace(namespace);

//解析 <cache-ref> 节点
cacheRefElement(context.evalNode("cache-ref"));

//解析 <cache> 节点
cacheElement(context.evalNode("cache"));

//废弃了
parameterMapElement(context.evalNodes("/mapper/parameterMap"));

// 解析 <resultMap> 节点
resultMapElements(context.evalNodes("/mapper/resultMap"));

// 解析 <sql> 节点
sqlElement(context.evalNodes("/mapper/sql"));

//解析 <select><insert><update><delete> 等节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}

上面就是整个解析过程,接下来我们会对上面过程进行分析

在分析<cache-ref> 之前,我们有必要先分析 <cache> 节点

2.3.解析<cache>节点

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。为了使它更加强大而且易于配置,在 MyBatis 3 中的缓存实现进行了许多改进。

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存( SqlSession 级别的缓存)。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

<cache/>


基本上就是这样。这个简单语句的效果如下:

  1. 映射语句文件中的所有 select 语句的结果将会被缓存。

  2. 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

  3. 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。

  4. 缓存不会定时进行刷新(也就是说,没有刷新间隔)。

  5. 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。

  6. 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域

这些属性可以通过 cache 元素的属性来修改。比如:

<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。

  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。

  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。速度上会慢一些,但是更安全,因此默认值是 false。

除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为

<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器,

通过 cache 元素传递属性值,例如,上面的例子将在你的缓存实现上调用一个名为 setCacheFile(String file) 的方法

你可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。你也可以使用占位符(如 ${cache.file}),以便替换成在配置文件属性中定义的值

从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。如果想要使用这个特性,请在你的自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
void initialize() throws Exception;
}

注意

上面中对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。

请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。默认情况下,语句会这样来配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

上面我们说了很多缓存的背景知识,下面我们开始进行源码分析

 private void cacheElement(XNode context) throws Exception {
if (context != null) {
//通过上面的背景知识的学习,下面我其实就是获取各种配置在cache中的属性了

//缓存类型(默认是PERPETUAL)
String type = context.getStringAttribute("type", "PERPETUAL");
//如果有别名,获取到真正的名字
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);

//缓存装饰器(默认LRU)
String eviction = context.getStringAttribute("eviction", "LRU");
//如果有别名,获取到真正的名字
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);

Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);

//获取子节点的配置
Properties props = context.getChildrenAsProperties();

//构建缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
}
}

代码比较简单了,大家对着注释读

下面我们来分析构建缓存对象

MapperBuilderAssistant

public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
Properties props) {

//缓存类型(默认缓存类型为PerpetualCache)
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
//缓存策略(默认缓存策略为LruCache)
evictionClass = valueOrDefault(evictionClass, LruCache.class);

//使用建造模式构建缓存对象
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.properties(props)
.build();

//添加缓存到configuration
configuration.addCache(cache);

//设置currentCache等于当前构建的cache
currentCache = cache;

//返回缓存对象
return cache;
}

接下来我们继续看缓存对象的构建 build方法

  public Cache build() {

/*
设置默认的缓存类型(PerpetualCache)和缓存装饰器(FifoCache:
这个代码执行的条件是缓存类型为空,很显然,我们上面设置了做了默认值的处理,正常情况下都不会执行。
当然也有些情况会执行:比如我们在构建缓存对象时:

Cache cache = new CacheBuilder(currentNamespace)


.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.properties(props)
.build();

没有设置implementation,其实这个地方相当于mybatis做的一个空指针策略,因为如果真的出现我们上面说的情况了,最起码我们在构建缓存对象时,不会报空指针异常
*/

setDefaultImplementations();

//反射创建缓存实例
Cache cache = newBaseCacheInstance(implementation, id);
//设置缓存属性
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
//如果缓存的名字是已ibatis开头的
if (cache.getClass().getName().startsWith("org.apache.ibatis")) {
//循环遍历装饰器集合
for (Class<? extends Cache> decorator : decorators) {
//反射创建缓存实例
cache = newCacheDecoratorInstance(decorator, cache);
//设置属性到缓存实例中
setCacheProperties(cache);
}
//标准的装饰器,(LoggingCacheSynchronizedCache)
cache = setStandardDecorators(cache);
}
return cache;
}

上面就是缓存的构建过程,下面我们总结下:

1:设置默认的缓存类型和缓存装饰器

2.通过反射创建缓存实例,设置缓存属性

3.对于是已org.apache.ibatis开头的缓存,遍历器装饰器集合。反射创建缓存,设置缓存属性

4.如果没有装饰器,则设置表不的缓存装饰器

下面我们继续对上面的过程进行分析

  private void setDefaultImplementations() {
if (implementation == null) {
//设置默认的缓存实现类
implementation = PerpetualCache.class;
//如果装饰器为空,添加FifoCache
if (decorators.size() == 0) {
decorators.add(FifoCache.class);
}
}
}

/*
* FIFO (first in, first out) cache decorator
*/

public class FifoCache implements Cache {
//无关代码
}

上面就是整个缓存对象的创建过程

下面我们来分析设置属性相关代码setCacheProperties

正常情况下我们使用mybatis自己的缓存时,一般都不会为它配置自定义属性,只有在我们在使用一些第三方缓存的时候,需要我们设置 一些属性,比如我们前面说的设置cacheFile

private void setCacheProperties(Cache cache) {
if (properties != null) {

//生成缓存的元对象实例(我们可以方便进行访问成员变量,get.set方法等)
MetaObject metaCache = SystemMetaObject.forObject(cache);

for (Map.Entry<Object, Object> entry : properties.entrySet()) {
//获取属性名和属性值
String name = (String) entry.getKey();
String value = (String) entry.getValue();

//如果有熟悉的set方法
if (metaCache.hasSetter(name)) {

//获取set方法的参数类型
Class<?> type = metaCache.getSetterType(name);

//根据不同的类型,进行数据转换,然后设置值
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
||
Integer.class == type) {
metaCache.setValue(name, Integer.valueOf(value));
} else if (long.class == type
||
Long.class == type) {
metaCache.setValue(name, Long.valueOf(value));
} else if (short.class == type
||
Short.class == type) {
metaCache.setValue(name, Short.valueOf(value));
} else if (byte.class == type
||
Byte.class == type) {
metaCache.setValue(name, Byte.valueOf(value));
} else if (float.class == type
||
Float.class == type) {
metaCache.setValue(name, Float.valueOf(value));
} else if (boolean.class == type
||
Boolean.class == type) {
metaCache.setValue(name, Boolean.valueOf(value));
} else if (double.class == type
||
Double.class == type) {
metaCache.setValue(name, Double.valueOf(value));
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
}

接下来我们继续分析设置标准装饰器的过程

private Cache setStandardDecorators(Cache cache) {
try {
//获取缓存的元书籍类型
MetaObject metaCache = SystemMetaObject.forObject(cache);

if (size != null && metaCache.hasSetter("size")) {
//设置size属性
metaCache.setValue("size", size);
}
if (clearInterval != null) {
//clearInterval 不为空,设置 ScheduledCache 装饰器
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
//设置SerializedCache(
cache = new SerializedCache(cache);
}
//日志缓存(让缓存具有打印日志的能力)
cache = new LoggingCache(cache);
//同步缓存 (让缓存具有同步缓存的能力)
cache = new SynchronizedCache(cache);
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}

上面除了LoggingCache和SynchronizedCache是两个必要的装饰器外,其余的用户按照自己的意愿进行配置

到这里我们就分析完了整个cache标签了

2.4.解析<cache-ref>节点

对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求(二级缓存可以共用),你可以使用 cache-ref 元素来引用另一个缓存。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>


接下来我们看cache-ref的解析过程

  private void cacheRefElement(XNode context) {
if (context != null) {
//configuration中添加cache-ref
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));

//创建cacheRefResolver
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//解析
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//捕获IncompleteElementException异常
//cacheRefResolver放入到configuration incompleteCacheRefs 集合中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}


下面我们继续看解析过程

public Cache resolveCacheRef() {

return assistant.useCacheRef(cacheRefNamespace);
}

MapperBuilderAssistant

public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//根据命名空间获取缓存示例
Cache cache = configuration.getCache(namespace);

/*
为空,报异常,出现的有两种原因:
1namespace是一个根本不存在的命名空间
2namespace存在,但是缓存示例还未创建
*/

if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
//设置当前当前缓存
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}

到这里cache-ref就分析完了,过程比价简单,就是通过namespace去找我们的cache标签设置的缓存,即二级缓存

2.5.解析<resultMap>节点

resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的长达数千行的代码。ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。

我们来看一个官方的例子

<!-- 非常复杂的语句 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
where B.id = #{id}
</select>

你可能想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。我们来看看下面这个完整的例子,它是一个非常复杂的结果映射(假设作者,博客,博文,评论和标签都是类型别名)。不用紧张,我们会一步一步来说明。虽然它看起来令人望而生畏,但其实非常简单。

<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>

结果映射(resultMap)

  • constructor - 用于在实例化类时,注入结果到构造方法中

    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能

    • arg - 将被注入到构造方法的一个普通结果

  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能

  • result – 注入到字段或 JavaBean 属性的普通结果

  • association – 一个复杂类型的关联;许多结果将包装成这种类型 嵌套结果映射 – 关联本身可以是一个 resultMap 元素,或者从别处引用一个

  • collection – 一个复杂类型的集合 嵌套结果映射 – 集合本身可以是一个 resultMap 元素,或者从别处引用一个

  • discriminator – 使用结果值来决定使用哪个 resultMap

    • case – 基于某些值的结果映射嵌套结果映射 – case 本身可以是一个 resultMap 元素,因此可以具有相同的结构和元素,或者从别处引用一个

下面我们直接分析该标签的解析过程

XMLMapperBuilder


private void resultMapElements(List<XNode> list) throws Exception {
//遍历<resultMap>
for (XNode resultMapNode : list) {
try {
//解析<resultMap>节点
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
//重载方法
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());

//获取id
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());

//获取type
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));

//获取extends
String extend = resultMapNode.getStringAttribute("extends");

//获取autoMapping
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");

//解析type对应的class类型
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);

//获取resultMap的子节点
List<XNode> resultChildren = resultMapNode.getChildren();
//遍历
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
//解析 constructor 节点,并生成对应的 ResultMapping
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
// 解析 discriminator 节点
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
ArrayList<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
//添加idflags
flags.add(ResultFlag.ID);
}
//解析除了<constructor><discriminator>节点的其它节点
//把生成的resultMapping放入resultMappings
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
//构建resultMap对象
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}

上面就是resultMap节点的解析过程了

1:获取resultMap的各种属性

2:遍历子节点,执行不同的解析逻辑

3:构建resultMap对象

我们下面对2,3过程进行分析

2.5.1.解析<constructor>节点

通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object, DTO)以及绝大部分领域模型的要求。但有些情况下你想使用不可变类。一般来说,很少改变或基本不变的包含引用或数据的表,很适合使用不可变类。构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。constructor 元素就是为此而生的。

看看下面这个构造方法:

public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}

为了将结果注入构造方法,MyBatis 需要通过某种方式定位相应的构造方法。在下面的例子中,MyBatis 搜索一个声明了三个形参的的构造方法,参数类型以 java.lang.Integer, java.lang.String 和 int 的顺序给出。

<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
<arg column="age" javaType="_int"/>
</constructor>

当你在处理一个带有多个形参的构造方法时,很容易搞乱 arg 元素的顺序。从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。为了通过名称来引用构造方法参数,你可以添加 @Param 注解,或者使用 '-parameters' 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。下面是一个等价的例子,尽管函数签名中第二和第三个形参的顺序与 constructor 元素中参数声明的顺序不匹配。

<constructor>
<idArg column="id" javaType="int" name="id" />
<arg column="age" javaType="_int" name="age" />
<arg column="username" javaType="String" name="username" />
</constructor>

如果存在名称和类型相同的属性,那么可以省略 javaType 。

下面我们看源码解析过程

  private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
//获取子节点
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
ArrayList<ResultFlag> flags = new ArrayList<ResultFlag>();
//添加CONSTRUCTOR标志
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
//添加ID标志
flags.add(ResultFlag.ID);
}
//构建resultMapping对象(下面我们会分析到)
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}

上面就是constructor节点的执行过程,代码比较简单,discriminator的解析大家自行去看下

我们开始分析 id 和 property 节点的解析过程

2.5.2.解析<id>和<property>节点

XMLMapperBuilder

  private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, ArrayList<ResultFlag> flags) throws Exception {


//获取各种属性
String property = context.getStringAttribute("property");
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");

/*
解析 resultMap 属性,出现的情况是在<association> <collection> 节点中
但是如果不存在的话,则调用 processNestedResultMappings 方法解析嵌套 resultMap

*/

String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));

String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
String typeHandler = context.getStringAttribute("typeHandler");
String resulSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");

//解析javaTypeclass类型
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")

//解析TypeHandlerclass类型
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
//解析jdbc的枚举类型
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);

//构建resultMapping对象
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resulSet, foreignColumn);
}


下面我们继续分析resultMapping对象的构建过程

 public ResultMapping buildResultMapping(
Class<?> resultType,
String property,
String column,
Class<?> javaType,
JdbcType jdbcType,
String nestedSelect,
String nestedResultMap,
String notNullColumn,
String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler,
List<ResultFlag> flags,
String resultSet,
String foreignColumn) {
//构建resultMapping对象
ResultMapping resultMapping = assembleResultMapping(
resultType,
property,
column,
javaType,
jdbcType,
nestedSelect,
nestedResultMap,
notNullColumn,
columnPrefix,
typeHandler,
flags,
resultSet,
foreignColumn);
return resultMapping;
}

private ResultMapping assembleResultMapping(
Class<?> resultType,
String property,
String column,
Class<?> javaType,
JdbcType jdbcType,
String nestedSelect,
String nestedResultMap,
String notNullColumn,
String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler,
List<ResultFlag> flags,
String resultSet,
String foreignColumn) {

/*
如果javatype为空,则使用property属性进行解析
resultType: <resultMap type="xxx"/> 中的 type 属性
property:即 <result property="xxx"/> 中的 property 属性
*/

Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);

//解析typeHandler
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);

//解析column = {property1=column1, property2=column2} 的情况,会拆成多个resultMapping
List<ResultMapping> composites = parseCompositeColumnName(column);
if (composites.size() > 0) column = null;

//通过建造者模式构建resultMapping
ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property, column, javaTypeClass);
builder.jdbcType(jdbcType);
builder.nestedQueryId(applyCurrentNamespace(nestedSelect, true));
builder.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true));
builder.resultSet(resultSet);
builder.typeHandler(typeHandlerInstance);
builder.flags(flags == null ? new ArrayList<ResultFlag>() : flags);
builder.composites(composites);
builder.notNullColumns(parseMultipleColumnNames(notNullColumn));
builder.columnPrefix(columnPrefix);
builder.foreignColumn(foreignColumn);
return builder.build();
}

public ResultMapping build() {
// lock down collections
//flags集合变为不可变集合
resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
//composites集合变为不可变集合
resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);

//typeHandlerRegistry获取typeHandler
resolveTypeHandler();
//做一些检查工作
validate();
return resultMapping;
}

上面就是整个resultMapping的构建过程:

1:解析javatype

2.解析typeHandler

3.解析复合的column

4.通过建造者模式构建resultMapping

1:将flags集合和composites集合变为不可变集合

2.从typeHandlerRegistry获取typeHandler(类型处理器,关于类型处理器大家可以看上一篇文章)

到这里我们分析完了resultMapping对象的构建工作

2.5.3.创建resultMap对象

在上面我们经过一系列的分析,<id>,<result> 等节点最终都被解析成了 ResultMapping(与单条的结果进行映射),下面我们的工作就是构建resultMap

ResultMapResolver
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {

//idextend拼接命名空间
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);


ResultMap.Builder resultMapBuilder = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping);
if (extend != null) {
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
//获取resultMap
ResultMap resultMap = configuration.getResultMap(extend);

List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());

//为扩展的resultMap取出重复项
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.

boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
//resultMapping中是否包含CONSTRUCTOR标志
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}

//如果包含CONSTRUCTOR标志
if (declaresConstructor) {
Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();
while (extendedResultMappingsIter.hasNext()) {
if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
//删除扩展的resultMapping 集合中的包含 CONSTRUCTOR 标志的元素
extendedResultMappingsIter.remove();
}
}
}
//将扩展的resultMapping加入resultMappings
resultMappings.addAll(extendedResultMappings);
}
resultMapBuilder.discriminator(discriminator);
//构建resultMap
ResultMap resultMap = resultMapBuilder.build();
//resultMap放入configuration
configuration.addResultMap(resultMap);
return resultMap;
}

上面的代码主要是用来处理resultMap的extend属性,大家对着注释读,最后会构建我们的resultMap对象

ResultMap

 public ResultMap build() {
if (resultMap.id == null) {
throw new IllegalArgumentException("ResultMaps must have an id");
}

//创建一系列的集合
resultMap.mappedColumns = new HashSet<String>();
resultMap.idResultMappings = new ArrayList<ResultMapping>();
resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
resultMap.propertyResultMappings = new ArrayList<ResultMapping>();


for (ResultMapping resultMapping : resultMap.resultMappings) {

//检测 <association> <collection> 节点,是否包含 select resultMap 属性
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);

final String column = resultMapping.getColumn();
if (column != null) {
//column转换成大写加入mappedColumns集合中
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
} else if (resultMapping.isCompositeResult()) {
for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
final String compositeColumn = compositeResultMapping.getColumn();
if (compositeColumn != null) {
resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
}
}
}
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
//添加 resultMapping constructorResultMappings
resultMap.constructorResultMappings.add(resultMapping);
} else {
// 添加 resultMapping propertyResultMappings
resultMap.propertyResultMappings.add(resultMapping);
}
// 添加 resultMapping idResultMappings
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
resultMap.idResultMappings.add(resultMapping);
}
}
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
// lock down collections
// 变成一些不可变集合
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);

resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
return resultMap;
}
}


resultMap其实就是维护一系列的Collection,将 ResultMapping 实例及属性分别存储到不同的集合中

集合名称用途
mappedColumns用于存储 <id>、<result>、<idArg>、<arg> 节点 column 属性
idResultMappings用于存储 <id> 和 <idArg> 节点对应的 ResultMapping 对象
constructorResultMappings用于存储 <idArgs> 和 <arg> 节点对应的 ResultMapping 对象
propertyResultMappings用于存储 <id> 和 <result> 节点对应的 ResultMapping 对象

我们可以写一个测试类,来完成我们上面结构的输出,提供一个简单的思路

       Configuration configuration = new Configuration();
String resource = "mapper/xxxMapper.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
builder.parse();

ResultMap resultMap = configuration.getResultMap("articleResult");

resultMap.getMappedColumns()



到这里我们就分析完了整个resultMap节点的解析过程

2.6.解析<sql>节点

这个元素可以被用来定义可重用的 SQL 代码段,这些 SQL 代码可以被包含在其他语句中。它可以(在加载的时候)被静态地设置参数。在不同的包含语句中可以设置不同的值到参数占位符上。比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>


这个 SQL 片段可以被包含在其他语句中,例如:

<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>

下面我们来分析下sql节点的解析过程

  private void sqlElement(List<XNode> list) throws Exception {
if (configuration.getDatabaseId() != null) {
//调用sqlElement解析sql节点
sqlElement(list, configuration.getDatabaseId());
}
//调用sqlElement解析sql节点。第二个参数此时为空
sqlElement(list, null);
}


会两次调用sqlElement解析sql节点,其中一种是用来处理配置了databaseid的情况,另外一种是没有配置的情况

  private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
//获取 id databaseId 属性
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");

//id填充nameSpace id:currentNamespace + "." + id
id = builderAssistant.applyCurrentNamespace(id, false);

// databaseId requiredDatabaseId 是否一致
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId))
//缓存idXNodesqlFraments(sql片段中)
sqlFragments.put(id, context);
}
}

方法比较简单,对着注释看,databaseIdMatchesCurrent匹配规则大家自行去看下

2.7.解析<sql语句【<select>、<insert>、<update> <delete> 】>节点

 private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
//datbaseId不为空,调用buildStatementFromContext方法解析sql语句
buildStatementFromContext(list, configuration.getDatabaseId());
}
//database为空,调用buildStatementFromContext方法解析sql语句
buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//遍历Xnode
for (XNode context : list) {
//创建解析器
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//解析节点
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//解析异常,把解析器放入 configuration incompleteStatements 集合中
configuration.addIncompleteStatement(statementParser);
}
}
}

上面方法比较简单,我们继续分析向下分析

XMLStatementBuilder

 public void parseStatementNode() {

//获取id
String id = context.getStringAttribute("id");
//获取database
String databaseId = context.getStringAttribute("databaseId");

//检查databaseid
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;

//获取一系列的属性
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

//解析resultTypeclass类型
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");

// 解析 Statement 类型,默认为 PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));

//解析ressultSetType
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

//获取节点的名称(selectupdatedeleteinsert)
String nodeName = context.getNode().getNodeName();

//根据节点的大写名称得到sqlCommendType
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

//是不是select类型
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

//获取缓存相关
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// Include Fragments before parsing
//解析include节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

// Parse selectKey after includes,
// in case if IncompleteElementException (issue #291)
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
//解析selectKey
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//解析sql语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");

KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
//获取keyGenerator
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}

//构建mappeerStament,并将结果存储大到mappedStament
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

上面是整个sql语句的解析流程,我们总结下

1:获取一系列的属性

2.解析<include>节点

3.解析<selectKey>

4.解析sql语句

5.获取keyGenerator

6.构建mappedStament,并放入configuration(基本上我们所有解析的内容都会放入configuration中,大家可以自行去看下Configuration的数据结构)

下面我们会对上面设计到的点进行分析

2.7.1.解析<include>节点

XMLIncludeTransformer

 public void applyIncludes(Node source) {
//节点名字是include
if (source.getNodeName().equals("include")) {
// 找到sql片段
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"));
/*
递归调用,用于将 <sql> 节点内容中出现的属性占位符 ${} 替换为对应的属性值
*/

applyIncludes(toInclude);

/*
如果<sql><include>不在一个文档中,则从
其它文档中把 <sql> 节点引入到 <include> 所在文档中
*/

if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
//<include>节点替换为<sql>节点
source.getParentNode().replaceChild(toInclude, source);

while (toInclude.hasChildNodes()) {
// <sql> 中的内容插入到 <sql> 节点之前
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
//我们前面已经把 <sql> 节点的内容插入到 dom 中了,
//现在不需要 <sql> 节点了,这里将该节点从 dom 中移除
toInclude.getParentNode().removeChild(toInclude);

}

else if (source.getNodeType() == Node.ELEMENT_NODE) {
//获取childNode
NodeList children = source.getChildNodes();
for (int i=0; i<children.getLength(); i++) {
//递归调用
applyIncludes(children.item(i));
}
}
}

我们简单总结下:

1:获取<include>节点中的sql片段

2:进行递归调用,处理属性占位符

3.如果sql节点和include节点不在一个文档中,则把sql节点移动到include节点所在的文档

4.把include节点替换为sql节点,并把sql中的内容插入到sql节点之前,【include节点就是我们的sql节点】

5.在当前的dom文档中移除我们的sql节点(sql已经已经被加载了)

2.7.2.解析<selectKey>节点
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

在上面的示例中,selectKey 元素中的语句将会首先运行,Author 的 id 会被设置,然后插入语句会被调用。这可以提供给你一个与数据库中自动生成主键类似的行为,同时保持了 Java 代码的简洁。

selectKey 元素描述如下:

属性描述
keyPropertyselectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
resultType结果的类型。MyBatis 通常可以推断出来,但是为了更加精确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的 Object 或一个 Map。
order这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先生成主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用
statementType与前面相同,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。

下面我们来看相关代码

    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
//databaseid存在时处理SelctKeys
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
//不存在时处理selectKey
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);


XMLStatementBuilder

  public void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
for (XNode nodeToHandle : list) {
// id = parentId + !selectKey,比如 selectMyKey!selectKey
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
//获取databaseid
String databaseId = nodeToHandle.getStringAttribute("databaseId");
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
//解析<selectKey>
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}

public void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {

//获取各种属性
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

//defaults
//一些默认值
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = new NoKeyGenerator();
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;

//创建sqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);

//类型为查询类型,因为selectKey节点中只适用select操作
SqlCommandType sqlCommandType = SqlCommandType.SELECT;

//构建mappedStament,将 MappedStatement 加到 Configuration mappedStatements
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, null, databaseId, langDriver, null);

// id = namespace + "." + id
id = builderAssistant.applyCurrentNamespace(id, false);

MappedStatement keyStatement = configuration.getMappedStatement(id, false);
//创建KeyGenerator并添加到configuration
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));

//selectKey删除
nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
}

下面我们简单总结下:

1:获取各种属性,

2:创建sqlSource

3:创建mappedStament,并加入到configuration中

4:创建缓存KeyGenerator

1.4.比较简单,不用分析了,2和3,我们最开始的sql语句分析也会涉及到,下面我们开始分析

2.7.3.创建sqlSource

上面我们已经把sql文件中的include,selectKey,sql节点都分析到了,而且当这些节点解析后,都会存原有文档消失,下面我们继续看sql语句的解析过程

  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script);
//解析sql语句
return builder.parseScriptNode();
}

public SqlSource parseScriptNode() {
//解析sql语句节点
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
//创建sqlSouece
SqlSource sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
return sqlSource;
}


上面代码比较简单,我们继续看

解析sql语句节点

private List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
//获取子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
String nodeName = child.getNode().getNodeName();
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
||
child.getNode().getNodeType() == Node.TEXT_NODE) {
//获取文本内容
String data = child.getStringBody("");

contents.add(new TextSqlNode(data));
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE && !"selectKey".equals(nodeName)) { // issue #628
//根据nodeName获取handler
NodeHandler handler = nodeHandlers.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//处理child节点,生成相应的 SqlNode
handler.handleNode(child, contents);
}
}
return contents;
}

我们继续分析handleNode,这个方法主要用来处理动态sql

  private class WhereHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//解析<where>节点
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
//创建whereSqlNode
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}

2.7.4.创建MappedStatement

上面我们分析了一系列的属性,而这些属性最终是被存储在MappedStatement中的,下面我们来看它的相关代码

 public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {

if (unresolvedCacheRef) throw new IncompleteElementException("Cache-ref not yet resolved");

id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

//build构建器,设置各种属性
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
statementBuilder.resource(resource);
statementBuilder.fetchSize(fetchSize);
statementBuilder.statementType(statementType);
statementBuilder.keyGenerator(keyGenerator);
statementBuilder.keyProperty(keyProperty);
statementBuilder.keyColumn(keyColumn);
statementBuilder.databaseId(databaseId);
statementBuilder.lang(lang);
statementBuilder.resultOrdered(resultOrdered);
statementBuilder.resulSets(resultSets);
setStatementTimeout(timeout, statementBuilder);

setStatementParameterMap(parameterMap, parameterType, statementBuilder);
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

//构建MappedStatement
MappedStatement statement = statementBuilder.build();
//添加到configuration
configuration.addMappedStatement(statement);
return statement;
}


代码比较简单,就不单独分析了

2.8.总结

上面的内容比较多,我们再来回顾下,我们映射文件的解析步骤

 public void parse() {
//映射文件是否被解析过
if (!configuration.isResourceLoaded(resource)) {
//解析mapper节点
configurationElement(parser.evalNode("/mapper"));
//添加资源的路径到:已解析的资源集合中,为了避免再次解析
configuration.addLoadedResource(resource);
//绑定mapper接口通过命名空间
bindMapperForNamespace();
}
//处理一些未完成的解析
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}

上面就是映射文件的解析流程,分为3步

1.解析mapper节点

2:绑定mapper接口通过命名空间

3:处理一些未完成的解析流程

上面说了那么多,我们才分析完第1步骤,解析mapper节点

2.9.绑定mapper接口

XMLMapperBuilder

 private void bindMapperForNamespace() {
//获取namespace
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//根据命名空间获取到对应的class类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
//是否被解析过
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
//绑定mapper
configuration.addMapper(boundType);
}
}
}
}

public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}


public <T> void addMapper(Class<T> type) {
//是否注册过
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
/*
MapperProxyFactory:可以为mapper接口生成代理类
typemapper代理工厂绑定
*/

knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
//注解解析器(mybatis支持xml和注解两种方式)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
//解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

上面就是整个绑定mapper接口的过程

1:首先获取nameSpace

2.根据nameSpace解析出对象的class类型

3:将2得到的结果和mapperProxyFactory绑定

4.生成注解解析器,解析注解

2.10.一些未完成的解析流程

不知道大家有没有注意到,我们在分析其它节点的解析工作时,在解析最后,如果发生异常,导致解析进行不下去了,mybatis此时是捕获这个异常(IncompleteElementException),然后在把相应的解析器放入到 incomplet* 集合中,对于这些未完成的解析节点,mybatis是怎么解析的呢,下面我们看相关代码

public void parse() {


// 处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

从上面的源码中可以知道有三种节点在解析过程中可能会出现异常,造成不能解析的情况,我们只分析其中的一种

  private void parsePendingResultMaps() {
//获取未完成的解析集合
Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
synchronized (incompleteResultMaps) {

Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
//遍历
while (iter.hasNext()) {
try {
//再次尝试解析,
iter.next().resolve();
//解析是成功的
iter.remove();
} catch (IncompleteElementException e) {
// ResultMap is still missing a resource...
//这里解析失败了,但是并放入到configuration相关的集合中

}
}
}
}

代码比较简单,大家对着相关注释看

3.总结

到这里我们就分析完了整个映射文件的解析过程,篇幅比较大,在分析过程中,因为能力问题,会有一些分析不到,可能还会存在一些错误,希望大家指出来,最后谢谢大家

参考

mybatis官方文档

https://mybatis.org/mybatis-3/zh/index.html


文章转载自易林的博客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论