上一篇我们分析了配置文件的加载与解析过程,本文将继续对映射文件的加载与解析实现进行分析。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 使用了典型的装饰模式为缓存对象增加不同的特性,下表对这些装饰器进行了简单介绍。
介绍完了缓存类的基本设计,我们再回过头来继续分析 <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/>
标签以外的其它子标签配置(该标签具备自己的封装类),后者则用于封装整个 <resultMap/>
标签。
ResultMapping
public class ResultMapping {
private Configuration configuration;
/** 对应标签的 property 属性 */
private String property;
/** 对应标签的 column 属,配置数据表列名(or 别名) */
private String column;
/** 对应 java 类型,配置类型全限定名(or 别名) */
private Class<?> javaType;
/** 对应列的 JDBC 类型 */
private JdbcType jdbcType;
/** 类型处理器,会覆盖默认类型处理器 */
private TypeHandler<?> typeHandler;
/** 对应标签的 resultMap 属性,以 id 的方式引某个已定义的 <resultMap/> */
private String nestedResultMapId;
/** 对应标签的 select 属性,以 id 的方式引用某个已定义的 <select/> */
private String nestedQueryId;
/** 对标签的 notNullColumns 属性 */
private Set<String> notNullColumns;
/** 对应标签的 columnPrefix 属性 */
private String columnPrefix;
/** 记录处理后的标志 */
private List<ResultFlag> flags;
/** 记录标签 column 拆分后生成的结果 */
private List<ResultMapping> composites;
/** 对应标签 resultSet 属性 */
private String resultSet;
/** 对应标签 foreignColumn 属性 */
private String foreignColumn;
/** 对应标签 fetchType 属性,配置是否延迟加载 */
private boolean lazy;
// ... 省略构造器类定义,以及 getter 和 setter 方法
}
ResultMapping 类中定义的属性如上述代码注释。此外,还内置了一个 Builder 内部构造器类,用于封装数据构造 ResultMapping 对象,并实现了对属性值的基本校验逻辑。
ResultMap
public class ResultMap {
private Configuration configuration;
/** 对应标签的 id 属性 */
private String id;
/** 对应标签的 type 属性 */
private Class<?> type;
/** 记录除 <discriminator/> 标签以外的其它映射关系 */
private List<ResultMapping> resultMappings;
/** 记录带有 id 属性的映射关系 */
private List<ResultMapping> idResultMappings;
/** 记录带有 constructor 属性的映射关系 */
private List<ResultMapping> constructorResultMappings;
/** 记录带有 property 属性的映射关系 */
private List<ResultMapping> propertyResultMappings;
/** 记录配置中所有的 column 属性集合 */
private Set<String> mappedColumns;
/** 记录配置中所有的 property 属性集合 */
private Set<String> mappedProperties;
/** 封装 <discriminator/> 标签 */
private Discriminator discriminator;
/** 是否包含嵌套的结果映射 */
private boolean hasNestedResultMaps;
/** 是否包含嵌套查询 */
private boolean hasNestedQueries;
/** 是否开启自动映射 */
private Boolean autoMapping;
// ... 省略构造器类,以及 getter 和 setter 方法
}
ResultMap 类中定义的属性如上述代码注释。与 ResultMapping 一样,也是通过内置 Builder 内部构造器类来构造 ResultMap 对象,构造器的实现比较简单,读者可以参考源码实现。
了解了内部数据结构 ResultMapping 和 ResultMap 的定义,以及二者之间的相互依赖关系,接下来开始分析 <resultMap/>
标签的解析过程,实现位于 XMLMapperBuilder#resultMapElements
方法中:
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取 type 属性,支持 type、ofType、resultType,以及 javaType 类型配置
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 基于 TypeAliasRegistry 解析 type 属性对应的实体类型
Class<?> typeClass = this.resolveClass(type);
if (typeClass == null) {
// 尝试基于 <association/> 子标签或 <case/> 子标签解析实体类型
typeClass = this.inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
// 用于记录解析结果
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
// 获取并遍历处理所有的子标签
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
// 解析 <constructor/> 子标签,封装成为 ResultMapping 对象
if ("constructor".equals(resultChild.getName())) {
this.processConstructorElement(resultChild, typeClass, resultMappings);
}
// 解析 <discriminator/> 子标签,封装成为 Discriminator 对象
else if ("discriminator".equals(resultChild.getName())) {
discriminator = this.processDiscriminatorElement(resultChild, typeClass, resultMappings);
}
// 解析 <association/>、<collection/>、<id/> 和 <result/> 子标签,封装成为 ResultMapping 对象
else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// 创建 ResultMapping 对象,并记录到 resultMappings 集合中
resultMappings.add(this.buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 获取 id 属性(标识当前 <resultMap/> 标签),如果没有指定则基于规则生成一个
String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
// 获取 extends 属性,用于指定继承关系
String extend = resultMapNode.getStringAttribute("extends");
// 获取 autoMapping 属性,是否启用自动映射(自动查找与列名相同的属性名称,并执行注入)
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(
builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 基于解析得到的配置构造 ResultMap 对象,记录到 Configuration#resultMaps 中
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
// 记录解析异常的 <resultMap/> 标签,后续尝试二次解析
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
标签 <resultMap/>
包含 4 个属性配置,即 id、type、extends 和 autoMapping。
id:标识当前 <resultMap/>
标签,如果没有指定则会调用 XNode#getValueBasedIdentifier
方法基于规则自动生成一个,用于提升 MyBatis 的执行性能。
type:设置当前标签所关联的实体类对象,支持 type、ofType、resultType,以及 javaType 等配置方式,以尽可能用简单的配置支持更多的实体类型。
extends:指定当前标签的继承关系。
autoMapping:一个 boolean 类型的配置项,如果为 true 则表示开启自动映射功能,MyBatis 会自动查找实例类对象中与结果集列名相同的属性名,并调用 setter 方法执行注入。标签 <resultMap/>
中明确指定的映射关系优先级要高于自动映射。
标签 <resultMap/>
包含 <constructor/>
、<id/>
、<result/>
、<association/>
、<collection/>
,以及 <discriminator/>
六个子标签。关于这些子标签的作用可以参阅 官方文档,除 <discriminator/>
以外,其余五个标签的解析实现大同小异,下面以 <constructor/>
标签为例对解析实现展开分析。
子标签 <constructor/>
的解析由 XMLMapperBuilder#processConstructorElement
方法实现,如下:
private void processConstructorElement(
XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
// 获取并处理 <constructor/> 标签中配置的子标签列表
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<>();
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
flags.add(ResultFlag.ID); // 添加 ID 标识
}
// 封装标签配置为 ResultMapping 对象,记录到 resultMappings 集合中
resultMappings.add(this.buildResultMappingFromContext(argChild, resultType, flags));
}
}
子标签 <constructor/>
用于指定实体类的构造方法以实现在构造实体类对象时注入结果值。上述方法直接遍历处理该标签的所有子标签,即 <idArg/>
和 <arg/>
,并调用 XMLMapperBuilder#buildResultMappingFromContext
方法创建对应的 ResultMapping 对象,实现如下:
private ResultMapping buildResultMappingFromContext(
XNode context, Class