如何使用三个步骤快速读懂 Mybatis 的核心源码

425 阅读7分钟

对于工作了一两年的同学,如果想深入源码提升竞争力,Mybatis 是最好的起点。Mybatis 代码量不大,设计简洁优雅,按照平时用到的功能,各个击破,可能几个小时就搞明白啦!

今天这篇文章,主要介绍 3 板斧,5 分钟搞懂 Mybatis 源码的核心流程,可以作为一个入门路线,分享给大家。

Mybatis 的三板斧

  • SQL 组装(拼接动态SQL)
  • SQL 执行(提交到数据库)
  • SQL 结果处理(组装成 Java 对象)

Mybatis 作为一个 ORM 框架,其实就是在完成上述三个核心步骤。我们只需捏住 SQL 这个线头,挑动三板斧,整个过程就清晰了。

接下来,我们开始实战。

准备一个动态SQL程序

<mapper namespace="com.xcodemap.mybatis.mapper.UserMapper">
  <select id="findUser" resultType="com.xcodemap.mybatis.mapper.User">
    SELECT * FROM User
    WHERE 1=1
    <if test="age != null">
      AND age = #{age}
    </if>
    <if test="name != null">
      AND name = #{name}
    </if>
  </select>
</mapper>
try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    Map<String, Object> params = new HashMap<>();
    params.put("age", 101);
    params.put("name", "xcodemap101");
    List<User> users = mapper.findUser(params);
    System.out.println(users);
}

准备一个上下文序列图

要想 5 分钟搞懂核心流程,我们今天需要用到一个源码工具 XCodeMap,这个工具会录制程序的运行时行为,然后产生一个上下文序列图。

Debug with XCodeMap,运行结束,会看到如下的画面,左边是序列图,右边是上下文。

image.png

准备好之后,接下来,我们进入 Mybatis 的第一板斧,就是找到 SQL 拼装的关键函数。

这个时候注意了,如果我们一开始,就去逐个点开各个函数,层层往下看,序列图的节点数会呈指数级增长,大脑内存很快就爆了。

Mybatis 源码从入门到放弃,Game Over 了!

所以,敲黑板了!

当我们遇到陌生代码,不知道类名字,更不知道函数名字,但我们知道这些代码要完成的数据处理功能,此时我们可以直接根据数据值去搜索关键代码。具体到 Mybatis 的第一板斧,我们可以根据 SQL 去搜索关键代码。

值搜索带 #{} 的 SQL

我们先使用 XCodeMap 值搜索 “SELECT *”,可以看到带“#{}” 和 带“?”的语句,我们先选择带 “#{}”,毕竟这是我们在 XML 文件中配置的形式。

SQL-001.gif

搜索完成之后,画面就仅剩 3 个函数了,分别是:

  • DynamicContext::getSql
  • SqlSourceBuilder::parse
  • GenericTokenParser:parse

在右侧上下文观察各个函数的输入输出,可以很快发现,DynamicContext 负责返回带 *#{} *的SQL,而GenericTokenParser 负责把 *#{} *替换成 * *。

目前知道了一些知识点,但还没解决 Mybatis 第一板斧的问题:

  • SQL 是怎么拼接来的?

看上图,第一个返回 SQL 的地方是 DynamicContext,根据名字猜测,动态 SQL 应该跟 DynamicContext 有关。

这是一个线索,此时,我们需要做的是,把所有跟 DynamicContext 相关的函数调用给找出来:

  • 作为调用者,调用了哪些函数?
  • 作为参数,被哪些函数调用?

XCodeMap 可以通过对象追踪能力,一键完成这个目标。

追踪 DynamicContext 对象

右键 DynamicContext 对象,选择 “trace object”,就可以在左边的序列图上,把所有跟这个对象相关的节点标记出来。

SQL-002.gif

图上可以清晰地标记出来,DynamicContext 主要是和 MixedSqlNode,IfSqlNode,StaticTextSqlNode 在进行交互。

这三类 Node 都有调用一个 apply 方法,因为他们都实现了 SqlNode

public interface SqlNode {
  boolean apply(DynamicContext context);
}

这个时候,我们需要搞清楚这三个 Node 的基本职责,只需竖着看序列图,针对每个类,挨个点开成员函数,查看右侧的输入输出。

我们很快就会发现:

  • MixedSqlNode 就是一个包装器,就遍历子节点,没有实质内容
  • IfSqlNode 先 evaluate 一个表达式,如果为真,就继续往下执行子节点
  • StaticTextSqlNode 调用 appenSql 组装最后的文本

整个过程就是,从rootSqlNode开始,深度优先遍历整棵树,把符合条件的 StaticTextSqlNode 的文本直接拼接起来就可。

如果这个时候,我们想进一步了解 rootSqlNode 是如何生成的,采取类似办法,进一步追踪即可。

到这里,Mybatis 的第一板斧就完成了,接下来是第二板斧:

  • SQL 是怎么被提交到数据库执行的?

同样的问题,当我们对代码不熟悉,不知道关键函数的名字时,通过数据值搜索来定位。具体到 Mybatis 第二板斧,值搜索带 ? 的 SQL,这是数据库可以最终理解的 SQL。

值搜索带 ?的 SQL

image.png

搜索之后,画面上的节点略多。但没关系,我们迅速浏览一下,找到我们最熟悉的节点,很快,我们就会发现熟悉的 prepareStatement。

没错,这里就是大家学校期间都学过的 JDBC 调用范式,我们可以简单复习一下。

pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "xcodemap"); // 设置第一个参数
pstmt.setInt(2, 101); // 设置第二个参数
rs = pstmt.execute();
while(rs.next()) {
}

如果这个时候,我们追踪一下 ClientPreparedStatement 这个对象,就可以发现熟悉的配方:

image.png

  • setInt (设置参数 age)
  • setString (设置参数 name)
  • execute (执行数据库操作)

这里多说一句,利用熟悉的知识,去链接不熟悉的知识,是一个非常重要的学习技巧,甚至可以说是人脑认知世界的根本方式,有些大佬喜欢把这个过程叫做编织,AnyWay,如果你之前不了解这个概念,那么恭喜你,今天可以比别人多扫一个盲区。

回到 Mybatis,本质上它就是封装 JDBC 的 Preparement 调用范式,提供更加方便的操作体验而已。

此时,我们已经找到了关键的 execute 函数,好比我们在地图上已经锁定了目的地,此时需要做的是,找出出发地到目的地的路径。

利用 XCodeMap 的栈回溯能力,可以一键完成这个目标。

回溯 execute 堆栈

SQL-003.gif

回溯之后,惊喜发生了,Mybatis 的 SQL 执行流程,一览无遗。

你可以在流程中,看到 SimpleExecutor → PrepareStatementHandler → DefaultResultSetHandler 等重要类缓缓呈现出来。

顺着这个核心流程,我们只需往上一步,也就是 PreparedStatementHandler 的 query 方法,可以非常直观地看到它调用了 DefaultResultSetHandler 的 handleResultSets,而这个方法返回了我们需要的 List,也就是说,就是这个方法,完成了 Java 对象的组装。

组装 Java 对象

这是最后一板斧了,胜利的曙光就在眼前。

在这最后一刻,我们不要忘记今天学到的内容,就是关注数据值,避免陷入指数级的代码调用中,否则就这 handleResultSets 也够你喝一壶的。

对于有返回值的函数,这时候,我们继续运用 XCodeMap 的对象追踪能力,追踪这个返回值的来龙去脉。

image.png

在图上,把一些函数折叠一下,整个思路就清晰了。

组装 Java 对象其实也是可以分成三个基本步骤:

  • 构造对象,对应图上的 createResultObject 和 newMetaObject
  • 获取 java属性到 db 列名的映射关系(图上没有直接显示,但你可以通过show stack 找到)
  • 调用 setBeanProperty 进行赋值(点进去可以看到 TypeHandler)

总结

image.png

回顾一下我们今天的动作:

  • 值搜索 原始 SQL —— 找到 GenericTokenParser,其完成 #{} 到 ?的转换
  • 追踪对象 DynamicContext —— 找到动态 SQL 的组装过程
  • 值搜索 最终 SQL —— 找到熟悉的 PrepareStatement 类
  • 追踪类 ClientPrepareStatement —— 找到熟悉的 JDBC 调用范式
  • 栈回溯 execute —— 找到核心流程,包括 SimpleExecutor,PrepareStatementHandler
  • 追踪对象 User —— 找到 TypeHandler,了解属性的赋值过程

这里的 6 个动作,其实都是 XCodeMap 三板斧的灵活运用:

  • 值搜索(搜索程序运行过程中产生的数据,比如 SQL 字符串)
  • 对象追踪(追踪某个对象的来龙去脉,搞清楚对象的角色和演变)
  • 栈回溯(溯源主干,找到支线)

用好 XCodeMap,避免陷入分支抽象的干扰,避免频繁 Debug,避免大脑内存爆炸,快速定位 Mybatis 的核心函数和基本逻辑,从而做到 5 分钟读懂 Mybatis 源码。

XCodeMap 或许是一个可以改变你竞争现状的工具,强烈推荐大家去试用。

参考:

  1. xcodemap.tech