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 {
//分别获取resourc,url,class属性
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)(依赖驱动)。 |
| statementType | STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
| resultSetType | FORWARD_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)(依赖驱动)。 |
| statementType | STATEMENT,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/>
基本上就是这样。这个简单语句的效果如下:
映射语句文件中的所有 select 语句的结果将会被缓存。
映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
缓存不会定时进行刷新(也就是说,没有刷新间隔)。
缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
缓存只作用于 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);
}
//标准的装饰器,如(LoggingCache、SynchronizedCache)
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);
/*
为空,报异常,出现的有两种原因:
1:namespace是一个根本不存在的命名空间
2:namespace存在,但是缓存示例还未创建
*/
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())) {
//添加id到flags中
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");
//解析javaType的class类型
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
//解析TypeHandler的class类型
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) {
//为id和extend拼接命名空间
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))
//缓存id,XNode在sqlFraments(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);
//解析resultType的class类型
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);
//获取节点的名称(如select,update,delete,insert)
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 元素描述如下:
| 属性 | 描述 |
|---|---|
| keyProperty | selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 |
| 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接口生成代理类
将type和mapper代理工厂绑定
*/
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




