Mybaits源码解析4——SimpleExecutor、ReuseExecutor、BatchExecutor、CachingExecutor

上一篇对Executor接口进行了大致的分析,这边来针对它的实现进行分析。

现在我们知道,mybatis默认情况下是使用simpleExecutor的,如果你需要修改,有两种方式,一是在setting中配置defualtExecutorType,另一种方式是通过SqlSessionManager指定executorType并创建sqlsession。下面以doUpdate和doQuery方法为例,比较这几种executor的差异,因为executor提供sqlSession的就是这两个核心基础方法。

doUpdate

simpleExecutor:

  @Override
  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());//创建statement
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);//关闭statement,意味着下一次使用需要重新开启statement
    }
  }
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());//创建statement
    handler.parameterize(stmt);//将参数存入statement
    return stmt;
  }

ReuseExecutor:

将statement存入map中:

private final Map statementMap = new HashMap();
  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.update(stmt);//没有关闭statement
  }
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    if (hasStatementFor(sql)) {
      stmt = getStatement(sql);//如果已缓存statement则取出
      applyTransactionTimeout(stmt);
    } else {
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);//否则创建statement并存入
    }
    handler.parameterize(stmt);
    return stmt;
  }

看看它是怎么映射判断已缓存的statement的:

  private boolean hasStatementFor(String sql) {
    try {
      return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();//sql语句就是key,statement是值
    } catch (SQLException e) {
      return false;
    }
  }

BatchExecutor:

  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;//为何?防无限循环

  private final List statementList = new ArrayList();//使用list缓存statement
  private final List batchResultList = new ArrayList();
  private String currentSql;
  private MappedStatement currentStatement;
 @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {//判断当前使用sql和statement是否是上一次的statement和sql
      int last = statementList.size() - 1;
      stmt = statementList.get(last);//如果是则取出
      applyTransactionTimeout(stmt);
     handler.parameterize(stmt);//fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    //fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);//否则创建statement并存入
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
  // handler.parameterize(stmt);
    handler.batch(stmt);//仅仅是存入批处理参数而不执行
    return BATCH_UPDATE_RETURN_VALUE;//始终返回一个常量
  }

在BatchExecutor中的doupdate并不会想前面两者那样执行返回行数,而是每次执行将statement预存到有序集合,官方说明这个executor是用于执行存储过程的和批量操作的,因此这个方法是循环或者多次执行构建一个存储过程或批处理过程。

doQuery

simpleExecutor:

没有什么特别,常规操作

  @Override
  public  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);//也是关闭statement
    }
  }

ReuseExecutor:

  @Override
  public  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);//不关闭,对缓存的statement进行操作
  }

BatchExecutor:

  @Override
  public  List doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      flushStatements();//刷新statement
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);//关闭statement
    }
  }
可以看到,batch的执行方式也有所不同,每次执行查询都会执行刷新statement,为何?如下:
@Override
  public List doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List results = new ArrayList();
      if (isRollback) {
        return Collections.emptyList();
      }
      for (int i = 0, n = statementList.size(); i < n; i++) {//批量执行有序集合中的statement
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          batchResult.setUpdateCounts(stmt.executeBatch());//执行批处理或存储过程
          MappedStatement ms = batchResult.getMappedStatement();
          List parameterObjects = batchResult.getParameterObjects();
          KeyGenerator keyGenerator = ms.getKeyGenerator();
          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter);
            }
          }
          // Close statement to close cursor #1109
          closeStatement(stmt);
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId())
              .append(" (batch index #")
              .append(i + 1)
              .append(")")
              .append(" failed.");
          if (i > 0) {
            message.append(" ")
                .append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  } 
  

其实就是执行doUpdate并不会提交过程或批处理,这个操作只有doQuery才会执行。

到这里,我们可以总结一下SimpleExecutor、ReuseExecutor、BatchExecutor的区别了:

  • SimpleExecutor是一种常规执行器,每次执行都会创建一个statement,用完后关闭。
  • ReuseExecutor是可重用执行器,将statement存入map中,操作map中的statement而不会重复创建statement。
  • BatchExecutor是批处理型执行器,doUpdate预处理存储过程或批处理操作,doQuery提交并执行过程。

另外还有一个执行器,CachingExecutor,之所以没有加入对比,因为这个执行器不是工作在同一个层面上的,什么时候会使用到他?回过头查看如何在configuration中被创建的:

    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }

protected boolean cacheEnabled = true;

可以看出默认是开启的,在XMLConfigBuilder中可以判断,这也是存在配置文件中的一个选项,如果没有设置,则默认开启:

configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));

下面是官方文档:


除此之外,没有别的地方可以修改这个配置,也就是运行时不可更改。剧透一下,这个其实就是二级缓存。

在上一篇executor里面,提到了CachingExecutor其实是baseExecutor的一个包装,进一步说,也就是对SimpleExecutor、ReuseExecutor、BatchExecutor进行包装,现在来研究下进行了什么样的包装:

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);//刷新缓存
    return delegate.update(ms, parameterObject);
  }

对于update方法,在executor执行之前,可选择性的清空缓存,换一个角度,如果这里只是清空缓存,那么必定在它之前会有一个步骤是加入缓存,所以我们看下baseExecutor中的实现:

  @Override
  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);
  }

上述代码中,doUpdate是委托,不展开,关键是在这之前执行的clearLocalCache方法,看样子是清空本地的缓存:

  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();//清空本地
      localOutputParameterCache.clear();//清空本地输出
    }
  }

这两个本地缓存是什么,可以推测就是一级缓存,因为它内置于executor中,而executor内置于一个SqlSession中,所以这个应该就是session级别的缓存:

  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;

这里不关心缓存是怎么来的,只关心如何处理,因为缓存可能有很多种类型,不会是一个简单的map,如果深究进去,就偏离主题了,在另一篇里面再做缓存解析。

回归主题,从上面的过程判断,缓存在update方法中没有添加,反而可能会清空,其原因一个是对update的执行语句进行缓存意义不大,另一方面就是update之后缓存是过时的,那么在什么时候会触发添加缓存操作呢?结合实际我们实际操作分为update和query两种,也就是说缓存应该是query方法执行的时候触发添加的。而对于query方法,从executor接口中的定义可以看出对应的性质:

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

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

第一个query的参数定义中具有cachekey和boundsql,可以推测该方法要求实现的是带缓存的操作。这个时候需要注意了,这是接口方法,而CachingExecutor和BaseExecutor都是实现子类,如果他们的实现要求不存在冗余(职责单一),那么使用包装模式是必然的了,这时候应当将看源码的方式从尾查找切换为从根查找,先看BaseExecutor中的的一个query:

  @Override
  public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);//将statement、parameter等参数构建一个CacheKey
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);//执行带缓存的query
 }

可以看出,executor设计的两个query方法是考虑重用的,第一个query是可以被第二个query方法重用的,进一步看第二个query,在这之前,会先将一些数据作为CacheKey并创建缓存的key:

 @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());//可见加入缓存的方式并不是键值对的形式,而是统统丢入其中
    List parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {//如果sql有额外的参数则存入参数
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {//否则如果入参为null则存入null
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {//否则如果入参是经过TypeHandler处理则存入
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);//否则取入参的值(根据元信息的类型获取)
        }
        cacheKey.update(value);//存入
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

通过上面的代码可以发现,executor使用的本地缓存的键是一组数据,包括了statement的id、查询其实行、查询行数、sql、参数等。CacheKey不再本文讨论范围只能,而我们现在已经知道一级缓存的key就是通过这些数据产生的,接下来看第二个query方法如何处理:

  @Override
  public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List list;
    try {
      queryStack++;//用一个int来标识操作状态
      list = resultHandler == null ? (List) localCache.getObject(key) : null;//从缓存中取出查询结果list
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);//如果list存在则输出参数
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);//list不存在则执行查询
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();//将延迟加载清单中的条目加载
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {//如果范围级别是statement则清空本地缓存
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

如果传入了resultHandler,则不会从缓存中查找已查询的结果,如果没有结果,再执行查询,后面还会是根据配置文件条目来判断是否保持缓存,默认开启,见官方说明,实际上使用了取巧的办法,每次都是清空缓存-。-:


最终查询核心工作交给queryFromDatabase方法来完成:

  private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);//先缓存一个占位符
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);//缓存查询结果list
    if (ms.getStatementType() == StatementType.CALLABLE) {//缓存输出参数,仅Callable类型的statement支持
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
经过上面的过程可以推断,实际上,CachingExecutor的query的操作,尤其是一级缓存的操作,在baseExecutor中已经实现,那么CachingExecutor做了什么?来看一下,第一个query和baseExecutor中无异:
  @Override
  public  List 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);
  }

第二个query就有区别了:

  @Override
  public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);//如果statement有缓存并且设置为必需刷新则清空二级缓存
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);//确保statement没有输出参数
        @SuppressWarnings("unchecked")
        List list = (List) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;//如果list存在则返回二级缓存的list,否则执行查询并缓存结果list
      }
    }
    return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如官方所述那样,一个查询statement如果已被缓存,那么优先使用二级缓存,否则会执行委托executor,也即是baseExecutor的一级缓存。

上面清空缓存的工作由一个事务缓存管理器处理,所以说二级缓存又称为事务缓存:

 private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();//从mappedstatement中取出缓存对象
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);//清空缓存
    }
  }

需要注意的是,这种缓存对于Callable类型的statement的输出参数是无效的,而且还会抛出异常,见下:

  private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
    if (ms.getStatementType() == StatementType.CALLABLE) {
      for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
        if (parameterMapping.getMode() != ParameterMode.IN) {
          throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
        }
      }
    }
  }

到这里,所有的executor解析完毕,下一篇将会对statement进行解析。本篇产出的类关系结构图如下:


你可能感兴趣的:(源码解析)