映射文件包含多种二级节点,比如 < cache>,< resultMap>,< sql>以及 <select|insert|update|delete> 等。除此之外,还包含了一些三级节点,比如 < include>,< if>, < where> 等。这些节点的解析过程将会在接下来的内容中陆续进行分析。在分析之前,我们 先来看一个映射文件配置示例。
<mapper namespace="xyz.coolblog.dao.AuthorDao">
<cache/>
<resultMap id="authorResult" type="Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- ... -->
</resultMap>
<sql id="table">
author
</sql>
<select id="findOne" resultMap="authorResult">
SELECT
id, name, age, sex, email
FROM
<include refid="table"/>
WHERE
id = #{id}
</select>
<!-- <insert|update|delete/> -->
</mapper>
上面是一个比较简单的映射文件,还有一些的节点未出现在上面。以上配置中每种节点 的解析逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder 类的 configurationElement 方法统一调用。该方法的逻辑如下:
private void configurationElement(XNode context) {
try {
// 获取 mapper 命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || 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>、...、<delete> 等节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
解析< cache>节点
MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。 二级缓存配置在映射文件中,使用者需要显示配置才能开启。如果无特殊要求,二级缓存的 配置很简单。如下:
<cache/>
如果我们想修改缓存的一些属性,可以像下面这样配置。
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
根据上面的配置创建出的缓存有以下特点:
- 按先进先出的策略淘汰缓存项
- 缓存的容量为 512 个对象引用
- 缓存每隔 60 秒刷新一次
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象
下面我们来分析一下缓存配置的解析逻辑,如下:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
// 获取各种属性
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
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);
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取子节点配置
Properties props = context.getChildrenAsProperties();
// 构建缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
上面代码中,大段代码用来解析< cache>节点的属性和子节点, 缓存对象的构建逻辑封装在 BuilderAssistant 类的 useNewCache 方法中,下面我们来看一下 该方法的逻辑。
// -☆- MapperBuilderAssistant
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 使用建造模式构建缓存实例
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加缓存到 Configuration 对象中
configuration.addCache(cache);
// 设置 currentCache 遍历,即当前使用的缓存
currentCache = cache;
return cache;
}
上面使用了建造模式构建 Cache 实例,Cache 实例构建过程略为复杂,我们跟下去看看。
// -☆- CacheBuilder
public Cache build() {
// 设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
setDefaultImplementations();
// 通过反射创建缓存
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 仅对内置缓存 PerpetualCache 应用装饰器
if (PerpetualCache.class.equals(cache.getClass())) {
// 遍历装饰器集合,应用装饰器
for (Class<? extends Cache> decorator : decorators) {
// 通过反射创建装饰器实例
cache = newCacheDecoratorInstance(decorator, cache);
// 设置属性值到缓存实例中
setCacheProperties(cache);
}
// 应用标准的装饰器,比如 LoggingCache、SynchronizedCache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 应用具有日志功能的缓存装饰器
cache = new LoggingCache(cache);
}
return cache;
}
上面的构建过程流程较为复杂,这里总结一下。如下:
- 设置默认的缓存类型及装饰器
- 应用装饰器到 PerpetualCache 对象上
- 应用标准装饰器
- 对非 LoggingCache 类型的缓存应用 LoggingCache 装饰器
在以上 4 个步骤中,最后一步的逻辑很简单,无需多说。下面按顺序分析前 3 个步骤对 应的逻辑,如下:
private void setDefaultImplementations() {
if (implementation == null) {
// 设置默认的缓存实现类
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
// 添加 LruCache 装饰器
decorators.add(LruCache.class);
}
}
}
解析< cache-ref>节点
在 MyBatis 中,二级缓存是可以共用的。这需要通过< cache-ref>节点为命名空间配置参 照缓存,比如像下面这样。
<!-- Mapper1.xml -->
<mapper namespace="xyz.coolblog.dao.Mapper1">
<!-- Mapper1 与 Mapper2 共用一个二级缓存 -->
<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
</mapper>
<!-- Mapper2.xml -->
<mapper namespace="xyz.coolblog.dao.Mapper2">
<cache/>
</mapper>
对照上面的配置分析 cache-ref 的解析过程。
private void cacheRefElement(XNode context) {
if (context != null) {
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);
}
}
}
上所示,< cache-ref>节点的解析逻辑封装在了 CacheRefResolver 的 resolveCacheRef 方 法中,我们一起看一下这个方法的逻辑。
// -☆- MapperBuilderAssistant
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 根据命名空间从全局配置对象(Configuration)中查找相应的缓存实例
Cache cache = configuration.getCache(namespace);
/* * * * * *
若未查找到缓存实例,此处抛出异常。这里存在两种情况导致未查找到 cache实例,分别如下:
1.使用者在 <cache-ref> 中配置了一个不存在的命名空间, 导致无法找到 cache 实例
2.使用者所引用的缓存实例还未创建
*/
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 设置 cache 为当前使用缓存
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
解析< resultMap>节点
resultMap 元素是 MyBatis 中最重要最强大的元素,它可以把大家从 JDBC ResultSets 数 据提取的工作中解放出来。通过 resultMap 和自动映射,可以让 MyBatis 帮助我们完成 ResultSet → Object 的映射,这将会大大提高了开发效率。
下面开始分析 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, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取 id 和 type 属性
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 获取 extends 和 autoMapping
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 解析 type 属性对应的类型
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 {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
// 添加 ID 到 flags 集合中
flags.add(ResultFlag.ID);
}
// 解析 id 和 property 节点,并生成相应的 ResultMapping
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) {
/*
* 如果发生 IncompleteElementException 异常,
* 这里将 resultMapResolver 添加到 incompleteResultMaps 集合中 */
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
上面的代码比较多,看起来有点复杂,这里总结一下:
- 获取< resultMap>节点的各种属性
- 遍历< resultMap>的子节点,并根据子节点名称执行相应的解析逻辑
- 构建 ResultMap 对象
- 若构建过程中发生异常,则将 resultMapResolver 添加到incompleteResultMaps 集合中
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
// 根据节点类型获取 name 或 property 属性
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");
}
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
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 resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
// 解析 javaType、typeHandler 的类型以及枚举类型 JdbcType
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
下面分析 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,
boolean lazy) {
// 若 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} 的情况,
// 这里会将 column 拆分成多个 ResultMapping
List<ResultMapping> composites = parseCompositeColumnName(column);
// 通过建造模式构建 ResultMapping
return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
.jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
.resultSet(resultSet)
.typeHandler(typeHandlerInstance)
.flags(flags == null ? new ArrayList<ResultFlag>() : flags)
.composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix)
.foreignColumn(foreignColumn)
.lazy(lazy)
.build();
}
// -☆- ResultMapping.Builder
public ResultMapping build() {
// 将 flags 和 composites 两个集合变为不可修改集合
resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
// 从 TypeHandlerRegistry 中获取相应 TypeHandler
resolveTypeHandler();
validate();
return resultMapping;
}
解析< sql>节点
< sql>节点用来定义一些可重用的 SQL 语句片段,比如表名,或表的列名等。在映射文 件中,我们可以通过< include>节点引用< sql>节点定义的内容。下面我来演示一下< sql>节点 的使用方式,如下:
<sql id="table">
<select id="findOne" resultType="Article">
SELECT id, title FROM <include refid="table"/> WHERE id = #{id}
</select>
<update id="update" parameterType="Article">
UPDATE <include refid="table"/> SET title = #{title} WHERE id = #{id}
</update>
</sql>
往下分析
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
// 获取 id 和 databaseId 属性
String databaseId = context.getStringAttribute("databaseId");
// id = currentNamespace + "." + id
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
// 检测当前 databaseId 和 requiredDatabaseId 是否一致
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// 将 <id, XNode> 键值对缓存到 sqlFragments 中
sqlFragments.put(id, context);
}
}
}
这个方法逻辑比较简单,首先是获取< sql>节点的 id 和 databaseId 属性,然后为 id 属性 值拼接命名空间。最后,通过检测当前 databaseId 和 requiredDatabaseId 是否一致,来决定保 存还是忽略当前的节点。下面,我们来看一下 databaseId 的匹配逻辑是怎样的。
解析 SQL 语句节点
前面分析了< cache>、< cache-ref>、< resultMap>以及< sql>节点,从这一节开始,我们来 分析映射文件中剩余的几个节点,分别是< select>、< insert>、< update>以及< delete>等。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("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<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
// 解析Statement 类型,默认PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//解析 ResultSetType
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 获取节点的名称,比如 <select> 节点名称为 select
String nodeName = context.getNode().getNodeName();
// 根据节点名称解析 SqlCommandType
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 解析 <include> 节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析 <selectKey> 节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析 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);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 构建 MappedStatement 对象,并将该对象存储到
// Configuration 的 mappedStatements 集合中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
以 上代码做的事情如下。
- 解析< include>节点
- 解析< selectKey>节点
- 解析 SQL,获取 SqlSource
- 构建 MappedStatement 实例