我是一段不羁的公告!
记得给艿艿这 3 个项目加油,添加一个 STAR 噢。
https://github.com/YunaiV/SpringBoot-Labs
https://github.com/YunaiV/onemall
https://github.com/YunaiV/ruoyi-vue-pro

精尽 MyBatis 源码分析 —— MyBatis 初始化(三)之加载 Statement 配置

1. 概述

本文接 《精尽 MyBatis 源码分析 —— MyBatis 初始化(二)之加载 Mapper 映射配置文件》 一文,来分享 MyBatis 初始化的第三步,加载 Statement 配置。而这个步骤的入口是 XMLStatementBuilder 。下面,我们一起来看看它的代码实现。

《精尽 MyBatis 源码分析 —— MyBatis 初始化(二)之加载 Mapper 映射配置文件》「2.3.5 buildStatementFromContext」 中,我们已经看到对 XMLStatementBuilder 的调用代码。代码如下:

// XMLMapperBuilder.java

private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
// 上面两块代码,可以简写成 buildStatementFromContext(list, configuration.getDatabaseId());
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// <1> 遍历 <select /> <insert /> <update /> <delete /> 节点们
for (XNode context : list) {
// <1> 创建 XMLStatementBuilder 对象,执行解析
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// <2> 解析失败,添加到 configuration 中
configuration.addIncompleteStatement(statementParser);
}
}
}

2. XMLStatementBuilder

org.apache.ibatis.builder.xml.XMLStatementBuilder ,继承 BaseBuilder 抽象类,Statement XML 配置构建器,主要负责解析 Statement 配置,即 <select /><insert /><update /><delete /> 标签。

2.1 构造方法

// XMLStatementBuilder.java

private final MapperBuilderAssistant builderAssistant;
/**
* 当前 XML 节点,例如:<select />、<insert />、<update />、<delete /> 标签
*/
private final XNode context;
/**
* 要求的 databaseId
*/
private final String requiredDatabaseId;

public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
super(configuration);
this.builderAssistant = builderAssistant;
this.context = context;
this.requiredDatabaseId = databaseId;
}

2.2 parseStatementNode

#parseStatementNode() 方法,执行 Statement 解析。代码如下:

// XMLStatementBuilder.java

public void parseStatementNode() {
// <1> 获得 id 属性,编号。
String id = context.getStringAttribute("id");
// <2> 获得 databaseId , 判断 databaseId 是否匹配
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}

// <3> 获得各种属性
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");

// <4> 获得 lang 对应的 LanguageDriver 对象
LanguageDriver langDriver = getLanguageDriver(lang);

// <5> 获得 resultType 对应的类
Class<?> resultTypeClass = resolveClass(resultType);
// <6> 获得 resultSet 对应的枚举值
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// <7> 获得 statementType 对应的枚举值
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));

// <8> 获得 SQL 对应的 SqlCommandType 枚举值
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// <9> 获得各种属性
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// Include Fragments before parsing
// <10> 创建 XMLIncludeTransformer 对象,并替换 <include /> 标签相关的内容
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

// Parse selectKey after includes and remove them.
// <11> 解析 <selectKey /> 标签
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// <12> 创建 SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// <13> 获得 KeyGenerator 对象
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
// <13.1> 优先,从 configuration 中获得 KeyGenerator 对象。如果存在,意味着是 <selectKey /> 标签配置的
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
// <13.2> 其次,根据标签属性的情况,判断是否使用对应的 Jdbc3KeyGenerator 或者 NoKeyGenerator 对象
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys", // 优先,基于 useGeneratedKeys 属性判断
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) // 其次,基于全局的 useGeneratedKeys 配置 + 是否为插入语句类型
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

// 创建 MappedStatement 对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
  • <1> 处,获得 id 属性,编号。
  • <2> 处,获得 databaseId 属性,并调用 #databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) 方法,判断 databaseId 是否匹配。详细解析,见 「2.3 databaseIdMatchesCurrent」
  • <3> 处,获得各种属性。
  • <4> 处,调用 #getLanguageDriver(String lang) 方法,获得 lang 对应的 LanguageDriver 对象。详细解析,见 「2.4 getLanguageDriver」
  • <5> 处,获得 resultType 对应的类。
  • <6> 处,获得 resultSet 对应的枚举值。关于 org.apache.ibatis.mapping.ResultSetType 枚举类,点击查看。一般情况下,不会设置该值。它是基于 java.sql.ResultSet 结果集的几种模式,感兴趣的话,可以看看 《ResultSet 的 Type 属性》
  • <7> 处,获得 statementType 对应的枚举值。关于 org.apache.ibatis.mapping.StatementType 枚举类,点击查看
  • <8> 处,获得 SQL 对应的 SqlCommandType 枚举值。
  • <9> 处,获得各种属性。
  • <10> 处,创建 XMLIncludeTransformer 对象,并调用 XMLIncludeTransformer#applyIncludes(Node source) 方法,替换 <include /> 标签相关的内容。详细解析,见 「3. XMLIncludeTransformer」
  • <11> 处,调用 #processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) 方法,解析 <selectKey /> 标签。详细解析,见 「2.5 processSelectKeyNodes」
  • <12> 处,调用 LanguageDriver#createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) 方法,创建 SqlSource 对象。详细解析,见后续文章。
  • <13> 处,获得 KeyGenerator 对象。分成 <13.1><13.2> 两种情况。具体的,胖友耐心看下代码注释。
  • <14> 处,调用 MapperBuilderAssistant#addMappedStatement(...) 方法,创建 MappedStatement 对象。详细解析,见 「4.1 addMappedStatement」 中。

2.3 databaseIdMatchesCurrent

#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) 方法,判断 databaseId 是否匹配。代码如下:

// XMLStatementBuilder.java

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
// 如果不匹配,则返回 false
if (requiredDatabaseId != null) {
return requiredDatabaseId.equals(databaseId);
} else {
// 如果未设置 requiredDatabaseId ,但是 databaseId 存在,说明还是不匹配,则返回 false
// mmp ,写的好绕
if (databaseId != null) {
return false;
}
// skip this statement if there is a previous one with a not null databaseId
// 判断是否已经存在
id = builderAssistant.applyCurrentNamespace(id, false);
if (this.configuration.hasStatement(id, false)) {
MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
// 若存在,则判断原有的 sqlFragment 是否 databaseId 为空。因为,当前 databaseId 为空,这样两者才能匹配。
return previous.getDatabaseId() == null;
}
}
return true;
}
  • 代码比较简单,胖友自己瞅瞅就得。从逻辑上,和我们在 XMLMapperBuilder 看到的同名方法 #databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) 方法是一致的。

2.4 getLanguageDriver

#getLanguageDriver(String lang) 方法,获得 lang 对应的 LanguageDriver 对象。代码如下:

// XMLStatementBuilder.java

private LanguageDriver getLanguageDriver(String lang) {
// 解析 lang 对应的类
Class<? extends LanguageDriver> langClass = null;
if (lang != null) {
langClass = resolveClass(lang);
}
// 获得 LanguageDriver 对象
return builderAssistant.getLanguageDriver(langClass);
}
  • 调用 MapperBuilderAssistant#getLanguageDriver(lass<? extends LanguageDriver> langClass) 方法,获得 LanguageDriver 对象。代码如下:

    // MapperBuilderAssistant.java

    public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
    // 获得 langClass 类
    if (langClass != null) {
    configuration.getLanguageRegistry().register(langClass);
    } else { // 如果为空,则使用默认类
    langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
    }
    // 获得 LanguageDriver 对象
    return configuration.getLanguageRegistry().getDriver(langClass);
    }
    • 关于 org.apache.ibatis.scripting.LanguageDriverRegistry 类,我们在后续的文章,详细解析。

2.5 processSelectKeyNodes

#processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) 方法,解析 <selectKey /> 标签。代码如下:

// XMLStatementBuilder.java

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
// <1> 获得 <selectKey /> 节点们
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
// <2> 执行解析 <selectKey /> 节点们
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
// <3> 移除 <selectKey /> 节点们
removeSelectKeyNodes(selectKeyNodes);
}
  • <1> 处,获得 <selectKey /> 节点们。
  • <2> 处,调用 #parseSelectKeyNodes(...) 方法,执行解析 <selectKey /> 节点们。详细解析,见 「2.5.1 parseSelectKeyNodes」
  • <3> 处,调用 #removeSelectKeyNodes(List<XNode> selectKeyNodes) 方法,移除 <selectKey /> 节点们。代码如下:

    // XMLStatementBuilder.java

    private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
    for (XNode nodeToHandle : selectKeyNodes) {
    nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
    }
    }

2.5.1 parseSelectKeyNodes

#parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) 方法,执行解析 <selectKey /> 子节点们。代码如下:

// XMLStatementBuilder.java

private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
// <1> 遍历 <selectKey /> 节点们
for (XNode nodeToHandle : list) {
// <2> 获得完整 id ,格式为 `${id}!selectKey`
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// <3> 获得 databaseId , 判断 databaseId 是否匹配
String databaseId = nodeToHandle.getStringAttribute("databaseId");
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
// <4> 执行解析单个 <selectKey /> 节点
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
  • <1> 处,遍历 <selectKey /> 节点们,逐个处理。
  • <2> 处,获得完整 id 编号,格式为 ${id}!selectKey 。这里很重要,最终解析的 <selectKey /> 节点,会创建成一个 MappedStatement 对象。而该对象的编号,就是 id
  • <3> 处,获得 databaseId ,并调用 #databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) 方法,判断 databaseId 是否匹配。😈 通过此处,我们可以看到,即使有多个 <selectionKey /> 节点,但是最终只会有一个节点被解析,就是符合的 databaseId 对应的。因为不同的数据库实现不同,对于获取主键的方式也会不同。
  • <4> 处,调用 #parseSelectKeyNode(...) 方法,执行解析单个 <selectKey /> 节点。详细解析,见 「2.5.2 parseSelectKeyNode」

2.5.2 parseSelectKeyNode

#parseSelectKeyNode(...) 方法,执行解析单个 <selectKey /> 节点。代码如下:

// XMLStatementBuilder.java

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
// <1.1> 获得各种属性和对应的类
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"));

// defaults
// <1.2> 创建 MappedStatement 需要用到的默认值
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;

// <1.3> 创建 SqlSource 对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;

// <1.4> 创建 MappedStatement 对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

// <2.1> 获得 SelectKeyGenerator 的编号,格式为 `${namespace}.${id}`
id = builderAssistant.applyCurrentNamespace(id, false);
// <2.2> 获得 MappedStatement 对象
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
// <2.3> 创建 SelectKeyGenerator 对象,并添加到 configuration 中
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
  • <1.1> 处理,获得各种属性和对应的类。
  • <1.2> 处理,创建 MappedStatement 需要用到的默认值。
  • <1.3> 处理,调用 LanguageDriver#createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) 方法,创建 SqlSource 对象。详细解析,见后续文章。
  • <1.4> 处理,调用 MapperBuilderAssistant#addMappedStatement(...) 方法,创建 MappedStatement 对象。
  • <2.1> 处理,获得 SelectKeyGenerator 的编号,格式为 ${namespace}.${id}
  • <2.2> 处理,获得 MappedStatement 对象。该对象,实际就是 <1.4> 处创建的 MappedStatement 对象。
  • <2.3> 处理,调用 Configuration#addKeyGenerator(String id, KeyGenerator keyGenerator) 方法,创建 SelectKeyGenerator 对象,并添加到 configuration 中。代码如下:

    // Configuration.java

    /**
    * KeyGenerator 的映射
    *
    * KEY:在 {@link #mappedStatements} 的 KEY 的基础上,跟上 {@link SelectKeyGenerator#SELECT_KEY_SUFFIX}
    */
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

    public void addKeyGenerator(String id, KeyGenerator keyGenerator) {
    keyGenerators.put(id, keyGenerator);
    }

3. XMLIncludeTransformer

org.apache.ibatis.builder.xml.XMLIncludeTransformer ,XML <include /> 标签的转换器,负责将 SQL 中的 <include /> 标签转换成对应的 <sql /> 的内容。

3.1 构造方法

// XMLIncludeTransformer.java

private final Configuration configuration;
private final MapperBuilderAssistant builderAssistant;

public XMLIncludeTransformer(Configuration configuration, MapperBuilderAssistant builderAssistant) {
this.configuration = configuration;
this.builderAssistant = builderAssistant;
}

3.2 applyIncludes

#applyIncludes(Node source) 方法,将 <include /> 标签,替换成引用的 <sql /> 。代码如下:

// XMLIncludeTransformer.java

public void applyIncludes(Node source) {
// <1> 创建 variablesContext ,并将 configurationVariables 添加到其中
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
if (configurationVariables != null) {
variablesContext.putAll(configurationVariables);
}
// <2> 处理 <include />
applyIncludes(source, variablesContext, false);
}
  • <1> 处,创建 variablesContext ,并将 configurationVariables 添加到其中。这里的目的是,避免 configurationVariables 被下面使用时候,可能被修改。实际上,从下面的实现上,不存在这个情况。
  • <2> 处,调用 #applyIncludes(Node source, final Properties variablesContext, boolean included) 方法,处理 <include />

#applyIncludes(Node source, final Properties variablesContext, boolean included) 方法,使用递归的方式,将 <include /> 标签,替换成引用的 <sql /> 。代码如下:

// XMLIncludeTransformer.java

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
// <1> 如果是 <include /> 标签
if (source.getNodeName().equals("include")) {
// <1.1> 获得 <sql /> 对应的节点
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
// <1.2> 获得包含 <include /> 标签内的属性
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// <1.3> 递归调用 #applyIncludes(...) 方法,继续替换。注意,此处是 <sql /> 对应的节点
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { // 这个情况,艿艿暂时没调试出来
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// <1.4> 将 <include /> 节点替换成 <sql /> 节点
source.getParentNode().replaceChild(toInclude, source); // 注意,这是一个奇葩的 API ,前者为 newNode ,后者为 oldNode
// <1.4> 将 <sql /> 子节点添加到 <sql /> 节点前面
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); // 这里有个点,一定要注意,卡了艿艿很久。当子节点添加到其它节点下面后,这个子节点会不见了,相当于是“移动操作”
}
// <1.4> 移除 <include /> 标签自身
toInclude.getParentNode().removeChild(toInclude);
// <2> 如果节点类型为 Node.ELEMENT_NODE
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
// <2.1> 如果在处理 <include /> 标签中,则替换其上的属性,例如 <sql id="123" lang="${cpu}"> 的情况,lang 属性是可以被替换的
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
// <2.2> 遍历子节点,递归调用 #applyIncludes(...) 方法,继续替换
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
// <3> 如果在处理 <include /> 标签中,并且节点类型为 Node.TEXT_NODE ,并且变量非空
// 则进行变量的替换,并修改原节点 source
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// replace variables in text node
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
  • 这是个有自递归逻辑的方法,所以理解起来会有点绕,实际上还是蛮简单的。为了更好的解释,我们假设示例如下:

    // mybatis-config.xml

    <properties>
    <property name="cpu" value="16c" />
    <property name="target_sql" value="123" />
    </properties>

    // Mapper.xml

    <sql id="123" lang="${cpu}">
    ${cpu}
    aoteman
    qqqq
    </sql>

    <select id="testForInclude">
    SELECT * FROM subject
    <include refid="${target_sql}" />
    </select>
  • 方法参数 included ,是否正在处理 <include /> 标签中。😈 一脸懵逼?不要方,继续往下看。
  • 在上述示例的 <select /> 节点进入这个方法时,会首先进入 <2> 这块逻辑。
    • <2.1> 处,因为 不满足 included 条件,初始传入是 false ,所以跳过。
    • <2.2> 处,遍历子节点,递归调用 #applyIncludes(...) 方法,继续替换。如图所示:子节点
      • 子节点 [0][2] ,执行该方法时,不满足 <1><2><3> 任一一种情况,所以可以忽略。虽然说,满足 <3> 的节点类型为 Node.TEXT_NODE ,但是 included 此时为 false ,所以不满足。
      • 子节点 [1] ,执行该方法时,满足 <1> 的情况,所以走起。
  • 在子节点 [1] ,即 <include /> 节点进入 <1> 这块逻辑:
    • <1.1> 处,调用 #findSqlFragment(String refid, Properties variables) 方法,获得 <sql /> 对应的节点,即上述示例看到的,<sql id="123" lang="${cpu}"> ... </> 。详细解析,见 「3.3 findSqlFragment」
    • <1.2> 处,调用 #getVariablesContext(Node node, Properties inheritedVariablesContext) 方法,获得包含 <include /> 标签内的属性 Properties 对象。详细解析,见 「3.4 getVariablesContext」
    • <1.3> 处,递归调用 #applyIncludes(...) 方法,继续替换。注意,此处是 <sql /> 对应的节点,并且 included 参数为 true 。详细的结果,见 😈😈😈 处。
    • <1.4> 处,将处理好的 <sql /> 节点,替换掉 <include /> 节点。逻辑有丢丢绕,胖友耐心看下注释,好好思考。
  • 😈😈😈 在 <sql /> 节点,会进入 <2> 这块逻辑:
    • <2.1> 处,因为 includedtrue ,所以能满足这块逻辑,会进行执行。如 <sql id="123" lang="${cpu}"> 的情况,lang 属性是可以被替换的。
    • <2.2> 处,遍历子节点,递归调用 #applyIncludes(...) 方法,继续替换。如图所示:子节点
      • 子节点 [0] ,执行该方法时,满足 <3> 的情况,所以可以使用变量 Properteis 对象,进行替换,并修改原节点。

其实,整理一下,逻辑也不会很绕。耐心耐心耐心。

3.3 findSqlFragment

#findSqlFragment(String refid, Properties variables) 方法,获得对应的 <sql /> 节点。代码如下:

// XMLIncludeTransformer.java

private Node findSqlFragment(String refid, Properties variables) {
// 因为 refid 可能是动态变量,所以进行替换
refid = PropertyParser.parse(refid, variables);
// 获得完整的 refid ,格式为 "${namespace}.${refid}"
refid = builderAssistant.applyCurrentNamespace(refid, true);
try {
// 获得对应的 <sql /> 节点
XNode nodeToInclude = configuration.getSqlFragments().get(refid);
// 获得 Node 节点,进行克隆
return nodeToInclude.getNode().cloneNode(true);
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
}
}

private String getStringAttribute(Node node, String name) {
return node.getAttributes().getNamedItem(name).getNodeValue();
}
  • 比较简单,胖友瞅瞅注释。

3.4 getVariablesContext

#getVariablesContext(Node node, Properties inheritedVariablesContext) 方法,获得包含 <include /> 标签内的属性 Properties 对象。代码如下:

// XMLIncludeTransformer.java

private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) {
// 获得 <include /> 标签的属性集合
Map<String, String> declaredProperties = null;
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
String name = getStringAttribute(n, "name");
// Replace variables inside
String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
if (declaredProperties == null) {
declaredProperties = new HashMap<>();
}
if (declaredProperties.put(name, value) != null) { // 如果重复定义,抛出异常
throw new BuilderException("Variable " + name + " defined twice in the same include definition");
}
}
}
// 如果 <include /> 标签内没有属性,直接使用 inheritedVariablesContext 即可
if (declaredProperties == null) {
return inheritedVariablesContext;
// 如果 <include /> 标签内有属性,则创建新的 newProperties 集合,将 inheritedVariablesContext + declaredProperties 合并
} else {
Properties newProperties = new Properties();
newProperties.putAll(inheritedVariablesContext);
newProperties.putAll(declaredProperties);
return newProperties;
}
}
  • 比较简单,胖友瞅瞅注释。
  • 如下是 <include /> 标签内有属性的示例:

    <sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

    <select id="selectUsers" resultType="map">
    select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
    from some_table t1
    cross join some_table t2
    </select>

4. MapperBuilderAssistant

4.1 addMappedStatement

#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) 方法,构建 MappedStatement 对象。代码如下:

// MapperBuilderAssistant.java

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) {

// <1> 如果只想的 Cache 未解析,抛出 IncompleteElementException 异常
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}

// <2> 获得 id 编号,格式为 `${namespace}.${id}`
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

// <3> 创建 MappedStatement.Builder 对象
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)) // <3.1> 获得 ResultMap 集合
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);

// <3.2> 获得 ParameterMap ,并设置到 MappedStatement.Builder 中
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}

// <4> 创建 MappedStatement 对象
MappedStatement statement = statementBuilder.build();
// <5> 添加到 configuration 中
configuration.addMappedStatement(statement);
return statement;
}
  • <1> 处,如果只想的 Cache 未解析,抛出 IncompleteElementException 异常。
  • <2> 处,获得 id 编号,格式为 ${namespace}.${id}
  • <3> 处,创建 MappedStatement.Builder 对象。详细解析,见 「4.1.3 MappedStatement」
  • <4> 处,创建 MappedStatement 对象。详细解析,见 「4.1.1 MappedStatement」
  • <5> 处,调用 Configuration#addMappedStatement(statement) 方法,添加到 configuration 中。代码如下:

    // Configuration.java

    /**
    * MappedStatement 映射
    *
    * KEY:`${namespace}.${id}`
    */
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");

    public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
    }

4.1.1 MappedStatement

org.apache.ibatis.mapping.MappedStatement ,映射的语句,每个 <select /><insert /><update /><delete /> 对应一个 MappedStatement 对象。代码比较简单,但是有点略长,胖友直接点击 链接 查看,已经添加了完整的注释。

另外,比较特殊的是,<selectKey /> 解析后,也会对应一个 MappedStatement 对象。

在另外,MappedStatement 有一个非常重要的方法 #getBoundSql(Object parameterObject) 方法,获得 BoundSql 对象。代码如下:

// MappedStatement.java

public BoundSql getBoundSql(Object parameterObject) {
// 获得 BoundSql 对象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 忽略,因为 <parameterMap /> 已经废弃,参见 http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html 文档
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}

// check for nested result maps in parameter mappings (issue #30)
// 判断传入的参数中,是否有内嵌的结果 ResultMap 。如果有,则修改 hasNestedResultMaps 为 true
// 存储过程相关,暂时无视
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}

return boundSql;
}

4.1.2 ParameterMap

org.apache.ibatis.mapping.ParameterMap ,参数集合,对应 paramType=""paramMap="" 标签属性。代码比较简单,但是有点略长,胖友直接点击 链接 查看,已经添加了完整的注释。

4.1.3 getStatementResultMaps

#getStatementResultMaps(...) 方法,获得 ResultMap 集合。代码如下:

// MapperBuilderAssistant.java

private List<ResultMap> getStatementResultMaps(
String resultMap,
Class<?> resultType,
String statementId) {
// 获得 resultMap 的编号
resultMap = applyCurrentNamespace(resultMap, true);

// 创建 ResultMap 集合
List<ResultMap> resultMaps = new ArrayList<>();
// 如果 resultMap 非空,则获得 resultMap 对应的 ResultMap 对象(们)
if (resultMap != null) {
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
try {
resultMaps.add(configuration.getResultMap(resultMapName.trim())); // 从 configuration 中获得
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find result map " + resultMapName, e);
}
}
// 如果 resultType 非空,则创建 ResultMap 对象
} else if (resultType != null) {
ResultMap inlineResultMap = new ResultMap.Builder(
configuration,
statementId + "-Inline",
resultType,
new ArrayList<>(),
null).build();
resultMaps.add(inlineResultMap);
}
return resultMaps;
}

4.1.4 getStatementResultMaps

#getStatementParameterMap(...) 方法,获得 ParameterMap 对象。代码如下:

// MapperBuilderAssistant.java

private ParameterMap getStatementParameterMap(
String parameterMapName,
Class<?> parameterTypeClass,
String statementId) {
// 获得 ParameterMap 的编号,格式为 `${namespace}.${parameterMapName}`
parameterMapName = applyCurrentNamespace(parameterMapName, true);
ParameterMap parameterMap = null;
// <2> 如果 parameterMapName 非空,则获得 parameterMapName 对应的 ParameterMap 对象
if (parameterMapName != null) {
try {
parameterMap = configuration.getParameterMap(parameterMapName);
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e);
}
// <1> 如果 parameterTypeClass 非空,则创建 ParameterMap 对象
} else if (parameterTypeClass != null) {
List<ParameterMapping> parameterMappings = new ArrayList<>();
parameterMap = new ParameterMap.Builder(
configuration,
statementId + "-Inline",
parameterTypeClass,
parameterMappings).build();
}
return parameterMap;
}
  • 主要看 <1> 处,如果 parameterTypeClass 非空,则创建 ParameterMap 对象。
  • 关于 <2> 处,MyBatis 官方不建议使用 parameterMap 的方式。

666. 彩蛋

相比 《精尽 MyBatis 源码分析 —— MyBatis 初始化(二)之加载 Mapper 映射配置文件》 来说,简单太多太多,可能就 XMLIncludeTransformer 相对绕一丢丢。总的来说,轻松蛮多。

总访客数 && 总访问量