精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下)
摘自:https://www.cnblogs.com/lifullmoon/p/14015075.html
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址、Mybatis-Spring 源码分析 GitHub 地址、Spring-Boot-Starter 源码分析 GitHub 地址)进行阅读
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的初始化
在MyBatis初始化过程中,大致会有以下几个步骤:
-
创建
Configuration
全局配置对象,会往TypeAliasRegistry
别名注册中心添加Mybatis需要用到的相关类,并设置默认的语言驱动类为XMLLanguageDriver
-
加载
mybatis-config.xml
配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息会形成相应的对象并保存到Configuration全局配置对象中 -
构建
DefaultSqlSessionFactory
对象,通过它可以创建DefaultSqlSession
对象,MyBatis中SqlSession
的默认实现类
因为整个初始化过程涉及到的代码比较多,所以拆分成了四个模块依次对MyBatis的初始化进行分析:
- 《MyBatis初始化(一)之加载mybatis-config.xml》
- 《MyBatis初始化(二)之加载Mapper接口与XML映射文件》
- 《MyBatis初始化(三)之SQL初始化(上)》
- 《MyBatis初始化(四)之SQL初始化(下)》
由于在MyBatis的初始化过程中去解析Mapper接口与XML映射文件涉及到的篇幅比较多,XML映射文件的解析过程也比较复杂,所以才分成了后面三个模块,逐步分析,这样便于理解
初始化(四)之SQL初始化(下)
在上一篇文档中详细地讲述了MyBatis在解析<select /> <insert /> <update /> <delete />
节点的过程中,是如何解析SQL语句的,如何实现动态SQL语句的,最终会生成一个org.apache.ibatis.mapping.SqlSource
对象的,那么接下来我们来看看SqlSource
到底是什么
主要包路径:org.apache.ibatis.mapping、org.apache.ibatis.builder
主要涉及到的类:
org.apache.ibatis.builder.SqlSourceBuilder
:继承了BaseBuilder抽象类,SqlSource
构建器,负责将SQL语句中的#{}
替换成相应的?
占位符,并获取该?
占位符对应的ParameterMapping
对象org.apache.ibatis.builder.ParameterExpression
:继承了HashMap<String, String>
,参数表达式处理器,在SqlSourceBuilder
处理#{}
的内容时,需要通过其解析成key-value键值对org.apache.ibatis.mapping.ParameterMapping
:保存#{}
中配置的属性参数信息org.apache.ibatis.mapping.SqlSource
:SQL 资源接口,用于创建BoundSql对象(包含可执行的SQL语句与参数信息)org.apache.ibatis.mapping.BoundSql
:用于数据库可执行的SQL语句的最终封装对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler
:实现了ParameterHandler接口,用于将入参设置到java.sql.PreparedStatement
预编译对象中
用于将入参设置到java.sql.PreparedStatement
预编译对象中
我们先来回顾一下org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
的parseScriptNode()
方法,将 SQL 脚本(XML或者注解中定义的 SQL )解析成 SqlSource
对象
代码如下:
- 如果是动态 SQL 语句,使用了 MyBatis 的自定义标签(
<if /> <foreach />
等)或者使用了${}
都是动态 SQL 语句,则会创建DynamicSqlSource
对象 - 否则就是静态 SQL 语句,创建
RawSqlSource
对象
SqlSource
接口的实现类如下图所示:
SqlSourceBuilder
org.apache.ibatis.builder.SqlSourceBuilder
:继承了BaseBuilder抽象类,SqlSource
构建器,负责将SQL语句中的#{}
替换成相应的?
占位符,并获取该?
占位符对应的 org.apache.ibatis.mapping.ParameterMapping
对象
构造方法
其中PARAMETER_PROPERTIES
字符串定义了#{}
中支持定义哪些属性,在抛异常的时候用到
parse方法
解析原始的SQL(仅包含#{}
定义的参数),转换成StaticSqlSource对象
因为在DynamicSqlSource
调用该方法前会将MixedSqlNode
进行处理,调用其apply
方法进行应用,根据DynamicContext
上下文对MyBatis的自定义标签或者包含${}
的SQL生成的SqlNode
进行逻辑处理或者注入值,生成一个SQL(仅包含#{}
定义的参数)
代码如下:
该方法的入参originalSql
为原始的SQL,也就是其所有的SqlNode节点已经应用了,也就是都调用了apply
方法
包含的${}
也已经注入了对应的值,所以这里只剩#{}
定义的入参了
- 创建
ParameterMappingTokenHandler
处理器对象handler
- 创建GenericTokenParser对象,用于处理
#{}
中的内容,通过handler
将其转换成?
占位符,并创建对应的ParameterMapping
对象 - 执行解析,获取最终的 SQL 语句
- 创建
StaticSqlSource
对象
ParameterMappingTokenHandler
org.apache.ibatis.builder.SqlSourceBuilder
的内部类,用于解析#{}
的内容,创建ParameterMapping
对象,并将其替换成?
占位符
代码如下:
构造方法:创建additionalParameters
对应的MetaObject对象,便于操作上下文的参数集合,包含附加参数集合(通过 <bind />
标签生成的,或者<foreach />
标签中的集合的元素)
handleToken(String content)
方法:
-
调用
buildParameterMapping(content)
方法,解析#{}
的内容创建ParameterMapping
对象 -
直接返回
?
占位符
buildParameterMapping(content)
方法:
- 将字符串解析成 key-value 键值对,通过
org.apache.ibatis.builder.ParameterExpression
进行解析,其中有一个key为"property",value就是对应的属性名称 - 获得属性的名字和类型
- 创建
ParameterMapping.Builder
构建者对象,设置参数的名称与Java Type- 将上面第
1
步解析到key-value键值对设置到Builder中 - 如果TypeHandler类型处理器的别名非空,则尝试获取其对应的类型处理器并设置到Builder中
- 通过Builder创建
ParameterMapping
对象,如果没有配置TypeHandler类型处理器,则根据参数Java Type和Jdbc Type从TypeHandlerRegistry
注册中心获取并赋值到该对象中
- 将上面第
ParameterExpression
org.apache.ibatis.builder.ParameterExpression
:继承了HashMap<String, String>
,参数表达式处理器,在ParameterMappingTokenHandler
处理#{}
的内容时需要通过其解析成key-value键值对
构造方法:
在构造函数中调用其parse(String expression)
方法
先出去前面的空格或者非法字符,然后调用property(String expression, int left)
方法
如果left
开始位置小于字符串的长度,那么开始解析
-
调用
skipUntil
方法,获取从left
开始,
或者:
第一个位置,也就是分隔符的位置 -
这里第一次进入的话就会先获取第一个
,
的位置,那么调用trimmedStr
方法截取前面的字符串,也就是属性名称,然后存放一个键值对(key为property,value为属性名称)
-
调用
jdbcTypeOpt(String expression, int p)
方法,继续解析后面的字符串,也就是该属性的相关配置
如果p
(第一个,
的位置)后面还有字符串
则调用option(String expression, int p)
方法将一个,
后面的字符串解析成key-value键值对保存
逐步解析,将字符串解析成key-value键值对保存,这里保存的都是属性的相关配置,例如JdbcType
配置
ParameterMapping
org.apache.ibatis.mapping.ParameterMapping
:保存#{}
中配置的属性参数信息,一个普通的实体类,代码如下:
SqlSource
org.apache.ibatis.mapping.SqlSource
:SQL 资源接口,用于创建BoundSql对象(包含可执行的SQL语句与参数信息),代码如下:
StaticSqlSource
org.apache.ibatis.builder.StaticSqlSource
:实现 SqlSource 接口,静态的 SqlSource 实现类,代码如下:
在SqlSourceBuilder
构建的SqlSource类型就是StaticSqlSource
,用于获取最终的静态 SQL 语句
RawSqlSource
org.apache.ibatis.scripting.defaults.RawSqlSource
:实现了SqlSource接口,静态SQL语句对应的SqlSource对象,用于创建静态 SQL 资源,代码如下:
在构造函数中我们可以看到,会先调用getSql
方法直接创建SqlSource
因为静态的 SQL 语句,不需要根据入参来进行逻辑上的判断处理,所以这里在构造函数中就先初始化好 SqlSource,后续需要调用Mapper接口执行SQL的时候就减少了一定的时间
getSql
方法:
- 创建一个上下文对象
DynamicContext
,入参信息为null - 调用
StaticTextSqlNode
的apply
方法,将所有的SQL拼接在一起 - 返回拼接好的SQL语句
构造方法:
- 创建SqlSourceBuilder构建对象
sqlSourceParser
- 调用
sqlSourceParser
的parse
方法对该SQL语句进行转换,#{}
全部替换成?
占位符,并创建对应的ParameterMapping
对象 - 第
2
步返回的StaticSqlSource
对象设置到自己的sqlSource
属性中
getBoundSql
方法:直接通过StaticSqlSource
创建BoundSql
对象
DynamicSqlSource
org.apache.ibatis.scripting.defaults.DynamicSqlSource
:实现了SqlSource接口,动态SQL语句对应的SqlSource对象,用于创建静态 SQL 资源,代码如下:
在构造函数中仅仅是赋值,不像RawSqlSource
的构造函数一样直接可创建对应的SqlSource对象,因为动态SQL语句需要根据入参信息,来解析SqlNode节点,所以这里在getBoundSql
方法中每次都会创建StaticSqlSource
对象
getBoundSql
方法:
- 创建本次解析的动态 SQL 语句的上下文,设置入参信息
- 根据上下文应用整个 SqlNode,内部包含的所有SqlNode都会被应用,最终解析后的SQL会保存上下文中
- 创建 SqlSourceBuilder 构建对象
sqlSourceParser
- 调用
sqlSourceParser
的parse
方法对第2
步解析后的SQL语句进行转换,#{}
全部替换成?
占位符,并创建对应的ParameterMapping
对象 - 通过第
4
步返回的StaticSqlSource
对象创建BoundSql
对象 - 添加附加参数到
BoundSql
对象中,因为上一步创建的BoundSql
对象时候传入的仅是入参信息,没有添加附加参数(通过<bind />
标签生成的,或者<foreach />
标签中的集合的元素)
BoundSql
org.apache.ibatis.mapping.BoundSql
:用于数据库可执行的SQL语句的最终封装对象,一个普通的实体类,代码如下:
DefaultParameterHandler
org.apache.ibatis.scripting.defaults.DefaultParameterHandler
:实现了ParameterHandler接口,默认实现类,仅提供这个实现类,用于将入参设置到java.sql.PreparedStatement
预编译对象中
回看到org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
语言驱动类中,实现了createParameterHandler
方法,返回的参数处理器就是该对象
代码如下:
往PreparedStatement
中设置参数的大致逻辑如下:
- 获取SQL的参数信息
ParameterMapping
对象的集合,然后对其遍历 - 如果参数的模式不为
ParameterMode.OUT
(默认为ParameterMode.IN
),也就是说需要作为入参,那么开始接下来的赋值 - 获取该参数对应的属性名称,并通过其获取到对应的值
- 获取到
TypeHandler
类型处理器(在ParameterMapping
构建的时候会创建对应的TypeHandler
) - 获取到Jdbc Type
- 通过
TypeHandler
类型处理器,根据参数位置和Jdbc Type将属性值设置到PreparedStatement
中
这样就完成对PreparedStatement
的赋值,然后通过它执行SQL语句
总结
在MyBatis初始化的过程中,会将XML映射文件中的<select /> <insert /> <update /> <delete />
节点解析成MappedStatement
对象,其中会将节点中定义的SQL语句通过XMLLanguageDriver
语言驱动类创建一个SqlSource
对象,本文就是对该对象进行分析
通过SqlSource
这个对象根据入参可以获取到对应的BoundSql
对象,BoundSql
对象中包含了数据库需要执行的SQL语句、ParameterMapping
参数信息、入参对象和附加的参数(通过<bind />
标签生成的,或者<foreach />
标签中的集合的元素等等)
好了,对于MyBatis的整个初始化过程我们已经全部分析完了,其中肯定有不对或者迷惑的地方,欢迎指正!!!感谢大家的阅读!!!😄😄😄
参考文章:芋道源码《精尽 MyBatis 源码分析》