1. 概述
本文接 《精尽 MyBatis 源码分析 —— MyBatis 初始化(二)之加载 Mapper 映射配置文件》 一文,来分享 MyBatis 初始化的第三步,加载 Statement 配置。而这个步骤的入口是 XMLStatementBuilder 。下面,我们一起来看看它的代码实现。
在 《精尽 MyBatis 源码分析 —— MyBatis 初始化(二)之加载 Mapper 映射配置文件》 的 「2.3.5 buildStatementFromContext」 中,我们已经看到对 XMLStatementBuilder 的调用代码。代码如下:
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); } } }
|
2. XMLStatementBuilder
org.apache.ibatis.builder.xml.XMLStatementBuilder ,继承 BaseBuilder 抽象类,Statement XML 配置构建器,主要负责解析 Statement 配置,即 <select />、<insert />、<update />、<delete /> 标签。
2.1 构造方法
private final MapperBuilderAssistant builderAssistant;
private final XNode context;
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 解析。代码如下:
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"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
String nodeName = context.getNode().getNodeName(); 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);
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode());
processSelectKeyNodes(id, parameterTypeClass, langDriver);
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; }
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 是否匹配。代码如下:
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { if (requiredDatabaseId != null) { return requiredDatabaseId.equals(databaseId); } else { if (databaseId != null) { return false; } id = builderAssistant.applyCurrentNamespace(id, false); if (this.configuration.hasStatement(id, false)) { MappedStatement previous = this.configuration.getMappedStatement(id, false); return previous.getDatabaseId() == null; } } return true; }
|
- 代码比较简单,胖友自己瞅瞅就得。从逻辑上,和我们在 XMLMapperBuilder 看到的同名方法
#databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) 方法是一致的。
2.4 getLanguageDriver
#getLanguageDriver(String lang) 方法,获得 lang 对应的 LanguageDriver 对象。代码如下:
private LanguageDriver getLanguageDriver(String lang) { Class<? extends LanguageDriver> langClass = null; if (lang != null) { langClass = resolveClass(lang); } return builderAssistant.getLanguageDriver(langClass); }
|
2.5 processSelectKeyNodes
#processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) 方法,解析 <selectKey /> 标签。代码如下:
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) { List<XNode> selectKeyNodes = context.evalNodes("selectKey"); if (configuration.getDatabaseId() != null) { parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId()); } parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null); removeSelectKeyNodes(selectKeyNodes); }
|
2.5.1 parseSelectKeyNodes
#parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) 方法,执行解析 <selectKey /> 子节点们。代码如下:
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) { for (XNode nodeToHandle : list) { String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX; String databaseId = nodeToHandle.getStringAttribute("databaseId"); if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) { 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 /> 节点。代码如下:
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 = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); SqlCommandType sqlCommandType = SqlCommandType.SELECT;
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
id = builderAssistant.applyCurrentNamespace(id, false); MappedStatement keyStatement = configuration.getMappedStatement(id, false); configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); }
|
org.apache.ibatis.builder.xml.XMLIncludeTransformer ,XML <include /> 标签的转换器,负责将 SQL 中的 <include /> 标签转换成对应的 <sql /> 的内容。
3.1 构造方法
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 /> 。代码如下:
public void applyIncludes(Node source) { Properties variablesContext = new Properties(); Properties configurationVariables = configuration.getVariables(); if (configurationVariables != null) { variablesContext.putAll(configurationVariables); } 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 /> 。代码如下:
private void applyIncludes(Node source, final Properties variablesContext, boolean included) { if (source.getNodeName().equals("include")) { Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); Properties toIncludeContext = getVariablesContext(source, variablesContext); applyIncludes(toInclude, toIncludeContext, true); if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { toInclude = source.getOwnerDocument().importNode(toInclude, true); } source.getParentNode().replaceChild(toInclude, source); while (toInclude.hasChildNodes()) { toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); } 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); 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()) { source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); } }
|
- 方法参数
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> 处,因为 included 为 true ,所以能满足这块逻辑,会进行执行。如 <sql id="123" lang="${cpu}"> 的情况,lang 属性是可以被替换的。
<2.2> 处,遍历子节点,递归调用 #applyIncludes(...) 方法,继续替换。如图所示:
子节点
- 子节点
[0] ,执行该方法时,满足 <3> 的情况,所以可以使用变量 Properteis 对象,进行替换,并修改原节点。
其实,整理一下,逻辑也不会很绕。耐心耐心耐心。
3.3 findSqlFragment
#findSqlFragment(String refid, Properties variables) 方法,获得对应的 <sql /> 节点。代码如下:
private Node findSqlFragment(String refid, Properties variables) { refid = PropertyParser.parse(refid, variables); refid = builderAssistant.applyCurrentNamespace(refid, true); try { XNode nodeToInclude = configuration.getSqlFragments().get(refid); 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 对象。代码如下:
private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) { 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"); 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"); } } } if (declaredProperties == null) { return inheritedVariablesContext; } else { Properties newProperties = new Properties(); newProperties.putAll(inheritedVariablesContext); newProperties.putAll(declaredProperties); return newProperties; } }
|
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 对象。代码如下:
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) .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(); configuration.addMappedStatement(statement); return statement; }
|
4.1.1 MappedStatement
org.apache.ibatis.mapping.MappedStatement ,映射的语句,每个 <select />、<insert />、<update />、<delete /> 对应一个 MappedStatement 对象。代码比较简单,但是有点略长,胖友直接点击 链接 查看,已经添加了完整的注释。
另外,比较特殊的是,<selectKey /> 解析后,也会对应一个 MappedStatement 对象。
在另外,MappedStatement 有一个非常重要的方法 #getBoundSql(Object parameterObject) 方法,获得 BoundSql 对象。代码如下:
public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) { boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); }
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 集合。代码如下:
private List<ResultMap> getStatementResultMaps( String resultMap, Class<?> resultType, String statementId) { resultMap = applyCurrentNamespace(resultMap, true);
List<ResultMap> resultMaps = new ArrayList<>(); if (resultMap != null) { String[] resultMapNames = resultMap.split(","); for (String resultMapName : resultMapNames) { try { resultMaps.add(configuration.getResultMap(resultMapName.trim())); } catch (IllegalArgumentException e) { throw new IncompleteElementException("Could not find result map " + resultMapName, e); } } } 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 对象。代码如下:
private ParameterMap getStatementParameterMap( String parameterMapName, Class<?> parameterTypeClass, String statementId) { parameterMapName = applyCurrentNamespace(parameterMapName, true); ParameterMap parameterMap = null; if (parameterMapName != null) { try { parameterMap = configuration.getParameterMap(parameterMapName); } catch (IllegalArgumentException e) { throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e); } } 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 相对绕一丢丢。总的来说,轻松蛮多。