![dd3e9863d9bdcffaac4bd0ff91287bb2.png](https://img-blog.csdnimg.cn/img_convert/dd3e9863d9bdcffaac4bd0ff91287bb2.png)
上一篇我们分析了配置文件的加载与解析过程,本文将继续对映射文件的加载与解析实现进行分析。MyBatis 的映射文件用于配置 SQL 语句、二级缓存,以及结果集映射等,是区别于其它 ORM 框架的主要特色之一。
在上一篇分析配置文件 <mappers/>
标签的解析实现时,了解到 MyBatis 最终通过调用 XMLMapperBuilder#parse
方法实现对映射文件的解析操作,本文我们将以此方法作为入口,探究 MyBatis 加载和解析映射文件的实现机制。
方法 XMLMapperBuilder#parse
的实现如下:
public void parse() {
/* 1. 加载并解析映射文件 */
if (!configuration.isResourceLoaded(resource)) {
// 加载并解析 <mapper/> 标签下的配置
this.configurationElement(parser.evalNode("/mapper"));
// 标记该映射文件已被解析
configuration.addLoadedResource(resource);
// 注册当前映射文件关联的 Mapper 接口(标签 <mapper namespace=""/> 对应的 namespace 属性)
this.bindMapperForNamespace();
}
/* 2. 处理解析过程中失败的标签 */
// 处理解析失败的 <resultMap/> 标签
this.parsePendingResultMaps();
// 处理解析失败的 <cache-ref/> 标签
this.parsePendingCacheRefs();
// 处理解析失败的 SQL 语句标签
this.parsePendingStatements();
}
MyBatis 在解析映射文件时首先会判断该映射文件是否被解析过,对于没有被解析过的文件则会调用 XMLMapperBuilder#configurationElement
方法解析所有配置,并注册当前映射文件关联的 Mapper 接口。对于解析过程中处理异常的标签,MyBatis 会将其记录到 Configuration 对象对应的属性中,并在方法最后再次尝试二次解析。
整个 XMLMapperBuilder#configurationElement
方法实现了对映射文件解析的核心步骤,与配置文件解析的实现方式一样,这也是一个调度方法,实现如下:
private void configurationElement(XNode context) {
try {
// 获取 <mapper/> 标签的 namespace 属性,设置当前映射文件关联的 Mapper 接口
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref/> 子标签,多个 mapper 可以共享同一个二级缓存
this.cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache/> 子标签
this.cacheElement(context.evalNode("cache"));
// 解析 <parameterMap/> 子标签,已废弃
this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap/> 子标签,建立结果集与对象属性之间的映射关系
this.resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql/> 子标签
this.sqlElement(context.evalNodes("/mapper/sql"));
// 解析 <select/>、<insert/>、<update/> 和 <delete/> 子标签
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
每个映射文件都关联一个具体的 Mapper 接口,而 <mapper/>
节点的 namespace 属性则用于指定对应的 Mapper 接口限定名。上述方法首先会获取 namespace 属性,然后调用相应方法对每个子标签进行解析,下面逐一展开分析。
加载与解析映射文件
下面对各个子标签的解析过程逐一展开分析,考虑到 <parameterMap/>
子标签已废弃,所以不再对其多作介绍。
解析 cache 标签
MyBatis 在设计上分为一级缓存和二级缓存(关于缓存机制将会在下一篇分析 SQL 语句执行过程时进行介绍,这里只要知道有这样两个概念即可),该标签用于对二级缓存进行配置。在具体分析 <cache/>
标签之前,我们需要对 MyBatis 的缓存类设计有一个了解,不然可能会云里雾里。MyBatis 的缓存类设计还是非常巧妙的,不管是一级缓存还是二级缓存,都实现自同一个 Cache 接口:
public interface Cache {
/** 缓存对象 ID */
String getId();
/** 添加数据到缓存,一般来说 key 是 {@link CacheKey} 类型 */
void putObject(Object key, Object value);
/** 从缓存中获取 key 对应的 value */
Object getObject(Object key);
/** 从缓存中移除指定对象 */
Object removeObject(Object key);
/** 清空缓存 */
void clear();
/**
* 获取缓存对象的个数(不是缓存的容量),
* 该方法不会在 MyBatis 核心代码中被调用,可以是一个空实现
*/
int getSize();
/**
* 缓存读写锁,
* 该方法不会在 MyBatis 核心代码中被调用,可以是一个空实现
*/
ReadWriteLock getReadWriteLock();
}
Cache 接口中声明的缓存操作方法中规中矩。围绕该接口,MyBatis 实现了基于 HashMap 数据结构的基本实现 PerpetualCache 类,该实现类的各项方法实现都是对 HashMap API 的封装,比较简单。在整个缓存类设计方面,MyBatis 使用了典型的装饰模式为缓存对象增加不同的特性,下表对这些装饰器进行了简单介绍。
![16601491902aad366d6a7d4a4a100a98.png](https://img-blog.csdnimg.cn/img_convert/16601491902aad366d6a7d4a4a100a98.png)
介绍完了缓存类的基本设计,我们再回过头来继续分析<cache/>
标签的解析过程,由XMLMapperBuilder#cacheElement
方法实现:
private void cacheElement(XNode context) {
if (context != null) {
// 获取相应的是属性配置
String type = context.getStringAttribute("type", "PERPETUAL"); // 缓存实现类型,可以指定自定义实现
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU"); // 缓存清除策略,默认是 LRU,还可以是 FIFO、SOFT,以及 WEAK
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval"); // 刷新间隔,单位:毫秒
Integer size = context.getIntAttribute("size"); // 缓存大小,默认为 1024
boolean readWrite = !context.getBooleanAttribute("readOnly", false); // 是否只读
boolean blocking = context.getBooleanAttribute("blocking", false); // 是否阻塞
Properties props = context.getChildrenAsProperties();
// 创建二级缓存,并填充 Configuration 对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
上述方法首先会获取<cache/>
标签的相关属性配置,然后调用MapperBuilderAssistant#useNewCache
方法创建缓存对象,并记录到 Configuration 对象中。方法MapperBuilderAssistant#useNewCache
中使用了缓存对象构造器 CacheBuilder 创建缓存对象,一起来看一下CacheBuilder#build
方法实现:
public Cache build() {
// 如果没有指定自定义缓存实现类,则设置缓存默认实现(以 PerpetualCache 作为默认实现,以 LruCache 作为默认装饰器)
this.setDefaultImplementations();
// 反射创建缓存对象
Cache cache = this.newBaseCacheInstance(implementation, id);
// 初始化缓存对象
this.setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
// 如果缓存采用 PerpetualCache 实现,则遍历使用注册的装饰器进行装饰
if (PerpetualCache.class.equals(cache.getClass())) {
// 遍历装饰器集合,基于反射方式装饰缓存对象
for (Class<? extends Cache> decorator : decorators) {
cache = this.newCacheDecoratorInstance(decorator, cache);
this.setCacheProperties(cache);
}
// 采用标准装饰器进行装饰
cache = this.setStandardDecorators(cache);
}
// 采用日志缓存装饰器对缓存对象进行装饰
else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
构造缓存对象时首先会判断是否指定了自定义的缓存实现类,否则使用默认的缓存实现(即以 PerpetualCache 作为默认实现,以 LruCache 作为默认缓存装饰器);然后选择 String 类型参数的构造方法构造缓存对象,并基于配置对缓存对象进行初始化;最后依据缓存实现采用相应的装饰器予以装饰。
方法 CacheBuilder#setCacheProperties
除了用于设置相应属性配置外,还会判断缓存类是否实现了 InitializingObject 接口,以决定是否调用 InitializingObject#initialize
初始化方法。
解析 cache-ref 标签
标签 <cache/>
默认的作用域限定在标签所在的 namespace 范围内,如果希望能够让一个缓存对象在多个 namespace 之间共享,可以定义 <cache-ref/>
标签以引用其它命名空间中定义的缓存对象。标签 <cache-ref/>
的解析位于 XMLMapperBuilder#cacheRefElement
方法中:
private void cacheRefElement(XNode context) {
if (context != null) {
// 记录 <当前节点所在的 namespace, 引用缓存对象所在的 namespace> 映射关系到 Configuration 中
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// 构造缓存引用解析器 CacheRefResolver 对象
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 从记录缓存对象的 Configuration#caches 集合中获取引用的缓存对象
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
// 如果解析出现异常则记录到 Configuration#incompleteCacheRefs 中,稍后再处理
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
方法首先会在 Configuration#cacheRefMap
属性中记录一下当前的引用关系,其中 key 是 <cache-ref/>
所在的 namespace,value 则是引用的缓存对象所在的 namespace。然后从 Configuration#caches
属性中获取引用的缓存对象,在分析 <cache/>
标签时,我们曾提及到最终解析构造的缓存对象会记录到 Configuration#caches
属性中,这里则是一个逆过程。
解析 resultMap 标签
标签 <resultMap/>
用于配置结果集映射,建立结果集与实体类对象属性之间的映射关系。这是一个非常有用且提升开发效率的配置,如果是纯 JDBC 开发,在处理结果集与实体类对象之间的映射时还需要手动硬编码注入。对于一张字段较多的表来说,简直写到手抽筋,而 <resultMap/>
标签配置配合 mybatis-generator 工具的逆向工程可以解放我们的双手。下面是一个典型的配置,用于建立数据表 t_user 与 User 实体类之间的属性映射关系:
<resultMap id="BaseResultMap" type="org.zhenchao.mybatis.entity.User">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="username" jdbcType="VARCHAR" property="username"/>
<result column="password" jdbcType="VARCHAR" property="password"/>
<result column="age" jdbcType="INTEGER" property="age"/>
<result column="phone" jdbcType="VARCHAR" property="phone"/>
<result column="email" jdbcType="VARCHAR" property="email"/>
</resultMap>
在开始介绍 <resultMap/>
标签的解析过程之前,我们需要对该标签涉及到的两个主要的类 ResultMapping 和 ResultMap 有一个了解。前者用于封装除 <discriminator/>
标签以外的其它子标签配置(该标签具备自己的封装类),后者