MyBatis 核心配置综述之Executor

上一篇我们对SqlSessionSqlSessionFactory的创建过程有了一个详细的了解,但上述的创建过程只是为 SQL 执行和 SQL 映射做了基础的铺垫而已,就和我们 Spring 源码为 Bean 容器的加载进行许多初始化的工作相同,那么做好前期的准备工作接下来该做什么了?该做数据库连接驱动管理和 SQL 解析工作了!那么本篇本章就来讨论一下数据库驱动连接管理和 SQL 解析的管理组件之 Executor 执行器。

MyBatis 四大组件之 Executor 执行器

每一个 SqlSession 都会拥有一个 Executor 对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为 JDBC 中 Statement 的封装版。

Executor 的继承结构

如图所示,位于继承体系最顶层的是 Executor 执行器,它有两个实现类,分别是BaseExecutorCachingExecutor

BaseExecutor 是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式之接口适配的体现,是 Executor 的默认实现,实现了大部分 Executor 接口定义的功能,降低了接口实现的难度。BaseExecutor 的子类有三个,分别是SimpleExecutorReuseExecutorBatchExecutor

SimpleExecutor: 简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个 Statement 对象,用完就直接关闭 Statement 对象 (可以是 Statement 或者是 PreparedStatment 对象)

ReuseExecutor: 可重用执行器,这里的重用指的是重复使用 Statement,它会在内部使用一个 Map 把创建的 Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在 Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。因为每一个 SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的 Statement 作用域是同一个 SqlSession。

BatchExecutor: 批处理执行器,用于将多个 SQL 一次性输出到数据库

CachingExecutor: 缓存执行器,先从缓存中查询结果,如果存在,就返回;如果不存在,再委托给 Executor delegate 去数据库中取,delegate 可以是上面任何一个执行器

Executor 创建过程以及源码分析

上面我们分析完 SqlSessionFactory 的创建过程的准备工作后,我们下面就开始分析会话的创建以及 Executor 的执行过程。

在创建完 SqlSessionFactory 之后,调用其openSession方法:

SqlSession sqlSession = factory.openSession();

SqlSessionFactory 的默认实现是 DefaultSqlSessionFactory,所以我们需要关心的就是 DefaultSqlSessionFactory 中的 openSession() 方法

openSession 调用的是openSessionFromDataSource方法,传递执行器的类型,方法传播级别,是否自动提交,然后在 openSessionFromDataSource 方法中会创建一个执行器

public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 得到 configuration 中的 environment
final Environment environment = configuration.getEnvironment();
// 得到 configuration 中的事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 获取执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 返回默认的 SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause:" + e, e);
} finally {
ErrorContext.instance().reset();
}
}

调用 newExecutor 方法,根据传入的 ExecutorType 类型来判断是哪种执行器,然后执行相应的逻辑

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // defaultExecutorType 默认是简单执行器, 如果不传 executorType 的话,默认使用简单执行器
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 根据执行器类型生成对应的执行器逻辑
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    // 如果允许缓存,则使用缓存执行器
  	// 默认是 true,如果不允许缓存的话,需要手动设置
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
  	// 插件开发。
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

ExecutorType 的选择:

ExecutorType 来决定 Configuration 对象创建何种类型的执行器,它的赋值可以通过两个地方进行赋值:

  • 可以通过标签来设置当前工程中所有的 SqlSession 对象使用默认的 Executor
<settings>
<!-- 取值范围 SIMPLE, REUSE, BATCH -->
	<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
  • 另外一种直接通过 Java 对方法赋值的方式
session = factory.openSession(ExecutorType.BATCH);

ExecutorType 是一个枚举,它只有三个值 SIMPLE, REUSE, BATCH

创建完成 Executor 之后,会把 Executor 执行器放入一个 DefaultSqlSession 对象中来对四个属性进行赋值,他们分别是 configurationexecutordirtyautoCommit

Executor 接口的主要方法

Executor 接口的方法还是比较多的,这里我们就几个主要的方法和调用流程来做一个简单的描述

大致流程

Executor 中的大部分方法的调用链其实是差不多的,下面都是深入源码分析执行过程,如果你没有时间或者暂时不想深入研究的话,给你下面的执行流程图作为参考。

query() 方法

query 方法有两种形式,一种是直接查询;一种是从缓存中查询,下面来看一下源码

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

当有一个查询请求访问的时候,首先会经过 Executor 的实现类CachingExecutor,先从缓存中查询 SQL 是否是第一次执行,如果是第一次执行的话,那么就直接执行 SQL 语句,并创建缓存,如果第二次访问相同的 SQL 语句的话,那么就会直接从缓存中提取

CachingExecutor.j

	// 第一次查询,并创建缓存
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

MapperStatement维护了一条 <select|update|delete|insert> 节点的封装,包括资源 (resource),配置(configuration),SqlSource(sql 源文件) 等。使用 Configuration 的 getMappedStatement 方法来获取 MappedStatement 对象

BoundSql这个类包括 SQL 的基本信息,基本的 SQL 语句,参数映射,参数类型等

上述的 query 方法会调用到 CachingExecutor 类中的 query 查询缓存的方法

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
  // 得到缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 如果需要的话刷新缓存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); 
      }
      return list;
    }
  }
  // 委托模式,交给 SimpleExecutor 等实现类去实现方法。
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

由 delegate 执行 query 方法,delegate 即是BaseExecutor,然后由具体的执行器去真正执行 query 方法

注意:源码中一般以 do** 开头的方法都是真正加载执行的方法

// 经过一系列的调用,会调用到下面的方法 (与主流程无关,故省略)
// 以 SimpleExecutor 简单执行器为例
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 获取环境配置
    Configuration configuration = ms.getConfiguration();
    // 创建 StatementHandler,解析 SQL 语句
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 由 handler 来对 SQL 语句执行解析工作
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

由上面的源码可以看出,Executor 执行器所起的作用相当于是管理 StatementHandler 的整个生命周期的工作,包括创建、初始化、解析、关闭。

ReuseExecutor完成的 doQuery 工作:几乎和 SimpleExecutor 完成的工作一样,其内部不过是使用一个 Map 来存储每次执行的查询语句,为后面的 SQL 重用作准备。

BatchExecutor完成的 doQuery 工作:和 SimpleExecutor 完成的工作一样。

update() 方法

在分析完上面的查询方法后,我们再来聊一下 update()方法,update() 方法不仅仅指的是update()方法,它是一条 update 链,什么意思呢?就是 *insert、update、delete 在语义上其实都是更新的意思,而查询在语义上仅仅只是表示的查询,那么我们来偷窥一下 update 方法的执行流程,与 select 的主要执行流程很相似,所以一次性贴出。

// 首先在顶级接口中定义 update 方法,交由子类或者抽象子类去实现

// 也是首先去缓存中查询是否具有已经执行过的相同的 update 语句
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}

// 然后再交由 BaseExecutor 执行 update 方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}

// 往往 do* 开头的都是真正执行解析的方法,所以 doUpdate 应该就是真正要执行 update 链的解析方法了
// 交给具体的执行器去执行
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

ReuseExecutor完成的 doUpdate 工作:几乎和 SimpleExecutor 完成的工作一样,其内部不过是使用一个 Map 来存储每次执行的更新语句,为后面的 SQL 重用作准备。

BatchExecutor完成的 doUpdate 工作:和 SimpleExecutor 完成的工作相似,只是其内部有一个 List 列表来一次行的存储多个 Statement,用于将多个 sql 语句一次性输送到数据库执行.

queryCursor() 方法

我们查阅其源码的时候,在执行器的执行过程中并没有发现其与 query 方法有任何不同之处,但是在doQueryCursor 方法中我们可以看到它返回了一个 cursor 对象,网上搜索 cursor 的相关资料并查阅其基本结构,得出来的结论是:用于逐条读取 SQL 语句,应对数据量

// 查询可以返回 Cursor<T> 类型的数据,类似于 JDBC 里的 ResultSet 类,
// 当查询百万级的数据的时候,使用游标可以节省内存的消耗,不需要一次性取出所有数据,可以进行逐条处理或逐条取出部分批量处理。
public interface Cursor<T> extends Closeable, Iterable<T> {
<span class="hljs-type">boolean</span> <span class="hljs-title function_">isOpen</span><span class="hljs-params">()</span>;

<span class="hljs-type">boolean</span> <span class="hljs-title function_">isConsumed</span><span class="hljs-params">()</span>;

<span class="hljs-type">int</span> <span class="hljs-title function_">getCurrentIndex</span><span class="hljs-params">()</span>;

}

flushStatements() 方法

flushStatement()的主要执行流程和 query,update 的执行流程差不多,我们这里就不再详细贴代码了,简单说一下 flushStatement() 的主要作用,flushStatement() 主要用来释放 statement,或者用于 ReuseExecutor 和 BatchExecutor 来刷新缓存

createCacheKey() 方法

createCacheKey() 方法主要由 BaseExecutor 来执行并创建缓存,MyBatis 中的缓存分为一级缓存和二级缓存,关于缓存的讨论我们将在 Mybatis 系列的缓存章节

Executor 中的其他方法

Executor 中还有其他方法,提交 commit,回滚 rollback,判断是否时候缓存 isCached,关闭 close,获取事务 getTransaction 一级清除本地缓存 clearLocalCache 等

Executor 的现实抽象

在上面的分析过程中我们了解到,Executor 执行器是 MyBatis 中很重要的一个组件,Executor 相当于是外包的 boss,它定义了甲方 (SQL) 需要干的活 (Executor 的主要方法),这个外包公司是个小公司,没多少人,每个人都需要干很多工作,boss 接到开发任务的话,一般都找项目经理(CachingExecutor),项目经理几乎不懂技术,它主要和技术 leader(BaseExecutor) 打交道,技术 leader 主要负责框架的搭建,具体的工作都会交给下面的程序员来做,程序员的技术也有优劣,高级程序员(BatchExecutor)、中级程序员(ReuseExecutor)、初级程序员(SimpleExecutor),它们干的活也不一样。一般有新的项目需求传达到项目经理这里,项目经理先判断自己手里有没有现成的类库或者项目直接套用(Cache),有的话就直接让技术 leader 拿来直接套用就好,没有的话就需要搭建框架,再把框架存入本地类库中,再进行解析。