Mybatis原始碼分析(1)—— Mapper檔案解析
感覺CSDN對markdown的支援不夠友好,總是伴隨各種問題,很惱火!
xxMapper.xml的解析主要由XMLMapperBuilder類完成,parse方法來完成解析:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace() ;
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
configurationElement(parser.evalNode(“/mapper”));
上面的這行程式碼是提取部分來解析:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace" );
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql" ));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
}
}
將各個元素細分,逐一解析:
- parameterMapElement方法處理parameterMap節點部分
- resultMapElements方法處理resultMap節點部分
- sqlElement處理sql節點部分
- buildStatementFromContext方法處理select|insert|update|delete部分
重點看看buildStatementFromContext:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
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);
}
}
}
List型別的list包含了單個mapper.xml檔案的所有sql動作部分:
<select></select>
<insert></insert>
<update></update>
<delete></delete>
單一節點使用XMLStatementBuilder的parseStatementNode來解析,取其中重要的三行程式碼:
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
- List contents = parseDynamicTags(context);
private List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
String nodeName = child.getNode().getNodeName();
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
contents.add(new TextSqlNode(data));
} else {
NodeHandler handler = nodeHandlers.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
}
}
return contents;
}
if塊是處理text部分,else塊處理其他內嵌node部分:
<if></if>
<choose></choose>
....
最終的結果都會新增到List型別的contexts中。XNode形如父子關係,類似連結串列儲存。
例如:
<select id="getCurrSpaceNums" resultType="com.fcs.model.CarParkingSpaceNum">
select pp.permit_cards maxLeng,pp.permit_cards currLeng
from TB_UHOME_PARKING_PLACE pp
where pp.COMMUNITY_ID=#{orgId} and pp.STATUS='1'
and pp.PLACE_CODE = #{parkingCode}
<if test="parkingArea != null and parkingArea !=''">
and pp.PLACE_AREA= #{parkingArea}
</if>
</select>
body部分:
select pp.permit_cards maxLeng,pp.permit_cards currLeng
from TB_UHOME_PARKING_PLACE pp
where pp.COMMUNITY_ID=#{orgId} and pp.STATUS='1'
and pp.PLACE_CODE = #{parkingCode}
取上面的text構成TextSqlNode
第二個childNode是if標籤包裹部分,取出來的body為:
and pp.PLACE_AREA= #{parkingArea}
NodeHandler handler = nodeHandlers.get(nodeName);
上面的程式碼獲取IfHandler(對應的還有ChooseHandler,ForEachHandler等)。
handler.handleNode(child, contents);
看看內部類IfHandler會如何處理:
private class IfHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
繼續呼叫parseDynamicTags,然後構造IfSqlNode,新增到總的contents中。
這時候name為“select”的XNode下解析出的contents包含了三個SqlNode:
- MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
利用contents構造MixedSqlNode型別的rootSqlNode。
- SqlSource sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
利用rootSqlNode構造DynamicSqlSource。DynamicSqlSource的getBoundSql方法是非常重要的,用來獲取BoundSql。此時僅僅是構造sqlSource放到MappedStatement中,以sqlSource形式入,以BoundSql形式出。
- builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, keyGenerator, keyProperty, keyColumn, databaseId);
解析完一個statement節點,就會將其包裝成MappedStatement,基本上就是你在Mapper.xml檔案中寫的每個sql語句對應一個MappedStatement。最終都新增到Configuration的MappedStatement集合中。
補充
在DynamicSqlSource的getBoundSql方法中有下面一行程式碼:
rootSqlNode.apply(context);
我們之前存的rootSqlNode是一個MixedSqlNode,代表混合型SqlNode,看其apply方法:
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
就是將之前的SqlNode集合contents遍歷處理。這個contents包含兩種型別的SqlNode:TextSqlNode和IfSqlNode。
TextSqlNode的apply方法:
public boolean apply(DynamicContext context) {
GenericTokenParser parser = new GenericTokenParser("${", "}", new BindingTokenParser(context));
context.appendSql(parser.parse(text));
return true;
}
這裡就涉及到引數綁定了,將${param}替換為實際引數值。
IfSqlNode的apply方法:
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
通常IfSqlNode也是包含一個TextSqlNode,表示式滿足要求就繼續呼叫TextSqlNode的apply方法,append滿足條件的sql語句。
這樣一個動態sql就構造出來了個大概。後面還有進一步的處理:
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);
主要是針對#{param}部分的處理,後面在”引數繫結“分析時會詳細解讀。