代码执行至此,基本就结束了对Configuration对象的构建,MyBatis的第一阶段:构造,也就到这里结束了,现在再来回答我们在文章开头提出的那两个问题:MyBatis需要构造什么对象?以及是否两个配置文件对应着两个对象?,似乎就已经有了答案,这里做一个总结:
MyBatis需要对配置文件进行解析,最终会解析成一个Configuration对象,但是要说两个配置文件对应了两个对象实际上也没有错:
- Configuration对象,保存了mybatis-config.xml的配置信息。
- MappedStatement,保存了XXXMapper.xml的配置信息。
但是最终MappedStatement对象会封装到Configuration对象中,合二为一,成为一个单独的对象,也就是Configuration。
最后给大家画一个构建过程的流程图:
填坑
SQL语句在哪解析?
细心的同学可能已经发现了,上文中只说了去节点中获取一些属性从而构建配置对象,但是最重要的SQL语句并没有提到,这是因为这部分我想要和属性区分开单独说,由于MyBatis支持动态SQL和${}
、#{}
的多样的SQL,所以这里单独提出来说会比较合适。
首先可以确认的是,刚才我们走完的那一整个流程中,包含了SQL语句的生成,下面贴代码(这一段代码相当绕,不好读)。
//解析Sql(重要) 根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
这里就是生成Sql的入口,以单步调试的角度接着往下看。
/*进入createSqlSource方法*/ @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { //进入这个构造 XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); //进入parseScriptNode return builder.parseScriptNode(); } /** 进入这个方法 */ public SqlSource parseScriptNode() { //# //会先解析一遍 MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { //如果是${}会直接不解析,等待执行的时候直接赋值 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { //用占位符方式来解析 #{} --> ? sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } protected MixedSqlNode parseDynamicTags(XNode node) { List<SqlNode> contents = new ArrayList<>(); //获取select标签下的子标签 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) { //如果是查询 //获取原生SQL语句 这里是 select * from test where id = #{id} String data = child.getStringBody(""); TextSqlNode textSqlNode = new TextSqlNode(data); //检查sql是否是${} if (textSqlNode.isDynamic()) { //如果是${}那么直接不解析 contents.add(textSqlNode); isDynamic = true; } else { //如果不是,则直接生成静态SQL //#{} -> ? contents.add(new StaticTextSqlNode(data)); } } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 //如果是增删改 String nodeName = child.getNode().getNodeName(); NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null) { throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); } handler.handleNode(child, contents); isDynamic = true; } } return new MixedSqlNode(contents); }
/*从上面的代码段到这一段中间需要经过很多代码,就不一段一段贴了*/ public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); //这里会生成一个GenericTokenParser,传入#{}作为开始和结束,然后调用其parse方法,即可将#{}换为 ? GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); //这里可以解析#{} 将其替换为? String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); } //经过一段复杂的解析过程 public String parse(String text) { if (text == null || text.isEmpty()) { return ""; } // search open token int start = text.indexOf(openToken); if (start == -1) { return text; } char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; //遍历里面所有的#{} select ? ,#{id1} ${} while (start > -1) { if (start > 0 && src[start - 1] == '\\') { // this open token is escaped. remove the backslash and continue. builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { // found open token. let's search close token. if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) { if (end > offset && src[end - 1] == '\\') { // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else { expression.append(src, offset, end - offset); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { //使用占位符 ? //注意handler.handleToken()方法,这个方法是核心 builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } //BindingTokenParser 的handleToken //当扫描到${}的时候调用此方法 其实就是不解析 在运行时候在替换成具体的值 @Override public String handleToken(String content) { this.isDynamic = true; return null; } //ParameterMappingTokenHandler的handleToken //全局扫描#{id} 字符串之后 会把里面所有 #{} 调用handleToken 替换为? @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; }
这段代码相当绕,我们应该站在一个宏观的角度去看待它。所以我直接在这里概括一下:
首先这里会通过<select>
节点获取到我们的SQL语句,假设SQL语句中只有${}
,那么直接就什么都不做,在运行的时候直接进行赋值。
而如果扫描到了#{}
字符串之后,会进行替换,将#{}
替换为 ?
。
那么他是怎么进行判断的呢?
这里会生成一个GenericTokenParser,这个对象可以传入一个openToken和closeToken,如果是#{}
,那么openToken就是#{
,closeToken就是 }
,然后通过parse方法中的handler.handleToken()
方法进行替换。
在这之前由于已经进行过SQL是否含有#{}
的判断了,所以在这里如果是只有${}
,那么handler就是BindingTokenParser的实例化对象,如果存在#{}
,那么handler就是ParameterMappingTokenHandler的实例化对象。
分别进行处理。
上文中提到的解析不了的节点是什么意思?
根据上文的代码我们可知,解析Mapper.xml文件中的每个节点是有顺序的。
那么假设我写了这么一个几个节点:
<select id="demoselect" paramterType='java.lang.Integer' resultMap='demoResultMap'> </select> <resultMap id="demoResultMap" type="demo"> <id column property> <result coulmn property> </resultMap>
select节点是需要获取resultMap的,但是此时resultMap并没有被解析到,所以解析到<select>
这个节点的时候是无法获取到resultMap的信息的。
我们来看看MyBatis是怎么做的:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //解析xml节点 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { //xml语句有问题时 存储到集合中 等解析完能解析的再重新解析 configuration.addIncompleteStatement(statementParser); } } }
当解析到某个节点出现问题的时候,会抛一个异常,然后会调用configuration的addIncompleteStatement方法,将这个解析对象先暂存到这个集合中,等到所有的节点都解析完毕之后,在对这个集合内的解析对象继续解析:
public void parse() { //判断文件是否之前解析过 if (!configuration.isResourceLoaded(resource)) { //解析mapper文件 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //绑定Namespace里面的Class对象 bindMapperForNamespace(); } //重新解析之前解析不了的节点 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } private void parsePendingResultMaps() { Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps(); synchronized (incompleteResultMaps) { Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator(); while (iter.hasNext()) { try { //添加resultMap iter.next().resolve(); iter.remove(); } catch (IncompleteElementException e) { // ResultMap is still missing a resource... } } } } public ResultMap resolve() { //添加resultMap return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); }
结语
至此整个MyBatis的查询前构建的过程就基本说完了,简单地总结就是,MyBatis会在执行查询之前,对配置文件进行解析成配置对象:Configuration,以便在后面执行的时候去使用,而存放SQL的xml又会解析成MappedStatement对象,但是最终这个对象也会加入Configuration中。
至于Configuration是如何被使用的,以及SQL的执行部分,我会在下一篇说SQL执行的时候分享。
END