概述
根据上一篇文章,我们了解了 Mybatis 如何在加载配置文件后,根据指定配置方式寻找接口并且完成映射文件信息与接口的绑定的。在本篇文章,我们将延续上文,结合源码阐述映射文件中方法声明里的 sql 被解析为 java 对象的过程。
这是关于 Mybatis 的第三篇文章,前文:
一、sql解析
1.XMLMapperBuilder
Sql 解析与 MappedStatement
的生成都在 XMLMapperBuilder
进行。根据上文可知,在 XMLMapperBuilder
的 parsePendingStatements()
方法如下:
1 | private void parsePendingStatements() { |
其中,Configuration
类中的IncompleteStatements
是在XMLMapperBuilder.parse()
时添加进去的,我们可以理解他是一个刚从配置文件中根据<select><delete><update><insert>
标签名拿到的 XML 节点,还没有做任何解析。
等到完成了接口与映射文件信息的绑定以后,再遍映射文件中的方法声明依次解析。
2.parseStatementNode方法
这个方法比较长,但是主要作用就是解析一个方法声明中的属性与子标签:
1 | public void parseStatementNode() { |
3.SqlSource
仍然还是在上文的 parseStatementNode()
方法中,暂且忽略缓存与各种返回值类型的处理,我们关注一下addMappedStatement()
中传入的变量 sqlSource
,它本身是一个接口,跟DataSource
的功能一样,通过SqlSource
接口的实现类,可以获取 sql 对象:
1 | public interface SqlSource { |
他具有的唯一一个抽象方法即为getBoundSql()
,这个方法获取的 BoundSql
类就是实际上的我们认为的方法声明中的那个 sql 对象:
1 | public class BoundSql { |
里面包含有带占位符和动态标签的原始 sql 语句,以及方法声明上相关的入参。
4.LanguageDriver
LanguageDriver 本身也是一个接口,他的实现类用于解析参数以及注解和映射文件中的 sql,并最终根据此生成 sql 数据源,我们可以简单的理解为针对指定格式 sql 语句的解析器:
1 | public interface LanguageDriver { |
SqlSource
通过 LanguageDriver.createSqlSource()
创建:
1 | String lang = context.getStringAttribute("lang"); |
而这里的getLanguageDriver()
方法,最终会回到Configuration.getLanguageDriver()
中:
1 | if (langClass == null) { |
而映射文件里方法声明中 lang
属性我们一般不会刻意设置,按上述逻辑,获取的是默认的语言驱动,这里的语言驱动来自于 Configuration
初始化时候注册的 XMLLanguageDriver
和 RawLanguageDriver
:
1 | public Configuration() { |
5.XMLLanguageDriver
XMLLanguageDriver
是LanguageDriver
接口的实现,它用于解析 xml 格式的 sql 语句——或者更准确点说,是 mybatis 特定的方法声明语法。
RawLanguageDriver
继承了XMLLanguageDriver
,而XMLLanguageDriver
又实现了LanguageDriver
接口:
1 | public class XMLLanguageDriver implements LanguageDriver { |
基于XMLLanguageDriver
,RawLanguageDriver
进一步完善了方法:
1 | public class RawLanguageDriver extends XMLLanguageDriver { |
主要是再做检查,防止非静态的 sql 使用 RawSqlSource。
对于 Myabtis 来说,sql 中带有 ${}
和<where>
这类动态标签的都认为是动态 sql,反之则是静态 sql
7.XMLScriptBuilder
XMLScriptBuilder
这个类用于对 Mybatis 的映射文件中的方法声明里的动态标签做解析。它的内部有一个 NodeHandle
接口实现类集合,用于处理专门的动态标签:
1 | private void initNodeHandlerMap() { |
8.SqlNode
parseDynamicTags()
最终返回的 MixedSqlNode
是一个 SqlNode 实现类,SqlNode 是一个表示节点的特殊接口:
1 | public interface SqlNode { |
他的方法及其简单,每个 SqlNode 实现类都会实现apply()
方法,当调用以后,sql 节点会被解析为正常的 sql 语句放入 DynamicContext
上下文中。
所有方法声明中的 sql 节点都实现类这个接口:
简而言之,他们的实现类很多,但是实际上就分四类:
- StaticTextSqlNode:顾名思义,普通的静态 sql 语句部分,只带有
#{}
表达式而不含有动态标签和${}
赋值表达式; - TextSqlNode:带有
${}
赋值表达式的 sql 语句; - 动态标签节点:除了
TextSqlNode
、MixedSqlNode
和StaticTextSqlNode
的所有其他实现类,即处理 Mybatis 动态标签的特殊节点; - MixedSqlNode:如
parseDynamicTags()
所示,这是一个混合了所有类型标签的节点,也是实际上的根节点;
我们以 StaticTextSqlNode
为例:
1 | public class StaticTextSqlNode implements SqlNode { |
普通静态 sql 节点 apply()
以后就把当前的节点内的 sql 拼接到上下文的 sql 中,同理,其他的实现类也差不多。由于节点之间还有嵌套关系,因此有些节点还会自己维护一个独立的上下文,内部处理的时候先把 sql 拼到独立上下文里面,等自己的节点处理完再把独立上下文中的 sql 拼接到父目录的上下文中。
根据各自的逻辑处理后,最终都会把节点代表的 sql 拼接到上下文里,最终所有节点逻辑处理完,就会在上下文中拼接处一条完整的 sql。
而起到这个作用的,就是根节点 MixedSqlNode
:
1 | public class MixedSqlNode implements SqlNode { |
MixedSqlNode
内部有所有的 SqlNode
,它的 apply()
方法就是遍历节点然后把每个节点的 apply()
方法都执行一遍。
这里另外提到一点,对于动态标签里的语法,比如 <if test="...">
里的 test,这里的表达式实际上是 Ognl 表达式,这在 jsp 中也有使用,我们可以简单理解为一个脚本语言,通过表达式,我们可以实现一些简单的功能,比如取值或者比较等等。
8.RawSqlSource与DynamicSqlSource
XMLLanguageDriver.createSqlSource()
最终会根据textSqlNode.isDynamic()
的结果——也就是方法声明的 sql 中是否含有动态标签——区分要创建哪一种 SqlSource
,带${}
和动态标签的用DynamicSqlSource
,不带的用 RawSqlSource
。
我们以比较简单的 RawSqlSource
为例:
1 | public class RawSqlSource implements SqlSource { |
可以看到,getSql()
本质上就是拿到处理好的 sql 节点,然后调用他们的 apply()
方法,最终拼接到上下文中,然后再返回这个拼好的 sql。
1 | public class DynamicSqlSource implements SqlSource { |
DynamicSqlSource
和RawSqlSource
没有什么区别,只不过由于存在动态标签与${}
,相比简单的静态 sql,需要先将这两者解析为 sql。
9.SqlSourceBuilder
SqlSourceBuilder.getBoundSql()
的主要作用就是将#{}
占位符替换为 jdbc 中的?
占位符:
1 | public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { |
所有的 sql 最后都会完成解析,变为最终的自带?占位符的静态 sql 字符串,sql 与对应的入参、返回值类型等信息最终被封装为一个对象,这个对象就是 BoundSql
:
1 | public class BoundSql { |
里面包含有带占位符和动态标签的原始 sql 语句,以及方法声明上相关的入参。
然后我们再回头看看DynamicSqlSource
,我们知道由于存在动态标签,因此DynamicSqlSource
会在创建了StaticSqlSource
之后,通过StaticSqlSource
拿到仍然带有动态标签数据的 BoundSql
,接着在根据入参处理动态标签,最终再生成一个 BoundSql
。
二、总结
我们总结一下 Mapper 中方法声明被变成 sql 加载进 Mybatis 的过程:
-
方法声明的解析发生在一个 Mapper 文件被加载——也就是
XMLMapperBuilder
构建——的时候,此时各个 statement 都还只是未被解析的 XML 节点; -
在
XMLMapperBuilder
中,先通过parsePendingStatements()
遍历节点,然后依次调用节点的parseStatementNode()
方法,在这一步主要一下几件事:- 获取 id、超时时间与缓存等相关的配置信息;
- 获取方法入参与返回值类型;
- 解析
<include>
节点,并替换为相应的内容; - 解析
<selectKey>
节点,根据配置设置相应的主键生成策略,完成后然后删除节点; - 解析sql,根据配置的
LanguageDriver
将 sql 解析为对应的SqlSource
; - 获取
StatementTyoe
,默认都为prepared
;
然后根据以上的信息构建一个
MappedStatement
对象,这个MappedStatement
即是后续方法实现的基础。
然后回头再看看步骤二,步骤二干了很多事,但是最重要的在于解析 statement 语法生成 SqlSource
:
-
获取
LanguageDriver
接口实现类,该实现类为来在配置文件加载时 Mybatis 默认提供的XMLLanguageDriver
,主要用来解析 Myabtis 特有的带有动态标签与参数标签的语法; -
在
LanguageDriver
中创建XMLScriptBuilder
用于解析 statement 中的语法,这里如果是类似@Select
这样注解形式的sql,就先解析为 xml 再用XMLScriptBuilder
; -
在
XMLScriptBuilder
中将 statement 语法也解析为一串的 XML 节点,并根据节点对应的语法做了区分:- 如果是带有
${}
表达式的字符串节点被认为是动态 sql 节点,转为TextSqlNode
, - 如果是只带有
#{}
表达式的字符串节点或者干脆就是纯字符串的字符串节点,转为StaticTextSqlNode
, - 如果是动态标签,就通过节点解析器
NodeHandler
处理,最后转为类似WhereSqlNode
这样的动态标签节点;
- 如果是带有
-
还是在
XMLScriptBuilder
中,解析出来的各种 Node 都是SqlNode
接口的实现类,他们都有apply()
方法,当调用的时候,会把自身对应的数据解析为 sql 并拼接到一个上下文对象的 sql 字符串中。最终这些节点都被添加到一个集合中,最终放入一个根节点
MixedSqlNode
里,当调用MixedSqlNode
的apply()
方法时,就会调用所有节点的apply()
方法民,最终就会得到一个完整的 sql。 -
回到
LanguageDriver
中,在它的createSqlSource()
方法中,根据是否带有${}
与动态标签区分为DynamicSqlSource
与RawSqlSource
两种DataSource
。RawSqlSource
:调用MixedSqlNode.apply()
拿到完整的 sql,然后把#{}
表达式替换为?
占位符,接着根据 sql 创建一个StaticSqlSource
,然后通过StaticSqlSource
获取boundSql
。DynamicSqlSource
:跟RawSqlSource
一样,先获得完整的 sql,并生成一个StaticSqlSource
对象,然后通过StaticSqlSource
拿到boundSql
,接着再根据传入的参数处理BoundSql
中的动态标签,最终再生成一个BoundSql
。