MyBatis架构设计及源代码分析系列(一):MyBatis架构

如果不太熟悉 MyBatis 使用的请先参见MyBatis 官方文档, 这对理解其架构设计和源码分析有很大好处。

一、概述

MyBatis 并不是一个完整的 ORM 框架,其官方首页是这么介绍自己

The MyBatis data mapper framework makes it easier to use a relational database with object-oriented applications. MyBatis couples objects with stored procedures or SQL statements using a XML descriptor or annotations. Simplicity is the biggest advantage of the MyBatis data mapper over object relational mapping tools.
 

而在其官方文档中介绍“What is MyBaits”中说到

MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces and Java POJOs (Plain Old Java Objects) to database records. 
 
ORM 是 Object 和 Relation 之间的映射,包括 Object->Relation 和 Relation->Object 两方面。Hibernate 是个完整的 ORM 框架,而 MyBatis 完成的是 Relation->Object,也就是其所说的 data mapper framework。关于 ORM 的一些设计思路和细节可以参见 Martin Flow《企业应用架构模式》一书中的 ORM 章节,MyBatis 并不刻意于完成 ORM(对象映射) 的完整概念,而是旨在更简单、更方便地完成数据库操作功能,减轻开发人员的工作量,我想这对于应用系统来说也是最实用的,相信用 Hibernate 的都受过它的痛苦,而用过 MyBatis 的都会感觉它很简捷轻松。
 

二、整体架构

下面是从功能流程层次描述 MyBatis 的整体架构图

image

 

而下面是 MyBatis 源码包对应的架构图

image

 

下面以“功能流程角度的架构图”来简要地分析下各层的架构,在后面系列文章中将有专题来深入解析 MyBatis 重要的功能点。

 

1、接口层

我们知道,在不考虑与 Spring 集成的情况下, 使用 MyBatis 执行数据库操作的代码如下
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
  Blog blog = session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
  session.close();
}

SqlSessionFactory、SqlSession 这是 MyBatis 接口层的核心类,尤其是 SqlSession,是实现所有数据库操作的 API,这几个类都是 org.apache.ibatis.session 包下的,这个包的主体类结构图如下

image

Configuration 是 MyBatis 中相当重要的一个类,可以这么说,如果理解了其中的所有参数的意义,不仅清楚地知道 MyBatis 提供的所有配置项,还理解了 MyBatis 的内部核心运行原理,当然要真正理解这些参数的意义及实现,还需要阅读完完整的 MyBatis 框架之后才能做到。

由上图可以看到,Configuration 对象与 DefaultSqlSessionFactory 是 1:1 的关联关系,这也就意味着在一个 DefaultSqlSessionFactory 衍生出来的所有 SqlSession 作用域里,Configuration 对象是全局唯一的。同时 SqlSessionFactory 提供了 getConfiguration() 接口来公开 Configuration 对象,因此开发者除了配置文件之外,还可以在程序里动态更改 Configuration 的属性项以达到动态调整的目的,但此时不仅要考虑到执行完 reset,同时还要考虑在修改过程中会可能影响到其他 SqlSession 的执行。

2、核心层

2.1 配置解析

在应用启动的时候,MyBatis 解析两种配置文件

  • SqlMapConfig.xml
  • SqlMap.xml

SqlMapConfig.xml 是在 XMLConfigBuilder 类中完成解析的,其类图关系大致如下

1

我们知道 XML 有两种解析方式:一是 DOM,另一个是 SAX,MyBatis 使用的是 org.wrc.dom——JDK 提供的文档对象模型 (DOM) 接口(SqlMapConfig.xml 并不大,所以 DOM 方式并没有什么效率损耗,JDK 也提供了 SAX 模型接口 org.xml.sax,这两个都是 JAXP 的组件 API),以及 JDK 官方提供的 javax.xml.xpath.XPath 来作为 XML 路径寻找组件。

SqlMap.xml 是在 XMLMapperBuilder 中解析完成的,其中把对 Statement 的解析 (即 SqlMap.xml 中 SELECT|INSERT|UPDATE|DELETE 定义部分) 委托给 XMLStatementBuilder 来完成。SqlMap.xml 的解析比较复杂的,涉及到 PreparedMapping、ResultMapping、LanguageDriver、Discriminator、缓存、自动映射等一系列对象的构造,这里暂时略过,后面专题分析。

2.2 SQL 执行

MyBatis 中 Executor 是的核心,围绕着它完成了数据库操作的完整过程。下面是 Executor 的类图

image

在上图中我列出了 Executor 中方法的参数,而在其子类中就没有明确写出。从上图中可以看到,Executor 主要提供了

  • QUERY|UPDATE(INSERT 和 DELETE 也是使用 UPDATE),从方法定义中可看到,它需要 MappedStatement、parameter、resultHandler 这几个实例对象,这几个也是 SQL 执行的主要部分,详细实现在后面专题中再介绍。
  • 事务提交 / 回滚,这委托给 Transaction 对象来完成。
  • 缓存,createCacheKey()/isCached()。
  • 延迟加载,deferload()。
  • 关闭,close(),主要是事务回滚 / 关闭。

BaseExecutor 的属性表明:它内部维护了 localCache 来 localOutputParameterCache 来处理缓存,至于这缓存保存的是什么,这后面专题再说。以及线程安全的延迟加载列表 deferredLoads、事务对象 Transaction。

BatchExecutor 的属性已经表明:它内部维护了 StatementList 批量提交并通过 batchResultList 保存执行结果。

ResueExecutor 的属性及方法表明:它内部维护了 java.sql.Statement 对象缓存,以重用 Statement 对象 (对于支持预编译的数据库而言,在创建 PreparedStatement 时需要发送一次数据库请求预编译,而重用 Statement 对象主要是减少了这次预编译的网路开销)。

下面以 SqlSession.selectList 为例,画出 SQL 执行的时序图 (点击下方的图片查看大图,部分分支有所简化)

未命名

 

3、基础层

3.1、logging:

MyBatis 使用了自己定义的一套 logging 接口,根据开发者常使用的日志框架——Log4j、Log4j2、Apache Commons Log、java.util.logging、slf4j、stdout(控制台)——分别提供了适配器。由于各日志框架的 Log 级别分类法有所不同 (比如 java.util.logging.Level 提供的是 All、FINEST、FINER、FINE、CONFIG、INFO、WARNING、SEVERE、OFF 这九个级别,与通常的日志框架分类法不太一样),MyBatis 统一提供 trace、debug、warn、error 四个级别,这基本与主流框架分类法是一致的 (相比而言缺少 Info,也许 MyBatis 认为自己的日志要么是 debug 需要的,要么就至少是 warn,没有 Info 的必要)。

在 org.apache.ibatis.logging 里还有个比较特殊的包 jdbc,这不是按字面意义理解把日志通过 jdbc 记录到数据库里,而是将 jdbc 操作以开发者配置的日志框架打印出来,这也就是我们在开发阶段常用的跟踪 SQL 语句、传入参数、影响行数这些重要的调试信息。

3.2、IO

MyBatis 里的 IO 主要是包含两大功能:提供读取资源文件的 API、封装 MyBatis 自身所需要的 ClassLoader 和加载顺序。

3.3、reflection

在 MyBatis 如参数处理、结果映射这些大量地使用了反射,需要频繁地读取 Class 元数据、反射调用 get/set,因此 MyBatis 提供了 org.apache.ibatis.reflection 对常见的反射操作进一步封装,以提供更简洁方便的 API。比如我们 reflect 时总是要处理异常 (IllegalAccessException、NoSuchMethodException),MyBatis 统一处理为自定义的 RuntimeException,减少代码量。

3.4、exceptions

在以 Spring 为代表的开源框架中,对于应用程序中无法进一步处理的异常大都转成 RuntimeException 来方便调用者操作,另外如频繁遇到的 SQLException,JDK 约定其是个 Exception,从 JDK 的角度考虑,强制要求开发者捕获 SQLException 是为了能在 catch/finally 中关闭数据库连接,而 Spring 之类的框架为开发者做了资源管理的事情,自然就不需要开发者再烦心 SQLException,因此封装转换成 RuntimeException。MyBatis 的异常体系不复杂,org.apache.ibatis.exceptions 下就几个类,主要被使用的是 PersistenceException。

3.5、缓存

缓存是 MyBatis 里比较重要的部分,有两种缓存:

  • SESSION 或 STATEMENT 作用域级别的缓存,默认是 SESSION,BaseExecutor 中根据 MappedStatement 的 Id、SQL、参数值以及 rowBound(边界) 来构造 CacheKey,并使用 BaseExccutor 中的 localCache 来维护此缓存。
  • 全局的二级缓存,通过 CacheExecutor 来实现,其委托 TransactionalCacheManager 来保存 / 获取缓存,这个全局二级缓存比较复杂,后面还需要专题分析,至于其缓存的效率以及应用场景也留到那时候再分析。

3.6、数据源 / 连接池

MyBatis 自身提供了一个简易的数据源 / 连接池,在 org.apache.ibatis.datasource 下,后面专题分析。主要实现类是 PooledDataSource,包含了最大活动连接数、最大空闲连接数、最长取出时间 (避免某个线程过度占用)、连接不够时的等待时间,虽然简单,却也体现了连接池的一般原理。阿里有个“druid”项目,据他们说比 proxool、c3p0 的效率还要高,可以学习一下。

3.7 事务

MyBatis 对事务的处理相对简单,TransactionIsolationLevel 中定义了几种隔离级别,并不支持内嵌事务这样较复杂的场景,同时由于其是持久层的缘故,所以真正在应用开发中会委托 Spring 来处理事务实现真正的与开发者隔离。分析事务的实现是个入口,借此可以了解不扫 JDBC 规范方面的事情。

 

后续将对 MyBatis 各个部分做详细的设计及源代码分析,由于读取和解析 SqlMapConfig.xml 和 SqlMap.xml 的逻辑与各个模块的相关性较强,因此将把这部分内容与在各模块组合在一起分析。