MyBatis 原理

MyBatis 整体结构

MyBatis结构

配置文件

配置类提供的功能几乎贯穿了整个处理过程:

  1. 解析 Xml 文件
  2. 创建 SQL 处理器 Executor
  3. 对语句进行缓存 MappedStatement

怎么定位路径

  • getResourceAsStream

怎么解析文件

xml 文件的解析方式有两种,一种 DOM 是直接读入整个 xml 文件,根据标签的嵌套关系构建一棵文档树;另一种方式叫 SAX(Simple API for XML),是一种事件驱动的文档解析方式,什么是事件驱动呢?比如说 SAX 驱动扫描到了起始标签,就代表发生了一个事件,它会转而调用某个由用户定义的函数(startElement)执行逻辑。
有一种设计原则叫好莱坞法则(Hollywood),形象地说就是“你不要 call 我,需要你时我会 call 你”,一个例子是异步调用,这是一种通信机制,客户端在发出请求后不必等待服务端处理完毕就可以返回处理自己的逻辑,等到服务端处理完毕后再将结果传回,这种方式一定程度上可以解决客户端长期阻塞的问题、改善用户体验,回调函数也是一个例子。
据网上的说法,DOM 需要一次构建整棵 DOM 树,所以比较占内存,不适合大的 xml 文档解析,但是由于 DOM 树上可以任意遍历,所以自由度很高,相对来说,SAX 是读到什么就调用什么回调函数,所以内存占用小,但是编程多少会复杂一些。

构建数据库连接

数据库连接

SqlSessionFactoryBuilder

应用了建造者模式,根据配置文件来创建 SqlSessionFactory,创建后其任务就结束了,生命周期在一个方法内。

SqlSessionFactory

创建和数据库连接的工具,在整个应用运行期间应该作为一个单例存在,或者使用依赖注入管理其生命周期。

SqlSession

代表和数据库的一次连接,在 MyBatis 中其实现是线程不安全的,生命周期最好控制在一次请求之间。

数据源

  • DBCP
  • C3P0
  • Druid
  • MyBatis 内置数据源(UNPOOLED、POOLED、JNDI)
  • 自定义数据源

映射器

  • mapper 文件
  • 注解

SQL 执行

SqlSession 本身是可以直接执行 sql 语句的,它的所有 update、query 等方法都是对语句进行了包装(MappedStatement),然后再调用 Executor 的相应方法,Executor 是执行器,是 MyBatis 的核心。
SQL 的执行是由 Executor 负责的,Executor 对象是和 SqlSession 同时创建的,SqlSessionFactory 会为 Executor 创建事务,事务类默认为 ManagedTransactionFactory,Executor 需要从事务对象获取数据库连接(包装上一层事务后扩展性更好),事务会从环境对象中获取 DataSource 对象,然后委托 DataSource 创建连接,并且可以根据事务等级来为连接设置事务。说白了,把 Config 对象传给新建的 Transaction,由 Transaction 创建连接。
Executor 并不是直接执行 SQL 语句,SQL 语句由 MappedStatement 包装,再交给 StatementHandler 执行

Executor

MyBatis 提供 4 种 Executor,他们都继承于 BaseExecutor
BaseExecutor 是一个抽象类,实现了延迟加载、一级缓存(PerpetualCache)等功能
SimpleExecutor 语句使用 PreparedStatement 保存,使用 StatementHandler 处理
ReuseExecutor 与 SimpleExecutor 的区别是它使用一个 Map<String, Statement>来缓存 SQL 语句对应的 Statement,如果某些 Sql 复杂且使用频繁的话可以使用这个执行器,因为这个 Map 不是静态的,并且 MyBatis 实际上会为每个新建的 SqlSession 创建一个 Executor,所以这个缓存只在同一个 Session 内有效

1
2
3
4
5
6
7
8
9
10
private final Map<String, Statement> statementMap = new HashMap<String, Statement>();
if (hasStatementFor(sql)) {
//如果缓存中已经有了,直接得到Statement
stmt = getStatement(sql);
} else {
//如果缓存没有找到,则和SimpleExecutor处理完全一样,然后加入缓存
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
putStatement(sql, stmt);
}

BatchExecutor(批量执行器) 将一些 SQL 语句放在一个 List 中,最后 doFlushStatements 一块执行,并且如果两个相邻的 SQL 语句是相同的,还会复用前一个 Statement 对象。
CachingExecutor(二级缓存执行器) 为什么说是二级缓存?一级缓存由 BaseExecutor 中的 PerpetualCache 实现,CachingExecutor 会先在二级缓存中查找,如果找不到再委托给 delegate 执行,delegate 是 BaseExecutor 的子类,当然有一级缓存的功能。

参数类型和返回值

我们很多时候会指定 parameterType 和 resultType 为复杂类型,怎么将这些类型和数据库表结构进行映射正是 orm 框架的任务之一。
parameterType 表示传入参数类型,在 sql 语句中可以使用#{参数名}来调用,比如

1
sqlSession.selectOne("com.tallate.UserMapper.selectUser", 1);

传入了一个 Integer 类型的参数 1,那么 PreparedStatementHandler 在准备语句时,应该对这个参数的类型进行判断,这个是由 ParameterHandler 负责的。
resultType 表示返回值类型,PreparedStatementHandler 在获得 ResultSet 后应该将查询到的表记录转换为 Java 对象,这个是由 ResultSetHandler 负责的,它最终会调用对应类的构造函数将查询出的结果传入。

动态代理

我们平常使用 MyBatis 时都会定义一个 XXMapper 接口,对应 mapper.xml 中的一个 namespace,而且我们也不必显示写出其实现类,调用过程都是由动态代理实现的。
一般来说,代理类和被代理类应该实现相同的接口,但是现在我们的被代理类是一个 xxmapper.xml 文件,所以问题现在变成了:怎么将 xxmapper.xml 文件转换成被代理类。
查看源码中的 MapperProxyFactory 和 MapperProxy 可以知道,MyBatis 实现 Mapper 接口其实是调用了 SqlSession 中的方法(select、selectOne 等,已经实现了),但是它们的方法名并不相同,比如 selectUser 怎么和 selectOne 关联上呢?
MapperProxy 的 invoke 方法并不是直接调用被代理对象的方法,而是使用 MapperMethod 来表示映射的方法,通过 MapperMethod 可以判断接口方法的返回值、方法名等来确定应该调用 SqlSession 的哪个方法

  1. 启动时 XMLConfigBuilder 会为 config.xml 中所有 mapper 节点扫描包下所有映射器
  2. 创建对应的映射 interface -> MapperProxyFactory
  3. 添加动态代理对象到 MapperRegistry 中(我为了方便,直接加到 Config 中了,其实是刚开始对 MapperRegistry 的功能理解错了…)
  4. 之后每次 getMapper,都可以根据接口名来找到对应的动态代理对象,调用方法时实际上是调用了相应的 MapperMethod

并发

有哪些资源是中心化的?如果是,会不会被多线程同时访问?在 web 环境中,假设每个用户代表一个线程,当他们同时访问服务器就会出现并发问题。

  • 线程池(数据源)
    如果线程池是使用链表(LinkedList)实现的,可以使用 Collections.synchronizedList 进行包装,或者直接使用 Vector
  • Map<String, MappedStatement> MappedStatements
    使用 Map 容器储存 MappedStatement,MappedStatement 表示调用语句到 sql 语句的映射,比如”namespace.selectUser”到 mapper.xml 中对应的 sql 语句(使用 SqlSource 包装)。
  • List environments
    表示 config.xml 中注册的所有环境对象列表
  • List mappers
    表示 config.xml 中注册的所有 mapper 的列表
  • Map<Method, MapperMethod> methodCache
    MapperProxyFactory 中的映射器方法缓冲是使用 ConcurrentHashMap 实现的

QA

  1. 一级缓存不够吗?为什么要有二级缓存?
    一级缓存是会话级缓存,在 BaseExecutor 中,是成员变量,生命周期在一个 SqlSession 内,连接断开就没了;
    二级缓存是语句级缓存,在 MappedStatement 中,可以跨多个 SqlSession,当一些数据不常发生变化或者允许偶尔的并发的时候,二级缓存可能更有效率。
  2. 为什么不推荐使用 MyBatis 中的缓存?
    一级缓存会产生脏数据。因为作用范围是会话,如果有俩会话,会话 1 加载数据到缓存,会话 2 修改该条数据,之后会话 1 读到的是缓存里的老数据。
    二级缓存同样会产生脏数据。二级缓存作用范围是语句,需要手动刷新或在 xml 中配置需要刷新,一般在写入操作和事务提交后都需要刷新一下。但是如果表 A 的 Amapper.xml 中关联了表 B,即使表 B 的数据有变更,我们在 Amapper.xml 中执行查询语句仍然会读到缓存中的脏数据。
  3. MyBatis 与 JDBC 对象之前的关联?
    ParameterStatement - ParameterStatementHandler
    SimpleStatement - SimpleStatementHandler
    ResultSet - ResultSetHandler