映射檔案包含多種二級節點,比如 < 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 級别的,預設為開啟狀态。 二級緩存配置在映射檔案中,使用者需要顯示配置才能開啟。如果無特殊要求,二級緩存的 配置很簡單。如下:
如果我們想修改緩存的一些屬性,可以像下面這樣配置。
根據上面的配置建立出的緩存有以下特點:
- 按先進先出的政策淘汰緩存項
- 緩存的容量為 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 執行個體