Mybatis中SqlSource解析流程详解
前面几篇文章都在详细分析mapper的加载过程,但是始终没有看到sql的解析过程,今天来详细分析下。
解析sql的位置
前面分析到不管是通过注解还是通过xml方式生成mapper,最终都是调用MapperBuilderAssistant类的addMappedStatement方法,这个方法接受的其中一个SqlSource参数,SqlSource类中就是XML文件或者注解方法中映射语句的实现。
那么SqlSource对象是在哪里创建的呢?
在通过注解实现mapper的流程中是在MapperAnnotationBuilder类的parseStatement方法中对SqlSource进行初始化,初始化代码如下图:
通过xml文件实现mapper的流程中是在XMLStatementBuilder类的parseStatementNode方法中对SqlSource进行初始化,初始化代码如下图:
可以看到创建SqlSource对象都是通过LanguageDriver实现的,翻译过来叫做语言驱动,它是一个接口,通过上面源码可以看出来我们可以自己实现这个接口,并且可以指定使用哪个语言驱动。今天我们只关注mybatis自带的一个实现XMLLanguageDriver。
XMLLanguageDriver介绍
XMLLanguageDriver类有重载了两个createSqlSource方法,主要区别在于第二个参数script,从前面两张源码图中可以知道接受XNode类型的script是在解析xml时使用,接受String类型的script是在解析注解时使用,今天只解析接受XNode类型的方法。
这个方法比较简单只有两步:初始化一个XMLScriptBuilder对象,执行XMLScriptBuilder对象的parseScriptNode方法。所以重点来到XMLScriptBuilder这个类。
XMLScriptBuilder详解
XMLScriptBuilder类部分源码如下图:
XMLScriptBuilder的初始化比较简单,要记住XNode context对应的是xml中的一个select、update等节点,在最后调用了initNodeHandlerMap方法设置了select、update等节点子节点对应的处理器。
接着是parseScriptNode方法,可以看到parseScriptNode方法调用了parseDynamicTags方法生成了一个MixedSqlNode对象,然后根据属性isDynamic判断创建对应的SqlSource对象。
所以最终要看parseScriptNode方法,同时可以判断isDynamic这个属性肯定也会在这个方法中发生变化,parseScriptNode方法的源码如下图:
parseDynamicTags方法解析节点下面所有子节点进行遍历,如果节点是文本节点这解析里面的内容生成SqlNode(这里是TextSqlNode或者StaticTextSqlNode)对象放到contents集合中。
如果是脚本节点比如where、if等就调用初始化时保存的节点处理器,如上图的ForEachHandler、IfHandler处理器,这些对象的处理方法handleNode的第一行代码又在调用parseDynamicTags方法,就像是一种递归。所以我们可以得出xml中的where、if、foreach这些节点时可以彼此包含的,解析时再进行递归解析。
当然要想调用parseDynamicTags方法,这些对象都是属于当前类XMLScriptBuilder的内部类。
大的方向梳理了我们再来看这个方法到底在干什么,首先这个方法会收集SqlNode对象放到contents集合中,最后把contents作为参数生成MixedSqlNode对象。在处理的过程中如果遇到if、foreach等节点还会把contents传递进去,从上面的图中可以看到ForEachHandler、IfHandler处理器也会调用parseDynamicTags方法生成MixedSqlNode然后再生成对应的SqlNode放到contents中。
所以最终来到两个关键类MixedSqlNode、SqlNode,当然SqlNode肯定有各种子类。
MixedSqlNode与SqlNode
那么MixedSqlNode与SqlNode是什么样子的呢?又是如何组合的?具体源码如下图:
可以看到SqlNode是一个接口,而MixedSqlNode只是他的一种实现类,同时来贴出来了静态文本处理的类和if节点对应的IfSqlNode类,还有一些其他比如WhereSqlNode、ForEachSqlNode等就不再列出来了。
每一种实现类的初始化都比较简单,比如StaticTextSqlNode是保存一段文本,IfSqlNode保存了if节点的test属性对应的值和从if节点下解析出来的MixedSqlNode节点。
而他们有一个由SqlNode规定的apply方法,这才是他们正真的作用所在,比如MixedSqlNode是遍历所有节点执行对应的apply方法,StaticTextSqlNode就只是把对应sql拼接到后面,IfSqlNode是在进行判断后调用MixedSqlNode去执行if节点下所有的节点。
至于参数DynamicContext后面在调用的时候会分析的。
现在回到XMLScriptBuilder的parseScriptNode方法,方法在执行了parseDynamicTags方法后根据isDynamic属性初始化了SqlSource,对应类结构图如下图:
我们主要关注DynamicSqlSource这个类,可以看到它就两个属性configuration、rootSqlNode,分别是全局配置和刚刚分析的MixedSqlNode,也可以从他的getBoundSql方法中看到后面对rootSqlNode的使用,这个留着后面分析。
总结
在configuration中有一个map属性mappedStatements,他保存着MappedStatement对象,每个MappedStatement对象对应一个sql的所有信息,而MappedStatement也有一个属性SqlSource,通过SqlSource能够获取到对应的sql,而sql的解析时依靠SqlNode。
今天的重点就在于生成SqlNode的过程,它是通过XMLScriptBuilder类是处理xml中crud节点生成。
这里有意思的是设计时是XMLScriptBuilder自带处理节点的方法parseDynamicTags生成需要的MixedSqlNode,而在parseDynamicTags方法内部可能会调用内部类WhereHandler、IfHandler的handleNode方法生成对应的SqlNode,而在这些handleNode方法中第一步就是调用parseDynamicTags去生成MixedSqlNode,根据MixedSqlNode生成对应的SqlNode。通过这种递归实现了节点多层嵌套的解析。
第二个有意思的点在于SqlNode的框架设计,MixedSqlNode存储一个SqlNode的集合,而具体的比如IfSqlNode又可以持有SqlNode,通过这样实现,每种SqlNode都可以拥有所有各种的SqlNode功能,但是他们有拥有自己独立的特点。
正是XMLScriptBuilder和SqlNode这种灵活的设计才可以使xml标签有非常强大的支持,同时解析的时候又不至于太复杂。
Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!