第3章 MyBatis运行初探
在阅读一个项目的源码前,使用具有断点调试功能的开发软件对项目代码进行一次追踪是非常有必要的。这项工作将让我们对整个项目的骨架脉络有一个总体的认识,为之后的源码阅读提供指引。
在追踪的过程中要抓大放小,重点关注与项目核心功能相关的部分,忽略一些细枝末节的代码。在追踪过程中也可以对代码的逻辑进行一些猜测,但不要对看不懂的代码过分纠结。
进行代码追踪时,可以将整个过程大体记录下来,作为源码阅读的框架;也可以将遇到的问题记录下来,等待源码阅读时分析解答。
现在就开始对 MyBatis的源码进行一次调试追踪,从而了解 MyBatis源码的骨架脉络。在这一次调试追踪中,我们不会使用第2 章中建立的项目。因为上述项目通过mybatis-spring-boot-starter包引入了 mybatis-spring、mybatis-spring-boot-autoconfigure等包,这会给调试工作带来干扰。因此,我们会尽量不依赖其他外部项目,而搭建一个纯粹的MyBatis项目。
最终,搭建的项目除依赖 Spring Boot必需的 spring-boot-starter外,还依赖 mybatis和mysql-connector-java。该项目的依赖如代码3-1所示。
【代码3-1】
该项目需要手动创建 MyBatis配置文件,如代码3-2所示。
【代码3-2】
此外,User类、UserMapper接口、映射文件 UserMapper.xml均和第2章中建立的项目一致,并且 application.properties文件中不需要任何配置,只留下一个空文件即可。最终的项目文件结构如图3-1所示。
图3-1 项目文件结构
项目搭建完成后,在 Spring Boot的主函数中写入操作逻辑,如代码3-3所示。运行后便可以通过数据库查询出图3-2所示的程序运行结果。
【代码3-3】
图3-2 程序运行结果
通过代码可以清晰地看出,MyBatis的操作主要分为两大阶段:
· 第一阶段:MyBatis初始化阶段。该阶段用来完成 MyBatis运行环境的准备工作,只在 MyBatis启动时运行一次。
· 第二阶段:数据读写阶段。该阶段由数据读写操作触发,将根据要求完成具体的增、删、改、查等数据库操作。
在进行 MyBatis的运行追踪时,也按照上述两个阶段分别展开。
3.1 初始化阶段追踪
MyBatis 的初始化会在整个项目启动时开始执行,主要用来完成配置文件的解析、数据库的连接等工作。
3.1.1 静态代码块的执行
每个 Java类在被“首次主动使用”时都需要先进行类的加载。所谓的“首次主动使用”包括创建类的实例、访问类或接口的静态变量、被反射调用、初始化类的子类等。
类的加载就是 Java虚拟机将描述类的数据从 Class文件加载到 JVM的过程,在这一过程中会对 Class文件进行数据加载、连接和初始化,最终形成可以被虚拟机直接使用的 Java类。类的加载过程如图3-3所示。
图3-3 类的加载过程
而在图3-3所示的初始化阶段,会执行类的静态代码块。
静态代码块是类中一段由 static关键字标识的代码,它通常用来对类静态变量进行初始化。例如,代码3-4所示的静态代码块位于 MyBatis的 Jdk类中,用来判断当前环境中是否存在 java.lang.reflect.Parameter类,并根据结果初始化 parameterExists变量的值。
【代码3-4】
静态代码块会在类加载过程的初始化阶段执行,并且只会执行一次。一个类中可以有多个静态代码块,它们会按照顺序依次执行。
MyBatis 中存在众多的类,而这些类被“首次主动使用”的时间各不相同,因此不同类中的静态代码块的执行时机各不相同。但是,对于每一个类而言,类中的静态代码块都是这个类中首先被执行的代码。
因此,接下来系统调用任何一个类时,这个类的静态代码块必定已经执行完成。对于这一点,在后面的分析中不再单独提及。
3.1.2 获取InputStream
下面从主方法入手,来追踪 MyBatis 的初始化过程。主方法中首先进行的是InputStream对象的获取,如代码3-5所示。
【代码3-5】
在代码3-5中将配置文件的路径传递给了 Resource中的 getResourceAsStream方法。我们以此为入口通过单步执行的方式不断追踪代码。
最终,我们发现 ClassLoaderWrapper 中的 getResourceAsStream(String,ClassLoader[])方法根据配置文件的路径获取到配置文件的输入流。代码3-6给出了该方法的源码。
【代码3-6】
代码3-6 所示方法的输入参数除配置文件的路径外,还包括一组 ClassLoader。ClassLoader叫作类加载器,是负责加载类的对象。给定类的二进制名称,类加载器会尝试定位或生成构成该类定义的数据。一般情况下,类加载器会将名称转换为文件名,然后从文件系统中读取该名称的类文件。因此,类加载器具有读取外部资源的能力,这里要借助的正是类加载器的这种能力。
代码3-6 所示的 getResourceAsStream 方法会依次调用传入的每一个类加载器的getResourceAsStream方法来尝试获取配置文件的输入流。在尝试过程中如果失败的话,会在传入的地址前加上“/”再试一次。只要尝试成功,即表明成功加载了指定的资源,会将所获得的输入流返回。
整个过程中涉及的 Resource类和 ClassLoaderWrapper类均在 MyBatis的 io包中,这也印证了 Resource类和 ClassLoaderWrapper类是负责读写外部文件的。
3.1.3 配置信息读取
获取 InputStream后,进行的是代码3-7所示的操作。
【代码3-7】
这一步首先创建了一个SqlSessionFactoryBuilder类的实例,然后调用了其build方法。build方法有多个,其中的核心方法如代码3-8所示。
【代码3-8】
整个方法中最核心的部分如代码3-9所示。
【代码3-9】
这两句代码完成了两步操作:
(1)生成了一个 XMLConfigBuilder 对象,并调用了其 parse 方法,得到一个Configuration对象(因为 parse方法的输出结果为 Configuration对象)。
(2)调用了 SqlSessionFactoryBuilder 自身的 build 方法,传入参数为上一步得到的Configuration对象。
我们对上述两步操作分别进行追踪。
首先找到 XMLConfigBuilder类的 parse方法,如代码3-10所示。
【代码3-10】
在代码3-10 中出现了“/configuration”字符。“/configuration”是整个配置文件的根节点,因此这里是解析整个配置文件的入口。而 parseConfiguration方法是解析配置文件的起始方法,如代码3-11所示。
【代码3-11】
在代码3-11中,parseConfiguration方法依次解析了配置文件 configuration节点下的各个子节点,包括关联了所有的映射文件的 mappers子节点。
进入每个子方法可以看出,解析出的相关信息都放到了 Configuration类的实例中。因此 Configuration 类中保存了配置文件的所有设置信息,也保存了映射文件的信息。可见Configuration类是一个非常重要的类。
最终,XMLConfigBuilder对象的 parse方法返回了一个 Configuration对象。
通过 XMLConfigBuilder 对象的 parse 方法获得了 Configuration 对象后,SqlSessionFactoryBuilder 自身的 build 方法接受 Configuration 对象为参数,返回了SqlSessionFactory对象。
这样主函数中“SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream)”这一句的解析就结束了。
3.1.4 总结
通过上面的追踪,MyBatis 的初始化阶段已经分析完毕。在初始化阶段,MyBatis 主要进行了以下几项工作。
· 根据配置文件的位置,获取它的输入流 InputStream。
· 从配置文件的根节点开始,逐层解析配置文件,也包括相关的映射文件。解析过程中不断将解析结果放入 Configuration对象。
· 以配置好的 Configuration对象为参数,获取一个 SqlSessionFactory对象。
3.2 数据读写阶段追踪
在初始化阶段结束之后,我们来对读写阶段进行追踪,初步探究当进行一次数据库的读或写操作时,MyBatis内部都要经过哪些步骤。
3.2.1 获得SqlSession
在初始化阶段,我们已经获得了 SqlSessionFactory,而数据库操作过程中需要一个SqlSession对象。从类的名称就可以看出,SqlSession是由 SqlSessionFactory生成的。在主方法中,由 SqlSessionFactory生成 SqlSession的过程如代码3-12所示。
【代码3-12】
我们追踪 openSession 方法了解实现细节,在 DefaultSqlSessionFactory 中找到了openSessionFromDataSource方法,这是生成 SqlSession的核心源码,如代码3-13所示。
【代码3-13】
在代码3-13中,我们看到 Configuration对象中存储的设置信息被用来创建各种对象,包括事务工厂 TransactionFactory、执行器 Executor及默认的 DefaultSqlSession。
进入 DefaultSqlSession 类,可以看到它提供了查询、增加、更新、删除、提交、回滚等大量的方法。从 DefaultSqlSession 返回后,主方法中“SqlSession session=sqlSessionFactory.openSession()”这句代码就执行完毕了。
有一点需要注意,数据读写阶段是在进行数据读写时触发的,但并不是每次读写都会触发“SqlSession session=sqlSessionFactory.openSession()”操作,因为该操作得到的SqlSession对象可以供多次数据库读写操作复用。
3.2.2 映射接口文件与映射文件的绑定
在 1.5.2节我们已经介绍,映射接口文件是指 UserMapper.class等存有接口的文件,而映射文件是指 UserMapper.xml等存有 SQL操作语句的文件。最终,MyBatis将这两类文件一一对应了起来。
在进行数据查询之前,主方法先通过代码3-14所示的语句找到 UserMapper接口对应的实现。
【代码3-14】
该操作通过 Configuration类的 getMapper方法转接,最终进入 MapperRegistry类中的getMapper方法。MapperRegistry类中的 getMapper方法如代码3-15所示。
【代码3-15】
在代码3-15所示的源码中,getMapper方法通过映射接口信息从所有已经解析的映射文件中找到对应的映射文件,然后根据该映射文件组建并返回接口的一个实现对象。
3.2.3 映射接口的代理
我们已经知道“session.getMapper(UserMapper.class)”方法最终得到的是“mapperProxy Factory.newInstance(sqlSession)”返回的对象。那该对象到底是什么呢?
我们追踪“mapperProxyFactory.newInstance(sqlSession)”方法,可以在 MapperProxyFactory类中找到代码3-16所示的方法。
【代码3-16】
可见这里返回的是一个基于反射的动态代理对象,因此我们直接找到 MapperProxy类的 invoke方法并在其中打上断点。invoke方法的源码如代码3-17所示。
【代码3-17】
接下来主方法中代码3-18所示的操作会进入代码3-17所示的方法。
【代码3-18】
然后,会触发 MapperMethod对象的 execute方法,该方法如代码3-19所示。
【代码3-19】
在代码3-19中,MyBatis根据不同数据库操作类型调用了不同的处理方法。当前项目进行的是数据库查询操作,因此会触发代码3-19中的“result=execute ForMany(sqlSession,args)”语句。executeForMany方法的源码如代码3-20所示。
【代码3-20】
在代码3-20 所示的 executeForMany 方法中,MyBatis 开始通过 SqlSession 对象的selectList方法开展后续的查询工作。
追踪到这里,MyBatis 已经完成了为映射接口注入实现的过程。于是,对映射接口中抽象方法的调用转变为了数据查询操作。
3.2.4 SQL语句的查找
代码3-20所示的操作调用到了 DefaultSqlSession类中的 selectList方法,该方法的源码如代码3-21所示。
【代码3-21】
每个 MappedStatement 对象对应了我们设置的一个数据库操作节点,它主要定义了数据库操作语句、输入/输出参数等信息。
代码3-21 中的“configuration.getMappedStatement(statement)”语句将要执行的MappedStatement对象从 Configuration对象存储的映射文件信息中找了出来。
3.2.5 查询结果缓存
对应的数据库操作节点被查找到后,MyBatis 使用执行器开始执行语句。在代码3-21中可以看到代码3-22所示的触发操作。
【代码3-22】
上述 query方法实际是一个 Executor接口中的抽象方法,如代码3-23所示。
【代码3-23】
该抽象方法有两种实现,分别在BaseExecutor类和CachingExecutor类中。这时可以直接在抽象方法上打断点,如图3-4所示,查看程序会跳转到哪个实现方法上。
图3-4 抽象方法的断点
执行到断点后可以发现,实际执行的是 CachingExecutor类中的代码3-24所示的方法。
【代码3-24】
BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语句,而 CacheKey是为该次查询操作计算出来的缓存键。接下来流程会走到代码3-25所示的函数。
【代码3-25】
在代码3-25中,MyBatis查看当前的查询操作是否命中缓存。如果是,则从缓存中获取数据结果;否则,便通过 delegate调用 query方法。
3.2.6 数据库查询
3.2.5节中 delegate调用的 query方法再次调用了一个 Executor接口中的抽象方法,如代码3-26所示。我们同样在该抽象方法上打断点以追踪程序的实际流向。
【代码3-26】
我们发现,程序停留在了 BaseExecutor类中的 query方法上。该方法如代码3-27所示。
【代码3-27】
上述方法逻辑判断较多,相对复杂,我们不去深究。其中的关键操作如代码3-28所示。这表明 MyBatis开始调用数据库展开查询操作。
【代码3-28】
queryFromDatabase方法的源码如代码3-29所示。
【代码3-29】
通过代码3-29可以看出,MyBatis先在缓存中放置一个占位符,然后调用 doQuery方法实际执行查询操作。最后,又把缓存中的占位符替换成真正的查询结果。
doQuery方法是 BaseExecutor类中的抽象方法,实际运行的最终实现如代码3-30所示。
【代码3-30】
上述方法生成了Statement对象stmt。Statement类并不是MyBatis中的类,而是java.sql包中的类。Statement类能够执行静态 SQL语句并返回结果。
程序还通过 Configuration的 newStatementHandler方法获得了一个 StatementHandler对象 handler,然后将查询操作交给 StatementHandler对象进行。StatementHandler是一个语句处理器类,其中封装了很多语句操作方法,这里先不细究。继续追踪“handler.<E>query (stmt,resultHandler)”语句。
“handler.<E>query(stmt,resultHandler)”调用的是 StatementHandler接口中如代码3-31所示的抽象方法。
【代码3-31】
我们在代码3-31 所示的抽象方法上打断点,经过多次跳转后,程序执行到了PreparedStatementHandler类中的代码3-32所示的方法中。
【代码3-32】
这里 ps.execute()真正执行了 SQL 语句,然后把执行结果交给 ResultHandler 对象处理。而PreparedStatement类并不是MyBatis中的类,因而ps.execute()的执行不再由MyBatis负责,而是由 com.mysql.cj.jdbc包中的类负责,这里不再继续追踪。
查询完成之后的结果放在 PreparedStatement对象中,通过调试工具可以看到其中包含了这次查询得到的数据库字段信息、数据记录信息等,如图3-5所示。
图3-5 PreparedStatement对象中的信息
这一步数据库查询操作涉及的方法较多。整个流程的关键步骤如下。
· 在进行数据库查询前,先查询缓存;如果确实需要查询数据库,则数据库查询之后的结果也放入缓存中。
· SQL 语句的执行经过了层层转化,依次经过了 MappedStatement 对象、Statement对象和 PreparedStatement对象,最后才得以执行。
· 最终数据库查询得到的结果交给 ResultHandler对象处理。
3.2.7 处理结果集
查询得到的结果并没有直接返回,而是交给 ResultHandler对象处理。ResultHandler是结果处理器,用来接收此次查询结果的方法是该接口中的抽象方法 handleResultSets,如代码3-33所示。
【代码3-33】
最终实际执行的方法是 DefaultResultSetHandler中代码3-34所示的方法。
在上述方法中,查询出来的结果被遍历后放入了列表multipleResults 中并返回。multipleResults中存储的就是这次查询期望的结果 List<User>。
在结果处理中,我们最关心的是 MyBatis如何将数据库输出的记录转化为对象列表,因此详细追踪这个过程。然而整个过程非常长,在 DefaultResultSetHandler 的方法中进行了多次跳转,这里直接给出整个方法的调用链路,如图3-6所示。
图3-6 方法的调用链路
其中重点关注的是图3-6中粗线边框标注的三个方法。
· createResultObject(ResultSetWrapper,ResultMap,List<Class<?>>,List<Object>,String)方法:该方法创建了输出结果对象。在示例中,为 User对象。
· applyAutomaticMappings 方法:在自动属性映射功能开启的情况下,该方法将数据记录的值赋给输出结果对象。
· applyPropertyMappings方法:该方法按照用户的映射设置,给输出结果对象的属性赋值。
其中,createResultObject(ResultSetWrapper,ResultMap,List<Class<?>>,List<Object>,String)方法的源码如代码3-35所示,该方法根据输出对象的不同,使用类型处理器或通过调用构造方法等方式创建输出结果对象。
【代码3-35】
applyAutomaticMappings方法和 applyPropertyMappings方法的实现逻辑类似,以代码3-36所示的 applyAutomaticMappings方法为例进行介绍。
【代码3-36】
其基本思路就是循环遍历每个属性,然后调用“metaObject.setValue(mapping.property,value)”语句为属性赋值。
经过以上过程,MyBatis将数据库输出的记录转化为了对象列表。
之后,以上方法逐级返回。最后,装载着对象列表的 multipleResults 被返回给“List<User> userList”变量,我们便拿到了查询结果。追踪到这里,主方法中代码3-37所示的语句终于执行完成了。
【代码3-37】
3.2.8 总结
在整个数据库操作阶段,MyBatis完成的工作可以概述为以下几条。
· 建立连接数据库的 SqlSession。
· 查找当前映射接口中抽象方法对应的数据库操作节点,根据该节点生成接口的实现。
· 接口的实现拦截对映射接口中抽象方法的调用,并将其转化为数据查询操作。
· 对数据库操作节点中的数据库操作语句进行多次处理,最终得到标准的 SQL语句。
· 尝试从缓存中查找操作结果,如果找到则返回;如果找不到则继续从数据库中查询。
· 从数据库中查询结果。
· 处理结果集。
-建立输出对象;
-根据输出结果对输出对象的属性赋值。
· 在缓存中记录查询结果。
· 返回查询结果。
通过以上步骤可以看出,MyBatis完成一次数据库操作的过程还是十分复杂的。因此,平时的软件开发过程中要尽量减少数据库操作,这样能极大地提高软件运行的效率。
终于,我们完成了 MyBatis源码的一次运行追踪。整个追踪过程中可能会有一些点让我们恍然大悟,但有更多的点让我们感到迷茫。这种情况是正常的,因为这只是一次构建整个项目框架脉络的初步探索。接下来,将以包为单位详细阅读 MyBatis的源码,在此过程中,这些迷茫会渐渐消失。