分析映射文件中剩余的几个节点,分别是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); }
代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以 上代码做的事情如下。
解析include节点
解析selectKey节点
解析SQL,获取SqlSource
构建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都是文本节点,调用过程一致。子节点作为参数进行递归调用。
子节点 2 的调用过程
解析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)); }
步骤如下:
创建 SqlSource 实例
构建并缓存 MappedStatement 实例
构建并缓存 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 类。
这些 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。
到此,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; }