欢迎关注公众号 【11来了】 ,持续 MyBatis 源码系列内容!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
MyBatis 源码系列文章:
(一)MyBatis 源码如何学习?
(二)MyBatis 运行原理 - 读取 xml 配置文件
(三)MyBatis 运行原理 - MyBatis 的核心类 SqlSessionFactory 和 SqlSession
(四)MyBatis 运行原理 - MyBatis 中的代理模式
(五)MyBatis 运行原理 - 数据库操作最终由哪些类负责?
执行 Mapper 接口的方法时,MyBatis 怎么知道执行的哪个 SQL?
功能拆解中的示例代码同样使用下边开源项目的示例 3 的代码
参考源码示例:https://github.com/yeecode/MyBatisDemo
为了阅读时可以更清晰本节的重点,会先将内容总结放在前边:
本节主要讲 MyBatis 如何解析 UserMapper.xml 中的 SQL 语句,并且在执行数据库操作时如何关联上对应的 SQL 信息。
- 如何解析 UserMapper.xml 的 SQL 语句
MyBatis 会先解析 UserMapper.xml 文件内部的 SQL 语句,即: insert|delete|update|select
标签
解析完之后,会将标签对应的 SQL 信息包装为 MappedStatement 存储在 Configuration 的 Map 中,Map 中的 key 就是 UserMapper 的全限定类名 + 方法名
- 如何关联对应的 SQL 信息
在真正执行 UserMapper 接口的方法时,会在 MapperProxy 拦截器中去执行真正的数据库操作,此时再根据 UserMapper 的全限定类名 + 方法名
去获取对应的 MappedStatement
接下来,正文开始
如何解析 xml 文件的 SQL 语句
之前讲了 MyBatis 的整体执行流程,使用 MyBatis 时需要先读取 mybtis-config.xml
配置文件,底层会去解析 <configuration>
标签下的内容,入口方法如下:
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
// 1、读取 xml 配置文件
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 2、得到 SqlSessionFactory
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
解析 xml 配置文件的地方就在 SqlSessionFactoryBuilder().build()
方法中,内部通过 XMLConfigBuilder 的 parse() 方法去解析,那么接下来直接走到内部关键方法,方法入参的就是 <configuration>
节点下的所有内容:
// MyBatis 源码 builder 包下的 XMLConfigBuilder 类
private void parseConfiguration(XNode root) {
try { // ... 省略部分代码
// 解析 mappers 标签的内容
mapperElement(root.evalNode("mappers"));
}
}
方法内部是解析 <configuration>
节点下各种标签里的内容,这里我们只关注对 <mappers>
标签的解析,该标签下的内容如下:
接下来走进 mapperElement()
方法:
// MyBatis 源码 builder 包下的 XMLConfigBuilder 类
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历 <mappers> 标签内部的每个 <mapper> 标签,也就是每个 xml 文件
for (XNode child : parent.getChildren()) {
// 1、获取 resource 属性值,也就是 UserMapper.xml 文件的位置
String resource = child.getStringAttribute("resource");
if (resource != null && /*省略其他的条件*/) {
// 2、将 UserMapper.xml 读取为 InputStream
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 3、使用 XMLMapperBuilder 解析 UserMapper.xml 文件内容
mapperParser.parse();
// ...
}
}
}
}
在这里就会读取 UserMapper.xml 文件,并且也通过专门的类 XMLMaperBuilder
去解析他内部的内容
// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 核心方法:解析 UserMaper.xml 中的 <mapper> 标签
configurationElement(parser.evalNode("/mapper"));
// ...
}
// ...
}
在 configurationElement()
方法内部,会去解析 <mapper>
标签内部的内容,也就是下图中 select 标签内部的 sql:
进入 configurationElement()
方法内部:
// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
private void configurationElement(XNode context) {
// ...
// 解析增删改查标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
这里的 context 就是就是 UserMapper.xml 中的 <mapper>
标签内部的内容,进入 buildStatementFromContext()
方法:
// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
private void buildStatementFromContext(List<XNode> list) {
// ...
buildStatementFromContext(list, null);
}
// MyBatis 源码 builder 包下的 XMLMapperBuilder 类
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 遍历 <mapper> 标签下的所有增删改查标签节点
for (XNode context : list) {
// 1、创建解析语句的处理类 XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 2、解析对应语句
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
可以看到最终 <select>
标签的解析工作还是交给了对应的处理器 XMLStatementBuilder
来完成,并且解析式所需要的参数,在创建时直接就传入了,在该类的 parseStatementNode()
方法中,真正去解析 <select>
标签的内容,并且将解析到的内容存储下来:
// MyBatis 源码 builder 包下的 XMLStatementBuilder 类
public void parseStatementNode() {
// ...
// 这里 id 是执行 UserMapper 中的方法名:queryUserBySchoolName
String id = context.getStringAttribute("id");
// nodeName 就是 select 标签的名字,即:select
String nodeName = context.getNode().getNodeName();
// 判断语句类型,查询时 sqlCommandType = SELECT
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 解析返回值类型,这里为:com.github.yeecode.mybatisdemo.User
String resultType = context.getStringAttribute("resultType");
// 下边的入参是从 <select> 标签内部解析出来的数据
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
上边经过一系列解析操作,这里省略了很多,最终解析出来的参数都会通过 MapperBuilderAssistant
助手来构造 MappedStatement 并进行存储,如下:
// MyBatis 源码 builder 包下的 MapperBuilderAssistant 类
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, /*参数过多省略*/) {
// 将 id 拼接上 namespace,即:com.github.yeecode.mybatisdemo.UserMapper + queryUserBySchoolName
id = applyCurrentNamespace(id, false);
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
// ... builde 的属性太多省略
;
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
// 创建 MappedStatement
MappedStatement statement = statementBuilder.build();
// 将 MappedStatement 放入到 Configuration 的 Map 中
// 在 Map 中存储的 Key 就是 namespace + id
configuration.addMappedStatement(statement);
return statement;
}
在这里通过 MappedStatement 里边的 Builder 来构建 MappedStatement 对象,最终 MappedStatement 对象就被放入到 Configuration 中去,UserMapper.xml 中的 <select>
标签解析到 Configuration 的内容如下图:
如何关联对应的 SQL 信息
在执行 SQL 的时候,如何找到该 SQL 对应的 MappedStatement 呢?
首先,在之前的文章 MyBatis 运行原理中已经说了,当执行 UserMapper 方法时,会进入到 MapperProxy 拦截器中,最终会走到 DefaultSqlSession 对应的查询方法: getList()
// MyBatis 源码 session 包下的 DefaultSqlSession 类
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 根据 statement 获取对应的 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
// ...
}
这里根据 statement 作为 key 去 Configuration 中获取对应的 MappedStatement
statement 的值就是 UserMapper 的全限定类名 + 接口的方法名
:com.github.yeecode.mybatisdemo.UserMaper.queryUserBySchoolName
获取到 MappedStatement 之后,就交给执行器 Executor 去执行对应的 SQL 语句了,MappedStatement 的部分内容如下:
- sqlSource:存储了
<select>
标签内部的动态 SQL - id:唯一表示,UserMapper 的接口名 + 方法名
- resource:对应的 xml 文件
- …
总结
最后再总结一下,学完本节可以收获什么?
- MyBatis 内部是如何对 SQL 语句的信息进行解析和存储?
- MyBatis 针对这个功能设计了哪些类?每个类的职责如何划分?
MyBatis 是一个 ORM 框架,作为后端应用和数据库之间的 桥梁 ,目的是帮助研发人员管理和执行 SQL 语句,将 SQL 语句定义在 xml 文件之后,MyBatis 肯定需要存储起来,并且在执行 UserMapper 接口对应的方法时,可以找到对应的 SQL 语句以及对应的一些信息,包括参数类型、返回值类型等等
MyBatis 就是通过 MappedStatement 这个对象来存储的,通过唯一标识 UserMapper 接口全限定类名 + 方法名
来确定唯一的 MappedStatement
创建 MappedStatement 的地方是在解析 xml 文件时完成的,主要涉及三个类:
- XMLConfigBuilder: 解析 mybatis-config.xml 文件
- XMLMapperBuilder: 解析 UserMapper.xml 文件中
<mapper>
标签里的内容 - XMLStatementBuilder: 解析 UserMapper.xml 文件中
<mapper>
标签内增删改查标签的内容,如<select> | <update> ...
可以看到在解析的过程中,通过一层一层的职责拆分,不断将每个类的职责进行细化,避免一个类负责多种类型的任务
解析之后,将 MappedStatement 放入到全局配置类 Configuration 的 Map 中,当执行 Mapper 接口中的方法时,通过 【Mapper 接口的全限定类名 + 方法名】作为唯一标识来获取对应的 MappedStatement,就可以获取该接口对应 SQL 的一些信息了
整体流程如下: