mybatis源码解读(七):Executor详解

功能

在 mybatis 中执行语句时,有个非常重要的类就是 Executor,Executor 有些类似是mybatis的心脏,它负责这次语句执行操作的资源调度、流程执行等功能。Executor是一个接口,它有几个实现类,在前边sqlsession构建时已经分析过,mybatis默认的策略是 SimpleExecutor ,它完全继承自 BaseExecutor,没有特殊的地方。
Execute所要完成的工作:

  1. 根据当前配置的id找到MappedStatement,然后处理sqlSource拿到BoundSource,在这里会顺便去检查一下paramterMapping是不是正确如果有的话。
  2. 创建一个 CacheKey,CacheKey的作用是作为缓存的key,既然要能成为key就得保证唯一,CacheKey会将你当前要操作的关于sql的一些重要信息以栈(list)的方式存储到CacheKey中来保证唯一,在之后的流程执行的时候会一个个弹出来,可以保证顺序,这是一个很巧妙的设计,通过这种方式保证每个重要参数都不会丢失并且还能保证执行顺序。
  3. 执行sql语句,以query来讲,执行的时候会去判断有没有开启二级缓存,然后有没有允许每次执行刷新清空缓存,然后判断一级缓存中是否缓存(一级缓存是默认开启的,每次查询都会强制缓存一份结果,有策略控制),然后就是准备statement语句调用驱动去执行,封装返回结果。

UML

在这里插入图片描述
Executor在mybatis中有三个实现类,baseExecutor是一个基类,包含一些共有的功能。

  • SimpleExecutor 不做任何处理,普通的执行器
  • ReuseExecutor 复用已经处理好的语句,也就是prepareStatement,在本次会话中再次调用时不会再去解析,有些场景下可以考虑使用这个节省性能
  • BatchExecutor 允许批量操作,批量场景多的情况下可以考虑使用这个

CachingExecutor 是一个封装执行器,类似装饰者模式,在原有的模式上封装了一些功能,二级缓存就是在 CachingExecutor 中控住的。

代码详解

DefaultSqlSession的selectOne方法内部其实是很简单的,它将MappedStatement的 id 以及入参传入进来,在内部会去调用selectList方法,结果只取第一个元素,如果返回的结果超过1则会报错,这个错误大家也熟悉,是经常见的一个错误,就是这里报出来的。

User user = sqlSession.selectOne("org.apache.ibatis.test.UserMapper.selectById", 1);

// selectOne方法,在内部调用selectList方法
public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

selectList() 方法中继续调用一个重载方法,在这里有一个类是RowBounds,这个类是mybatis的默认分页,内部有offset以及limit等属性来控制分页,limit的默认值是Integer的最大值
selectList 重载方法中会根据你传入进来的id 去configuration中获取到当前的MappedStatement,然后调用executor去执行query()方法,这里的executor其实是CachingExecutor ,这个在sqlsession的获取时说明过。

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

CachingExecutor 的方法逻辑中分这几步,第一步是获取 BoundSql ,第二部是根据查询条件构造 CacheKey ,第三步是执行query方法

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
	// 第一步获取BoundSql 
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 第二步获取 CacheKey 
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // 第三步查询
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

Boundsql是sql语句在执行时的最终状态,sqlSource是mybatis在解析过程中可以存储到configuration中的形态,到了执行时会在这里将sql解析成为Boundsql,在Boundsql中有sql(调用sqlNode的apply方法解析后的sql),入参mapping,parameterObject(入参参数),metaParameters(反射工具类)这些信息,方便在之后的执行时使用。
在这里插入图片描述
getBoundSql()方法中,主要逻辑在 DynamicSqlSource.getBoundSql() 方法中,在其内会去循环调用所有SqlNode的apply()方法解析sql,包括 $ {}这种替换成相应的值,$ {}这种替换是在apply()中完成的,之前是根据配置属性,这里是根据入参。
再解析完sqlNode之后这里的sql已经全部都是静态文本了,之后会去重新构建解析一次sqlSource,防止之前有些没有替换掉的 # {} 处理完,这里的解析肯定会返回staticSqlSource类型,再次调用getBoundSql就会拿到BoundSql

public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
	/.../
    return boundSql;
  }
 // sqlSource.getBoundSql()
public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 执行各个sqlNode的apply方法去解析动态Sql,循环调用
    rootSqlNode.apply(context);
    // SqlSourceBuilder,主要是替换 #{}占位符,
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // 调用 staticSource()的方法
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

rootSqlNode.apply(context) 这个方法其实是调用的MixedSqlNode.apply方法,之前解析的时候应该已经说明,在内部可以看到会去循环调用apply()方法

public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }

之后SqlSource的parse()与之前的parse方法一致,不多做介绍,在getBoundSql()方法中其实也是很简单,就是同构构造方法构造了一个BoundSql

public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

回到 CachingExecutor 的query方法中,获取到BoundSql 之后就是构造CacheKey,缓存的唯一key,具体逻辑是在 BaseExecutor 类的 createCacheKey 方法中,他会将当前MappedStatement的id,分页逻辑的起始位置,最大长度以及sql都以类似栈(List)结构的形势存储起来并且根据算法计算一个hash值

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // id
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    // sql
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // 不配置parameterMapping的话这段逻辑没什么用
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    // 当前环境的id也会塞进去
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

update方法内部是一个算法,会去计算hashcode并且与之前的相乘,到时候取的时候则根据hashcode一个个逆向回来,保证每个参数都会被用到,这种设计的方式其实还是很巧妙的。

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

之后就是重载的query方法了,这里有个二级缓存的逻辑需要注意一下,首先会获取当前statmentt的cache,如果有的话就去看是否配置了使用缓存,如果配置了则会去查询缓存,如果没有则走db,并且将查询的结果塞到 TransactionalCacheManager 这个类中,TransactionalCacheManager 就是保管缓存的一个类。

 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 二级缓存,会获取当前statmentt的cache,如果有的话就去看是否配置了使用缓存,如果配置了则继续
    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.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 没有二级缓存则走普通查询
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

delegate.query() 方法其实调用的是 BaseExecutor 类的query,这里本来应该是SimpleExecutor,但是因为SimpleExecutor什么都没有做完全继承自BaseExecutor ,所以实际的执行者还是BaseExecutor ,在query方法中,首先会根据配置确定是否要清除localCache 中的缓存,然后就是执行查询,最后会根据你配置的localcache的缓存策略去判断是否保留本地缓存。
mybatis默认是开启着一个本地缓存的,也就是一级缓存,它会将查询的结果默认塞到这个本地缓存中,下一次来查询的时候就可以从这个缓存中获取结果,这个逻辑是必须要走的,不过是有策略可以控制这个缓存的生效策略。
在configuration中可以配置LocalCacheScope,有两种:
SESSION 本次会话内都会保留缓存
STATEMENT 每次语句执行完就清除掉

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 如果需要清理缓存则需要清理缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      // 查询计数器,queryStack是保证递归的时候全部结束掉再处理是否要清楚 localCache 中的一级缓存
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // 会根据LocalCacheScope缓存策略去判断是否清楚本地缓存
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

queryFromDatabase方法中,是通过环绕的方式将localCache中设值,具体的查询方法是在doQuery中

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 预设一个缓存占位
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // 执行完后塞进去
    localCache.putObject(key, list);
    // 三种执行语句,如果是 CALLABLE 语句则会往 localOutputParameterCache 设置一份
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

doQuery方法是个抽象方法,具体的实现逻辑是在各自的实现类中,这里是在SimpleExecutor中,它的逻辑也很简单,就是构建StatementHandler ,构建mysql的statement,然后通过handler去执行。

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();
      // 构建StatemenHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 构造statment
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行statement
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

查询数据并且封装结果是在StatementHandler 中完成,Executor负责的资源调度任务到这里就已经结束了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值