1. 程式人生 > 實用技巧 >mybatis原始碼配置檔案解析之五:解析mappers標籤(解析XML對映檔案)

mybatis原始碼配置檔案解析之五:解析mappers標籤(解析XML對映檔案)

在上篇文章中分析了mybatis解析<mappers>標籤,《mybatis原始碼配置檔案解析之五:解析mappers標籤》重點分析瞭如何解析<mappers>標籤中的<package>子標籤的過程。mybatis解析<mappers>標籤主要完成了兩個操作,第一個是把對應的介面類,封裝成MapperProxyFactory放入kownMappers中;另一個是把要執行的方法封裝成MapperStatement。

一、概述

在上篇文章中分析了<mappers>標籤,重點分析了<package>子標籤,除了可以配置<package>子標籤外,在<mappers>標籤中還可以配置<mapper>子標籤,該子標籤可以配置的熟悉有resource、url、class三個屬性,解析resource和url的過程大致相同,看解析resource屬性的過程。

下面看部分程式碼,

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();
}

可以看到呼叫了XMLMapperBuilder的parse方法,

public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//1、解析mapper檔案中的<mapper>標籤及其子標籤,並設定CurrentNamespace的值,供下面第2步使用
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//2、繫結Mapper介面,並解析對應的XML對映檔案
bindMapperForNamespace();
} parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

從上面的程式碼中可以看出首先解析resource資源說代表的XML對映檔案,然後解析XML;對映檔案中的namespace配置的介面。

二、詳述

通過上面的分析知道,解析resource配置的XML對映檔案,分為兩步,第一步就是解析XML對映檔案的內容;第二步是解析XML對映檔案中配置的namespace屬性,也就是對於的Mapper介面。

1、解析XML對映檔案

看如何解析XML對映檔案內容,也就是下面的這行程式碼,

configurationElement(parser.evalNode("/mapper"));

看具體的實現,

/**
* 解析XML對映檔案的內容
* @param context
*/
private void configurationElement(XNode context) {
try {
//獲得namespace屬性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//設定到currentNamespace中
builderAssistant.setCurrentNamespace(namespace);
//解析<cache-ref>標籤
cacheRefElement(context.evalNode("cache-ref"));
//二級快取標籤
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}

上面方法解析XML對映檔案的內容,其中有個和二級快取相關的配置,即<cache>標籤。那麼xml對映檔案可以配置哪些標籤那,看下面,

在XML對映檔案中可以配置上面的這些標籤,也就是上面方法中解析的內容。重點看解析select、update、delete、select。也就是下面這行程式碼

//解析select、insert、update、delete子標籤
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

其方法定義如下,

private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}

這裡會校驗databaseId,如果自定義配置了,則使用自定義的,否則使用預設的,看方法buildStatementFromContext方法

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}

呼叫XMLStatementBuilder的parseStatementNode方法

/**
* 解析select、update、delete、insert標籤
*/
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); Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//查詢語句預設開啟一級快取,這裡預設是true
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//生成SqlSource,這裡分兩種,DynamicSqlSource和RawSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
//例,id="selectUser"
//這裡的keyStatementId=selectUser!selectKey
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
//keyStatementId=cn.com.dao.userMapper.selectUser!selectKey
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;
} builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

上面的程式碼主要是解析標籤中的各種屬性,那麼標籤中可以配置哪些屬性那,下面看select標籤的屬性,詳情可參見https://www.w3cschool.cn/mybatis/f4uw1ilx.html

上面是select標籤中可以配置的屬性列表。

上面的程式碼重點看以下重點

二級快取

下面看和快取相關的

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//查詢語句預設開啟一級快取,這裡預設是true
boolean useCache = context.getBooleanAttribute("useCache", isSelect);

這裡僅針對select查詢語句使用快取,這裡的預設不會重新整理快取flushCache為false,預設開啟快取useCache為ture,這裡的快取指的是一級快取,經常說的mybatis一級快取,一級快取是sqlSession級別的。

看完了一級快取,下面看SqlSource的內容

SqlSource

下面是SqlSource相關的,

//生成SqlSource,這裡分兩種,DynamicSqlSource和RawSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

上面是生成SqlSource的過程,

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}

看parseScriptNode方法

public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {
//含義${}符合的為DynamicSqlSource
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//不含有${}的為rawSqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

從上面的程式碼可以看到在對映檔案中根據引數佔位符的識別符號(${}、#{})分為DynamicSqlSource和RawSqlSource。具體如何判斷,後面詳細分析。

addMappedStatement

最後看builderAssistant.addMappedStatement方法,

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");
}
//cn.com.dao.UserMapper.selectUser
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
} MappedStatement statement = statementBuilder.build();
/*向mappedStatements欄位中加入MappedStatement,這裡會加入兩個key
* cn.com.dao.UserMapper.selectUser statement
* selectUser statement
* 每次都會插入上面的兩種key,兩種key對應的value都是同一個statement
*
*/
configuration.addMappedStatement(statement);
return statement;
}

該方法主要完成的功能是生成MappedStatement,且放入configuration中。

2、解析namespace屬性

上面分析瞭解析XML對映檔案的內容的過程,最後的結果是把XML對映檔案中的select、update、insert、delete標籤的內容解析為MappedStatement。下面看解析XML對映檔案中的namespace屬性,

//2、繫結Mapper介面,並解析對應的XML對映檔案
bindMapperForNamespace();

上面我給的註釋是繫結介面並解析對應的XML對映檔案,這個方法沒有引數,怎麼繫結具體的介面並解析對應的對映檔案那,

private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//載入類,這裡載入的是mapper檔案中配置的namespace配置的介面
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {//判斷該介面是否被載入過,在mapperRegistry中的knowsMapper中判斷
// 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
//把該介面作為已載入的資源存放到loadedResources中,loadedResources存放的是已載入的mapper檔案的路徑
configuration.addLoadedResource("namespace:" + namespace);
//把該介面放到mapperRegistry中的knowsMapper中,並解析該介面,根據loadedResources判定是否需要解析相應的XML對映檔案
configuration.addMapper(boundType);
}
}
}
}

獲得builderAssistant.getCurrentNamespace(),在解析XML對映檔案時,第一步便是設定該屬性,這裡用到的便是上一步中設定的那個XML對映檔案中的namespace屬性值。獲得該介面的名稱,判斷是否生成過MapperProxyFactory,即放入過knownMappers中,看configuration.hasMapper方法,

public boolean hasMapper(Class<?> type) {
return mapperRegistry.hasMapper(type);
}
public <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}

如果在knownMappers中,則不進行解析,如果不在才進行下面的邏輯處理,呼叫configuration.addLoadedResource方法,放入loadedResources中,標識在第一步已經解析過對應的XML對映檔案;呼叫configuration.addMapper方法,解析該介面,這個過程和在<mapper>標籤中配置class屬性的過程是一樣的,後面詳細分析。

三、總結

本文分析了mappers標籤中mapper子標籤中resource和url屬性的解析過程,首先解析對應的XML對映檔案,解析的結果為MappedStatement物件,然後解析其namespace對應的介面,解析的結果為MapperProxyFactory物件。

有不當之處,歡迎指正,感謝!