Mybatis源码之Executor

本系列文章会在我的个人公众号“兮一昂吧”和掘金同步更新,欢迎关注!

上一篇文章《Mybatis源码之SqlSession》聊到了SqlSession其实是一个包工头儿,揽了活自己不干都安排给了执行器;而且在《Mybatis源码之SQL执行过程》中已经了解到,执行器通过对StatementHandler生命周期的调度与管理,最终完成SQL命令执行。

今天我们就来认识一下Mybatis的四大组件之一:Executor-执行器。提前说明一下,由于Executor涉及到Mybatis的一级缓存、二级缓存,这部分内容后面会单独分析,本文先不展开。

Executor简介

Executor位于executor包,Mybatis中所有的SQL命令都由它来调度执行。首先通过UML类图认识一下Executor家族:

Mybatis源码之Executor
image.png
  • 最顶层是Executor接口,定义了查询、更新、事务、缓存操作相关的接口方法,Executor接口对外暴露,由SqlSession依赖,并受其调度与管理。

  • 第二层左侧为BaseExecutor,它是一个抽象类,实现了大部分Executor的接口。它有三个子类,分别是SimpleExecutor、ReuseExecutor、BatchExecutor。BaseExecutor及其子类完成了一级缓存管理和与数据库交互有关的操作。

  • 第二层右侧为CachingExecutor,缓存执行器,Mybatis二级缓存的核心处理类。CachingExecutor持有一个BaseExecutor的实现类(SimpleExecutor、ReuseExecutor或BatchExecutor)实例作为委托执行器。它主要完成Mybatis二级缓存处理逻辑,当缓存查询中不存在或查询不到结果时,会通过委托执行器查询数据库。

  • 第三层就是BaseExecutor的三个子类。简单执行器为默认执行器,具备执行器的所有能力;可重用执行器,是相对简单执行器而言的,它具备MappedStatement的缓存与复用能力,即在一个SqlSession会话内重复执行同一个命令,可以直接复用已缓存的MappedStatement;批量执行器,即一次可以执行多个命令。

Executor的核心功能是调度执行SQL,参与了全过程;为了提高查询性能,Mybatis在Executor中设计了一级缓存和二级缓存,一级缓存由BaseExecutor及其子类实现,二级缓存由CachingExecutor实现。一级缓存是默认开启的,二级缓存需要启用配置。由于CachingExecutor负责缓存管理,真正的数据库查询是由BaseExecutor完成的,所以对外来看,Executor有三种类型SIMPLE、REUSE、BATCH,默认是SIMPLE,我们可以在配置文件或创建SqlSession时指定参数修改默认执行器类型。以下为Mybatis中ExecutorType定义:

1public enum ExecutorType {
2    // 简单执行器
3    SIMPLE, 
4    // 可重用的执行器
5    REUSE, 
6    // 批处理执行器
7    BATCH
8}

创建过程分析

前面已经说到,SqlSession依赖Executor,Executor接受SqlSession的请求并执行,而且Executor的创建是随着SqlSession的创建而创建的。Executor的创建流程始于DefaultSqlSessionFactory#openSession()及其重载方法,代码如下:

 1  @Override
2  public SqlSession openSession() {
3    //这里使用默认的执行器类型:SIMPLE
4    return openSessionFromDataSource(configuration.getDefaultExecutorType(), nullfalse);
5  }
6
7  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
8    Transaction tx = null;
9    try {
10      final Environment environment = configuration.getEnvironment();
11      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
12      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
13      //通过configuration.newExecutor方法创建执行器
14      final Executor executor = configuration.newExecutor(tx, execType);
15      return new DefaultSqlSession(configuration, executor, autoCommit);
16    } catch (Exception e) {
17      closeTransaction(tx); // may have fetched a connection so lets call close()
18      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
19    } finally {
20      ErrorContext.instance().reset();
21    }
22  }

简单分析一下这段代码:这里采用了无参openSession方法,其内部使用Configuration#defaultExecutorType作为执行器类型调用openSessionFromDataSource方法;后者对execType未做处理调用了Configuration#newExecutor()执行Executor创建流程。

Configuration#defaultExecutorType默认值为SIMPLE,它会在Mybatis解析配置文件时被修改。若配置文件未涉及executorType的配置,默认值不会改变。另外,可以调用openSession的重载方法指定执行器类型。两种设置方式如下所示:

  • Mybatis默认(缺省)的执行器类型是SIMPLE,我们可以在配置文件中进行修改设置:

1    <settings>
2        <setting name="defaultExecutorType" value="SIMPLE"/>
3    </settings>
  • 或者通过代码进行设置:

1// 指定执行器类型为REUSE
2try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
3    //……    
4}

了解了ExecutorType的设置方式,接着看Configuration#newExecutor()方法:

 1//Executor执行器创建方法
2  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
3    //executorType为null:使用默认执行器,否则按照入参类型
4    //看Configuration代码,其实有:protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
5    executorType = executorType == null ? defaultExecutorType : executorType;
6    //executorType和defaultExecutorType都为null,默认使用SIMPLE
7    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
8    Executor executor;
9    //按照executorType创建不同的执行器。
10    if (ExecutorType.BATCH == executorType) {
11      executor = new BatchExecutor(this, transaction);
12    } else if (ExecutorType.REUSE == executorType) {
13      executor = new ReuseExecutor(this, transaction);
14    } else {
15      executor = new SimpleExecutor(this, transaction);
16    }
17    //这里判断是否开启了二级缓存,默认值是true。
18    //所以如果没有修改配置文件的话,这个if语句是会进入执行的
19    if (cacheEnabled) {
20      //创建缓存执行器,同时把上面创建的executor作为其委托执行器
21      executor = new CachingExecutor(executor);
22    }
23    //这里是加载与Executor有关的插件。
24    executor = (Executor) interceptorChain.pluginAll(executor);
25    return executor;
26  }

总结一下执行过程:

  • 首先:处理执行器类型参数,确保其不为空,最终以SIMPLE兜底;

  • 其次:根据执行器类型调用对应的执行器实现类做初始化,执行器可以为SimpleExecutor、ReuseExecutor、BatchExecutor中的一个;

  • 然后:如果启用了二级缓存(cacheEnabled默认是true,但是如果不设置cache的相关参数,二级缓存是不起作用的),创建缓存执行器,同时把上面创建的executor作为缓存执行器的委托执行器;

  • 最后:加载与Executor有关的插件。

默认情况下,会返回一个CachingExecutor对象,其内部包装了BaseExecutor的某个子类对象。

Mybatis源码之Executor
image.png

query流程

Executor接口中query方法有两个重载,我们从参数少的这个开始入手,它内部也会调用到另外一个。默认情况下会使用CachingExecutor,所以我们从CachingExecutor#query()开始:

 1  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
2    // 获取BoundSql
3    BoundSql boundSql = ms.getBoundSql(parameterObject);
4    // 创建缓存key
5    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
6    // 调用重载query方法
7    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
8  }
9
10  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
11      throws SQLException 
{
12    // 获取缓存对象,启用二级缓存才会有
13    Cache cache = ms.getCache();
14    // 缓存不空
15    if (cache != null) {
16      // 刷新缓存
17      flushCacheIfRequired(ms);
18      if (ms.isUseCache() && resultHandler == null) {
19        ensureNoOutParams(ms, boundSql);
20        @SuppressWarnings("unchecked")
21        // 从缓存中查询
22        List<E> list = (List<E>) tcm.getObject(cache, key);
23        if (list == null) {
24          // 缓存中没有,通过委托查询
25          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
26          tcm.putObject(cache, key, list); // issue #578 and #116
27        }
28        return list;
29      }
30    }
31    //默认情况没有开启二级缓存,会直接走到这里
32    //delegate即BaseExecutor三个子类的其中一个
33    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
34  }

进入CachingExecutor#query,首先通过MappedStatement获取BoundSql,创建缓存key,然后调用了重载的query方法。重载query查询在不考虑缓存的情况下,会直接通过委托执行器的query方法进行查询。

这里的委托执行器为BaseExecutor的子类,而BaseExecutor实现了query方法,所以我们先进入BaseExecutor#query()(同样先忽略一级缓存部分的逻辑):

 1public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
2    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
3    if (closed) {
4      throw new ExecutorException("Executor was closed.");
5    }
6    if (queryStack == 0 && ms.isFlushCacheRequired()) {
7      clearLocalCache();
8    }
9    List<E> list;
10    try {
11      queryStack++;
12      //从一级缓存(本地缓存)中查询
13      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
14      if (list != null) {
15        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
16      } else {
17        //缓存中不存在,从数据库中查询
18        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
19      }
20    } finally {
21      queryStack--;
22    }
23    if (queryStack == 0) {
24      for (DeferredLoad deferredLoad : deferredLoads) {
25        deferredLoad.load();
26      }
27      // issue #601
28      deferredLoads.clear();
29      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
30        // issue #482
31        clearLocalCache();
32      }
33    }
34    return list;
35  }
36
37  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
38    List<E> list;
39    //缓存占位
40    localCache.putObject(key, EXECUTION_PLACEHOLDER);
41    try {
42      //调用抽象方法执行数据库查询,子类实现
43      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
44    } finally {
45      //移除占位
46      localCache.removeObject(key);
47    }
48    //设置缓存
49    localCache.putObject(key, list);
50    if (ms.getStatementType() == StatementType.CALLABLE) {
51      localOutputParameterCache.putObject(key, parameter);
52    }
53    return list;
54  }

两个方法比较长,大部分是缓存处理。忽略缓存的情况下(直接看我注释的部分),从query方法调用了数据库查询方法queryFromDatabase,但是,真正查询的逻辑是在抽象方法doQuery中实现的,doQuery由BaseExecutor子类实现。我们依次看下子类实现逻辑:

SimpleExecutor#doQuery

 1  @Override
2  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
3    Statement stmt = null;
4    try {
5      //获取配置对象
6      Configuration configuration = ms.getConfiguration();
7      //创建StatementHandler
8      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
9      //准备Statement
10      stmt = prepareStatement(handler, ms.getStatementLog());
11      //执行查询
12      return handler.query(stmt, resultHandler);
13    } finally {
14      //关闭Statement
15      closeStatement(stmt);
16    }
17  }
18
19  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
20    Statement stmt;
21    Connection connection = getConnection(statementLog);
22    stmt = handler.prepare(connection, transaction.getTimeout());
23    handler.parameterize(stmt);
24    return stmt;
25  }

SimpleExecutor#doQuery方法代码比较简单,过程清晰明了,简单说明一下:

  • 从MappedStatement对象获取全局Configuration配置对象;

  • 调用Configuration#newStatementHandler创建StatementHandler对象;

  • 创建并初始化Statement对象;

  • 调用StatementHandler#query执行Statement,并使用resultHandler解析返回值;

  • 最后关闭Statement。

从上述流程可知,doQuery方法调度StatementHandler完成了对Statement的初始化、参数设置、执行、结果处理与关闭,是对Statement整个生命周期的管理与控制,与前文所说的Executor参与了SQL语句执行的全过程名副其实。

ReuseExecutor#doQuery

 1  //Statement缓存
2  private final Map<String, Statement> statementMap = new HashMap<>();
3
4  @Override
5  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
6    Configuration configuration = ms.getConfiguration();
7    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
8    Statement stmt = prepareStatement(handler, ms.getStatementLog());
9    return handler.query(stmt, resultHandler);
10  }
11
12  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
13    Statement stmt;
14    BoundSql boundSql = handler.getBoundSql();
15    String sql = boundSql.getSql();
16    //检查缓存中是否存在当前sql
17    if (hasStatementFor(sql)) {
18      //如果有,就直接拿出来用
19      stmt = getStatement(sql);
20      applyTransactionTimeout(stmt);
21    } else {
22      //如果没有,就新建一个
23      Connection connection = getConnection(statementLog);
24      stmt = handler.prepare(connection, transaction.getTimeout());
25      //然后,缓存起来。
26      putStatement(sql, stmt);
27    }
28    handler.parameterize(stmt);
29    return stmt;
30  }

ReuseExecutor#doQuery与SimpleExecutor#doQuery的逻辑基本一致,不同点在于prepareStatement方法的实现逻辑。prepareStatement使用statementMap对执行过的sql进行缓存,只有statementMap中不存在当前sql的时候才会执行创建流程,对性能有一定的提升。需要注意的是,Executor对象是SqlSession的组成部分,所以这个缓存与SqlSession的生命周期一致。

BatchExecutor#doQuery

 1  @Override
2  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
3      throws SQLException 
{
4    Statement stmt = null;
5    try {
6      //批量执行,目前我理解是为了把之前批量更新的语句执行掉
7      flushStatements();
8      //获取Configuration对象
9      Configuration configuration = ms.getConfiguration();
10      //创建StatementHandler
11      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
12      //创建Statement
13      Connection connection = getConnection(ms.getStatementLog());
14      stmt = handler.prepare(connection, transaction.getTimeout());
15      //设置Statement参数
16      handler.parameterize(stmt);
17      //执行并返回结果
18      return handler.query(stmt, resultHandler);
19    } finally {
20      closeStatement(stmt);
21    }
22  }

BatchExecutor#doQuery方法除了多执行flushStatements方法外,与SimpleExecutor基本一致,不再展开。

update()流程

update()方法的对应了insert、update、delete等不同的命令,其执行流程与query()方法流程类似。同样是经过CachingExecutor->BaseExecutor->SimpleExecutor/ReuseExecutor/BatchExecutor。把整个过程的代码一起贴出来分析(注释):

 1  //org.apache.ibatis.executor.CachingExecutor#update
2  @Override
3  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
4    // 刷新缓存:开启缓存时,update命令默认情况是需要刷新缓存的
5    flushCacheIfRequired(ms);
6    //调用委托执行器进行update
7    return delegate.update(ms, parameterObject);
8  }
9
10  //org.apache.ibatis.executor.BaseExecutor#update
11  @Override
12  public int update(MappedStatement ms, Object parameter) throws SQLException {
13    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
14    if (closed) {
15      throw new ExecutorException("Executor was closed.");
16    }
17    // 清空缓存,然后调用子类的doUpdate方法
18    clearLocalCache();
19    //调用抽象方法执行数据库更新操作
20    return doUpdate(ms, parameter);
21  }

Mybatis的缓存机制仅对查询有效,所以Executor能做的就是:使缓存失效、请求中转,最终调用doUpdate执行数据库操作,所以:

  • CachingExecutor#update首先使二级缓存失效,然后调用委托执行器执行update操作。

  • BaseExecutor#update也是首先使得一级缓存失效,然后调用抽象方法doUpdate执行数据库的更新操作。

SimpleExecutor#doUpdate

SimpleExecutor#doUpdate与doQuery完全一致,不再说明了。

ReuseExecutor#doUpdate

ReuseExecutor#doUpdate与doQuery完全一致,不再说明了。

BatchExecutor#doUpdate

BatchExecutor的执行与前两个不一样,它用于执行批量的sql命令,所以多了一些批量准备工作。为了减少与数据库的交互次数,BatchExecutor会批量执行sql命令。代码如下:

 1  @Override
2  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
3    //获取Configuration对象
4    final Configuration configuration = ms.getConfiguration();
5    //创建StatementHandler
6    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, nullnull);
7    //获取sql对象
8    final BoundSql boundSql = handler.getBoundSql();
9    //获取sql与酒
10    final String sql = boundSql.getSql();
11    final Statement stmt;
12    //如果当前命令与上一次执行一样,就不再重复创建Statement,性能提升
13    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
14      //取出最后一条的索引
15      int last = statementList.size() - 1;
16      //取出最后一个Statement对象
17      stmt = statementList.get(last);
18      applyTransactionTimeout(stmt);
19      //设置Statement参数
20      handler.parameterize(stmt);//fix Issues 322
21      //获取BatchResult
22      BatchResult batchResult = batchResultList.get(last);
23      //设置BatchResult参数对象
24      batchResult.addParameterObject(parameterObject);
25    } else {
26      //如果当前命令与上一次执行不一样,重新创建
27      Connection connection = getConnection(ms.getStatementLog());
28      //初始化、准备Statement
29      stmt = handler.prepare(connection, transaction.getTimeout());
30      //设置参数
31      handler.parameterize(stmt);    //fix Issues 322
32      //设置当前执行的sql命令信息
33      currentSql = sql;
34      currentStatement = ms;
35      //存起来
36      statementList.add(stmt);
37      //保存结果对象
38      batchResultList.add(new BatchResult(ms, sql, parameterObject));
39    }
40    //批量处理
41    handler.batch(stmt);
42    return BATCH_UPDATE_RETURN_VALUE;
43  }

BatchExecutor#doUpdate方法完成了Statement执行前的准备工作,在准备Statement时与上一次要执行的Statement进行对比,如果一致则不再执行重新创建Statement的流程。所以,使用BatchExecutor时应该尽量执行相同的sql命令。

但是,BatchExecutor#doUpdate并未进行数据库的执行操作,它需要通过SqlSession#flushStatements进行触发,然后调用到BatchExecutor#doFlushStatements执行最终的操作,这里就不再展开了。

Executor总结

本文从源码角度对Executor的创建流程、query流程、update流程进行了详细梳理,了解了Executor执行update/query的执行流程,重点说明了其与数据库交互的过程,即Statement的调度管理,Executor的缓存机制我们并没有展开。Executor是Mybatis四大组件之一,虽然还没有研究其他三个,但是已经对Mybatis的SQL执行过程有了整体的了解,由此可见Executor的作用举足轻重。

本文到这里就结束了,希望对您有用!本人水平有限,如您发现有任何错误或不当之处,欢迎批评指正。

欢迎关注我的微信公众号:“兮一昂吧”,点赞、转发、在看就是对我的鼓励!


原文始发于微信公众号(码路印记):Mybatis源码之Executor

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/23806.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!