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

一、概述

MyBatis是一个轻量级的ORM框架,其官方首页是这么介绍自己。

MyBatis数据映射器框架使将关系数据库与面向对象的应用程序结合使用变得更加容易。 MyBatis使用XML描述符或注释将对象与存储过程或SQL语句耦合。 相对于对象关系映射工具,简单性是MyBatis数据映射器的最大优势。

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

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

二 、整体架构

从功能流程层次描述MyBatis的整体架构图如下图所示:

1、接口层

我们知道,在不考虑与Spring集成的情况下,使用MyBatis执行数据库操作的代码如下:

String resource = "mybatis-config.xml";  //配置文件放在了resource类路径下
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try(SqlSession session = sqlSessionFactory.openSession()) {
  Blog blog = session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}catch(Exception e){
    e.printStackTrace();
}

上面的代码中就使用了接口层的两个核心接口:SqlSessionFactorySqlSession,其中SqlSessionFactory用于构建一个SqlSession(openSession()方法)以及生成MyBatis获取(getConfiguration()方法)SqlSession接口中定义了MyBatis 暴露给应用程序调用CRUD的API ,也就是上层应用与MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。这两个类都在MyBatis的session包下,这个包的主体类结构图如下:

两个核心接口:SqlsessionFactory和SqlSession。

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

(1)配置解析

在MyBatis 初始化过程中,会加载mybatis-config.xml 配置文件、映射配置文件以及Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configuration 对象中。

之后,利用该Configuration 对象创建SqlSessionFactory 对象。待MyBatis 初始化之后,开发人员可以通过初始化得到Sq!SessionFactory 创建SqlSession 对象并完成数据库操作。

1)全局配置文件mybatis-config.xml是在XMLConfigBuilder中完成解析的,有关类图大致如下:

xml解析常见的方式有三种:DOM(Document Object Model)、SAX(Simple API for XML)、StAX(Streaming API for XML)

MyBatis在初始化过程中处理mybatisConfig.xml配置文件及映射配置文件时,使用的是DOM解析方式,并结合使用XPath(JDK提供的一个解析xml的类)解析XML配置文件。
DOM是基于树形结构的XML解析方式,它会将整个XML文档读入内存并构建一个DOM树,基于这棵树形结构对各个节点(XNode)进行操作。如下图所示:

2)SQL映射文件是在XMLMapperBuilder这个类中完成解析的,其中把对Statement的解析(即XxxMapper.xml中SELECT|INSERT|UPDATE|DELETE定义部分)委托给XMLStatementBuilder来完成。XxxMapper.xml的解析比较复杂的,涉及到PreparedMapping、ResultMapping、LanguageDriver、Discriminator、缓存、自动映射等一系列对象的构造。

(2)SQL执行

SQL 语句的执行涉及多个组件,其中比较重要的是Executor 、StatementHandler 、ParameterHandler 和ResultSetHandler。

1)Executor 主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给StatementHandler 完成。下面是Executor的类图

从上图中可以看到,Executor主要提供了一下功能:

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

BaseExecutor:它内部维护了localCache来localOutputParameterCache来处理缓存,以及线程安全的延迟加载列表deferredLoads、事务对象Transaction。

BatchExecutor:它内部维护了statementList批量提交并通过batchResultList保存执行结果

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

2)**StatementHandler **首先通过 **ParameterHandler 完成SQL 语句的实参绑定。然后通过java.sql.Statement 对象执行SQL 语句并得到结果集ResultSet。最后通过ResultSetHandler **完成结果集的映射,得到结果对象并返回。下图所示就是一条SQL语句在MyBatis中的执行流程:

3)SOL 解析与scripting 模块:MyBatis 有动态SQL 语句的功能,提供了多种动态SQL 语句对应的节点,例如,< where>节点、< if>节点、< foreach>节点等。通过这些节点的组合使用, 开发人员可以写出几乎满足所有需求的动态SQL 语句。

MyBatis 中的scripting 模块会根据用户传入的实参,解析映射文件中定义的动态SQL节点,并形成数据库可执行的SQL 语句。之后会处理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(我们写代码常用的Resource类就在这个包下)、 封装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里比较重要的部分,MyBatis提供两种级别的缓存:

  • SESSION/一级缓存,默认生命周期是一次SESSION,BaseExecutor中根据MappedStatement的Id、SQL、参数值以及rowBound(边界)来构造CacheKey,并使用BaseExccutor中的localCache来维护此缓存。
  • 全局的二级缓存,通过CachingExecutor来实现,其委托TransactionalCacheManager来保存/获取缓存,这个全局二级缓存比较复杂,详细的内容大家可以参考我的另一篇博文深入理解MyBatis缓存机制

3.6、数据源/连接池

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

3.7 事务

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

3.8 Binding 模块

MyBatis通过绑定模块将用户自定义的SQL映射文件和混和Mapper接口关联起来了,系统可以通过调用自定义Mapper 接口中的方法执行相应的SQL 语句完成数据库操作。

开发人员无须编写自定义Mapper 接口的实现, MyBatis会通过动态代理自动为Mapper解口创建实现类实例。

参考资料

留言区

还能输入500个字符