分析映射文件中剩余的几个节点,分别是select、insert、update以及delete等等。这几个节点中存储的是相同的内容,都是 SQL 语句,所以这几个节点的解析过程也是相同的

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
// 调用重载方法构建 Statement
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 调用重载方法构建 Statement,requiredDatabaseId 参数为空
buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list,
String requiredDatabaseId) {
for (XNode context : list) {
// 创建 Statement 建造类
final XMLStatementBuilder statementParser = new XMLStatementBuilder(
configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析 Statement 节点,并将解析结果存储到
// configuration 的 mappedStatements 集合中
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// 解析失败,将解析器放入 Configuration 的 incompleteStatements 集合中
configuration.addIncompleteStatement(statementParser);
}
}
}

实际的解析逻辑,在parseStatementNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public void parseStatementNode() {
// 获取 id 和 databaseId 属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");

// 根据 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 实例
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 创建 KeyGenerator 实例
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);
}

代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以
上代码做的事情如下。

  1. 解析include节点
  2. 解析selectKey节点
  3. 解析SQL,获取SqlSource
  4. 构建MappedStatement实例

解析include节点

解析逻辑封装在 applyIncludes 中

1
2
3
4
5
6
7
8
9
10
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
if (configurationVariables != null) {
// 将 configurationVariables 中的数据添加到 variablesContext 中
variablesContext.putAll(configurationVariables);
}
// 调用重载方法处理 <include> 节点
applyIncludes(source, variablesContext, false);
}

创建了一个新的 Properties 对象,并将全局 Properties 添加到其中。这样做的原
因是 applyIncludes 的重载方法会向 Properties 中添加新的元素,如果直接将全局 Properties 传给重载方法,会造成全局 Properties 被污染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void applyIncludes(Node source, 
final Properties variablesContext, boolean included) {
// ⭐ 第一个条件分支
if (source.getNodeName().equals("include")) {
// 获取 <sql> 节点。若 refid 中包含属性占位符 ${},
// 则需先将属性占位符替换为对应的属性值
Node toInclude = findSqlFragment(
getStringAttribute(source, "refid"), variablesContext);
// 解析<include>的子节点<property>,并将解析结果与 variablesContext 融合,
// 然后返回融合后的 Properties。若 <property> 节点的 value 属性中存在
// 占位符 ${},则将占位符替换为对应的属性值
Properties toIncludeContext =
getVariablesContext(source, variablesContext);
/*
* 这里是一个递归调用,用于将 <sql> 节点内容中出现的属性占位符 ${}
* 替换为对应的属性值。这里要注意一下递归调用的参数:
*
* - toInclude:<sql> 节点对象
* - toIncludeContext:<include> 子节点 <property> 的解析结果与
* 全局变量融合后的结果
*/
applyIncludes(toInclude, toIncludeContext, true);
// 如果 <sql> 和 <include> 节点不在一个文档中,
// 则从其他文档中将 <sql> 节点引入到 <include> 所在文档中
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude=source.getOwnerDocument().importNode(toInclude,true);
}
// 将 <include> 节点替换为 <sql> 节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
// 将 <sql> 中的内容插入到 <sql> 节点之前
toInclude.getParentNode().insertBefore(
toInclude.getFirstChild(), toInclude);
}// 前面已经将 <sql> 节点的内容插入到 dom 中了,
// 现在不需要 <sql> 节点了,这里将该节点从 dom 中移除
toInclude.getParentNode().removeChild(toInclude);
// ⭐ 第二个条件分支
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
// 将 source 节点属性中的占位符 ${} 替换成具体的属性值
attr.setNodeValue(PropertyParser.parse(
attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 递归调用
applyIncludes(children.item(i), variablesContext, included);
}
// ⭐ 第三个条件分支
} else if (included && source.getNodeType() == Node.TEXT_NODE &&
!variablesContext.isEmpty()) {
// 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值
source.setNodeValue(PropertyParser.parse(
source.getNodeValue(), variablesContext));
}
}

上面的代码如果从上往下读,不太容易看懂。因为上面的方法由三个条件分支,外加两
个递归调用组成,代码的执行顺序并不是由上而下。

演绎代码的执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mapper namespace="xyz.coolblog.dao.ArticleDao">
<sql id="table">
${table_name}
</sql>
<select id="findOne" resultType="xyz.coolblog.dao.Article">
SELECT
id, title
FROM
<include refid="table">
<property name="table_name" value="article"/>
</include>
WHERE id = #{id}
</select>
</mapper>

applyIncludes 方法第一次被调用时的状态

1
2
3
4
5
6
7
8
9
10
参数值:
source = <select> 节点
节点类型:ELEMENT_NODE
variablesContext = [ ] // 无内容
included = false

执行流程:
1. 进入条件分支 2
2. 获取 <select> 子节点列表
3. 遍历子节点列表,将子节点作为参数,进行递归调用

第一次调用 applyIncludes 方法,source=select,代码进入条件分支 2。首先要获取节点的子节点列表。

编号 子节点 类型 描述
1 SELECT id, title FROM TEXT_NODE 文本节点
2 ELEMENT_NODE 普通节点
3 WHERE id = #{id} TEXT_NODE 文本节点

在上面三个子节点中,子节点1和子节点3都是文本节点,调用过程一致。子节点作为参数进行递归调用。
image.png

子节点 2 的调用过程
image.png

解析selectKey节点

对于一些不支持自增主键的数据库来说,我们在插入数据时,需要明确指定主键数据。
��� Oracle 数据库为例,Oracle数据库不支持自增主键,但它提供了自增序列工具。我们每次向数据库中插入数据时,可以先通过自增序列获取主键数据,然后再进行插入。

这里涉及到两次数据库查询操作,但我们并不能在一个select节点中同时配置两个 select 语句,这会导致SQL语句出错。对于这个问题,MyBatis提供的可以很好的解决

1
2
3
4
5
6
7
8
9
<insert id="saveAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select author_seq.nextval from dual
</selectKey>
insert into Author
(id, name, password)
values
(#{id}, #{username}, #{password})
</insert>

查询语句会先于插入语句执行,这样我们就可以在插入时获取到主键的值。

解析过程

1
2
3
4
5
6
7
8
9
10
11
12
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass,     
LanguageDriver langDriver) {
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
// 解析 <selectKey> 节点,databaseId 不为空
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
// 解析 <selectKey> 节点,databaseId 为空
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
// 将 <selectKey> 节点从 dom 树中移除
removeSelectKeyNodes(selectKeyNodes);
}

selectKey节点在解析完成后,会被从 dom 树中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private void parseSelectKeyNodes(String parentId, List<XNode> list, 
Class<?> parameterTypeClass, LanguageDriver langDriver,
String skRequiredDatabaseId) {
for (XNode nodeToHandle : list) {
// id = parentId + !selectKey,比如 saveUser!selectKey
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 获取 <selectKey> 节点的 databaseId 属性
String databaseId = nodeToHandle.getStringAttribute("databaseId");
// 匹配 databaseId
if (databaseIdMatchesCurrent(id, databaseId,skRequiredDatabaseId)) {
// 解析 <selectKey> 节点
parseSelectKeyNode(id, nodeToHandle,
parameterTypeClass, langDriver, databaseId);
}
}
}


private void parseSelectKeyNode(String id, XNode nodeToHandle,
Class<?> parameterTypeClass, LanguageDriver langDriver,
String databaseId) {
// 获取各种属性
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
StatementType statementType = StatementType.valueOf(
nodeToHandle.getStringAttribute(
"statementType", StatementType.PREPARED.toString()));
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");

boolean executeBefore = "BEFORE".equals(
nodeToHandle.getStringAttribute("order", "AFTER"));
// 设置默认值
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;

// 创建 SqlSource
SqlSource sqlSource = langDriver.createSqlSource(
configuration, nodeToHandle, parameterTypeClass);
// <selectKey> 节点中只能配置 SELECT 查询语句,
// 因此 sqlCommandType 为 SqlCommandType.SELECT
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
// 构建 MappedStatement,并将 MappedStatement
// 添加到 Configuration 的 mappedStatements map 中
builderAssistant.addMappedStatement(id, sqlSource, statementType,
sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass,
resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache,
resultOrdered, keyGenerator, keyProperty, keyColumn,
databaseId, langDriver, null);
// id = namespace + "." + id
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement =
configuration.getMappedStatement(id, false);
// 创建 SelectKeyGenerator,并添加到 keyGenerators map 中
configuration.addKeyGenerator(id,
new SelectKeyGenerator(keyStatement, executeBefore));
}

步骤如下:

  1. 创建 SqlSource 实例
  2. 构建并缓存 MappedStatement 实例
  3. 构建并缓存 SelectKeyGenerator 实例

解析 SQL 语句

目前的 SQL 语句节点由一些文本节点和普通节点组成,比如if、where等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// -☆- XMLLanguageDriver
public SqlSource createSqlSource(Configuration configuration,
XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(
configuration, script, parameterType);
return builder.parseScriptNode();
}

// -☆- XMLScriptBuilder
public SqlSource parseScriptNode() {
// 解析 SQL 语句节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
// 根据 isDynamic 状态创建不同的 SqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(
configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

SQL 语句的解析逻辑被封装在了 XMLScriptBuilder 类的 parseScriptNode 方法中。该方法首先会调用 parseDynamicTags 解析 SQL语句节点,在解析过程中,会判断节点是是否包含一些动态标记,比如${}占位符以及动态 SQL 节点等。

若包含动态标记,则会将isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource。下面,我们来看一下parseDynamicTags 方法的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/** 该方法用于初始化 nodeHandlerMap 集合,该集合后面会用到 */
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}

protected MixedSqlNode 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));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE ||
child.getNode().getNodeType() == Node.TEXT_NODE) {
// 获取文本内容
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 若文本中包含 ${} 占位符,也被认为是动态节点
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
// 设置 isDynamic 为 true
isDynamic = true;
} else {
// 创建 StaticTextSqlNode
contents.add(new StaticTextSqlNode(data));
}
// child 节点是 ELEMENT_NODE 类型,比如 <if>、<where> 等
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 获取节点名称,比如 if、where、trim 等
String nodeName = child.getNode().getNodeName();
// 根据节点名称获取 NodeHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
// 如果 handler 为空,表明当前节点对与 MyBatis 来说,是未知节点。
// MyBatis 无法处理这种节点,故抛出异常
if (handler == null) {
throw new BuilderException("……");
}
// 处理 child 节点,生成相应的 SqlNode
handler.handleNode(child, contents);
// 设置 isDynamic 为 true
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}

这里,不管是动态 SQL 节点还是静态 SQL 节点,我们都可以把它们看成是 SQL片段,一个 SQL 语句由多个 SQL 片段组成。在解析过程中,这些SQL片段被存储在contents集合中。最后,该集合会被传给 MixedSqlNode构造方法,用于创建MixedSqlNode 实例。

从 MixedSqlNode 类名上可知,它会存储多种类型的SqlNode。除了上面代码中已出现的几种 SqlNode 实现类,还有一些 SqlNode 实现类未出现在上面的代码中。但它们也参与了 SQL 语句节点的解析过程,这里我们来看一下这些幕后的 SqlNode 类。
image.png

这些 SqlNode 是如何生成的呢?答案是由各种NodeHandler生成。我们再回到上面的代码中,可以看到这样一句代码:

1
handler.handleNode(child, contents);

下面来简单分析一下WhereHandler 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 定义在 XMLScriptBuilder 中 */
private class WhereHandler implements NodeHandler {
public WhereHandler() {
}
@Override
public void handleNode(XNode nodeToHandle,List<SqlNode> targetContents){
// 调用 parseDynamicTags 解析 <where> 节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 创建 WhereSqlNode
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
// 添加到 targetContents
targetContents.add(where);
}
}

handleNode 方法内部会再次调用parseDynamicTags解析节点中的内容,这样又会生成一个 MixedSqlNode 对象。最终,整个SQL语句节点会生成一个具有树状结构的MixedSqlNode。
image.png

到此,SQL 语句的解析过程就分析完了。现在,我们已经将 XML 配置解析了 SqlSource,但这还没有结束。SqlSource 中只能记录 SQL语句信息,除此之外,这里还有一些额外的信息需要记录。

因此,我们需要一个类能够同时存储 SqlSource 和其他的信息。这个类就是MappedStatement。

构建 MappedStatement

SQL 语句节点可以定义很多属性,这些属性和属性值最终存储在 MappedStatement中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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");
}

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)3
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.resultSetType(resultSetType) .useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
// 获取或创建 ParameterMap
ParameterMap statementParameterMap =
getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 构建 MappedStatement,没有什么复杂逻辑,不跟下去了
MappedStatement statement = statementBuilder.build();

// 添加 MappedStatement 到 configuration 的 mappedStatements 集合中
configuration.addMappedStatement(statement);
return statement;
}