从Mybatis源码到Spring动态数据源底层原理分析系列二、Mybatis执行器源码分析

本文深入分析Mybatis执行器Executor的工作原理,从Executor接口的缓存体系入手,探讨SimpleExecutor、ReuseExecutor和BatchExecutor的实现细节。通过对Executor接口方法的分析,揭示了Executor如何利用MappedStatement和参数执行SQL,以及缓存机制的运用。此外,还介绍了StatementHandler组件的角色和功能,梳理了从创建Statement到执行SQL的完整流程。
摘要由CSDN通过智能技术生成

一、引入

在上篇文章, 我们对mybatis初始化的代码进行了简单的分析, 了解到了mybatis中配置类的简单结构, 了解了我们定义的mapper文件中, 一个个的sql标签以MappedStatement的形式存储, 利用mapper文件的namespace和sql标签的id构成key, MappedStatement构成value, 存储在Configuration中的Map中, MappedStatement中存储了从一个sql执行到将结果集解析成我们期望的java对象所需要的信息, 将这些信息利用起来正是Mybatis中的执行器对象Executor

二、缓存体系分析

在分析执行器代码之前, 我们先来了解一下mybatis中缓存接口体系, 这样我们在后面遇到缓存相关的源码时就不会造成阻碍, 在mybatis中定义了缓存接口Cache, 如下:

public interface Cache {
  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();
}
复制代码

非常清晰的缓存接口, 放入缓存、获取缓存、移除缓存、清空缓存等等, Cache的继承结构如下图所示, 只有一层结构, 即定义一个类并实现了Çache接口的功能, 下面这张图我仅仅放出来了部分的实现, 其他也是类似

PerpetualCache里面其实就是利用一个Map对象来完成缓存的功能, LruCache即利用LRU算法实现的缓存, FifoCache即先进先出形式的缓存, 缓存的实现多种多样, mybatis中默认使用的缓存对象就是PerpetualCache了, 其他类型的我们可以通过配置来完成, 这边再简单提及一下, mybatis缓存利用的是装饰者模式, 如果对这个模式熟悉的同学应该可以清楚的了解到, 装饰者模式有增强对象的功能, 即我们可以创建一个缓存拥有多个功能(比如同时拥有LRU、FIFO), 如果对Java流API有所了解的话, 那么也可以知道InputStream的实现类就是典型的装饰器模式, 如下:

BufferedInputStream inputStream = new BufferedInputStream( new FileInputStream( "/test" ) );
复制代码

BufferedInputStream是InputStream类型, FileInputStream也是InputStream类型, BufferedInputStream通过接收一个InputStream类型的参数, 进而实现缓冲的输入流, mybatis中的Cache模块也是一样的

02_Cache缓存继承体系.png

三、源码分析

3.1、Executor接口方法分析

public interface Executor {
  int update(MappedStatement ms, Object parameter) throws SQLException;

  <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;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  Transaction getTransaction();
}
复制代码

省略了部分接口方法, 从上面的这些执行sql的方法可以看到, Executor通过MappedStatement对象来完成sql的执行, 同时提供了sql参数, RowBounds是mybatis提供的内存分页功能, 一般我们开发不会用到(我们通常会通过sql来进行分页, 即sql利用limit关键字进行分页), BoundSql中保存了动态的sql(比如 select xx from xx where id = #{id}, 里面保存的是select xx from xx where id = ? 这是在初始化的时候进行解析动态sql得到后的结果, 如果对动态sql的解析有兴趣的同学可以深入研究下mybatis初始化过程中构建MappedStatement的过程), 之后会利用parameter来填充sql中的问号, cacheKey是Executor提供的缓存功能中的key相关对象, 有两种类型的缓存, 之后我们会进行介绍

Executor提供了所有的数据库操作方法(包括存储过程的调用, 省略了), 最基本的增删改查、事务的提交回滚等, 所以说Executor其实就是利用MappedStatement中保存的信息来完成sql的执行以及结果集的处理, 称为执行器, 我们接下来分析这一部分的源码

3.2、Executor接口继承层次分析

如下图所示为Executor的继承体系, BaseExecutor是公共实现, 完成了所有sql执行的公共功能, SimpleExecutor是最通 用的执行器, 封装的是大家熟悉的jdbc代码, 即通过PreparedStatement完成sql的执行, ReuseExecutor跟 SimpleExecutor不同的是, 其缓存了通过sql创建出来的PreparedStatement对象, BatchExecutor提供的是批量操作, 通常情况下, 我们使用的都是SimpleExecutor

在另一边, 可以看到与BaseExecutor处于同一级的CachingExecutor, 这是一个典型的装饰者模式, CachingExecutor提 供了另一层次的缓存(之后我们也会分析, 跟Executor接口中所说的缓存不是一回事, Executor接口本身规定了缓存功能), 除了缓存之外, 所有的数据库操作均是采用委托的形式让其他Executor来完成, 所以CachingExecutor类似于如下的结构:

public class CachingExecutor {
    Executor delegate;

    void query () {
        if (存在缓存) {
            返回缓存
        } else {
            Object result = delegate.query();
            将result写入缓存
        }
    }
}
复制代码

01_Executor继承体系.png

3.3、SimpleExecutor源码分析

3.3.1、query方法分析

了解了Executor的继承层次之后, 我们开始分析SimpleExecutor接口的源码, 以BaseExecutor的query方法作为入口进行分析, 先分析公共部分, BaseExecutor定义了模板, SimpleExecutor则是执行模板中要求子类来实现的功能:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        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--;
    }

    return list;
}
复制代码

BaseExecutor按照Executor的定义, 提供了缓存的功能, 我们来看看第一个query方法, 利用sql以及sql参数调用createCacheKey方法, 根据一定的规则构建了缓存key, 然后调用了第二个方法, 所以缓存的key我们也可以自定义规则

在第二个方法中, 第一个if判断说明如果不使用缓存(强制刷新缓存), 那么就调用clearLocalCache来清空缓存, queryStack是查询嵌套次数, 我们在实现结果集映射即定义resultMap时允许嵌套查询, queryStack就是指嵌套查询的层数, 每一次嵌套查询都会使得queryStack ++, 所以queryStack为0的时候即最开始执行mapper的时候(如果对嵌套查询不熟悉的同学可以跟着mybatis中文官网的例子写一个嵌套查询, 嵌套查询在我目前的接触中其实用的比较少)

localCache的实现为PerpetualCache, 所以在这一层次的缓存中, 其实就是利用Map完成了缓存功能而已, 如果拿到的list不为空, 说明存在缓存, 这个时候执行了handleLocallyCachedOutputParameters方法, 否则调用queryFromDatabase方法从数据库中查询数据, handleLocallyCachedOutputParameters方法我们不用关注, 这个跟存储过程有关, 里面会根据MappedStatement的类型, 如果是存储过程则执行一定的逻辑, 否则啥也不执行

缓存这一块后面我们会专门拿一篇文章来说明, 这个需要我们深刻的理解SqlSession这个组件才能更好的掌握mybatis中的两层 缓存是如何运转的

3.3.2、queryFromDatabase方法分析

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    return list;
}
复制代码

在真正查询数据库之前, 先往缓存中放入一个占位对象, 当从数据库中查询到数据后, 再将占位对象移除, 从而放入真正的缓存对象, 之所以有这个操作, 跟缓存所在的层次有关, 这个我们先不说, 等到我们揭秘完sqlSession, 真正开始缓存的分析时, 再来解释为啥mybatis为啥在这里会这样做, 这样做能够达到什么效果(小提示, sqlSession是会话的意思, localCache其实跟sqlSession是绑定的, 会话通常是一对一的, 如果出现了多个人与一个人同时使用一个会话(即多线程操作sqlsession), 需要抛出异常, 即类型转换异常.....通过异常来告诉缓存的操作是非法的, 有点像集合遍历时对集合进行增删改查操作时引发的并发修改异常)

3.3.3、StatementHandler体系分析

在分析doQuery之前, 我们先来聊一下StatementHandler这个组件

public interface StatementHandler {
  Statement prepare(Connection connection, Integer transactionTimeout);

  void parameterize(Statement statement);

  void batch(Statement statement);

  int update(Statement statement);

  <E> List<E> query(Statement statement, ResultHandler resultHandler);

  <E> Cursor<E> queryCursor(Statement statement);

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();
}
复制代码

在执行doQuery之前, 我们手里有MappedStatement对象还有sql参数parameter, 前者保存了执行一个sql需要的相关信息, 后者是用来替换sql中问号的参数, 回想一下我们开始学习jdbc编程的时候, 有了这些, 我们要创建一个Statement对象, 然后设置每一个问号的参数, 最后调用其对应的sql方法, 而StatemetHandler正是利用手里已经存在的数据来完成这些jdbc操作的

  • prepare: 这个方法是用来创建Statement的, 可以创建预编译的PreparedStatement(即对问号进行sql参数替换, 防止sql注入), 也可以创建执行存储过程的CallableStatement, 还可以创建最普通的Statement(没有防止sql注入的功能)

  • parameterize: 进行问号替换, 或者说sql预编译, 如果是PreparedStatement, 即调用对应的setxxx方法而已, 如果是普通的Statement, 就不进行任何操作

  • batch、update、query、queryCursor就是执行对应的sql功能了

  • ParameterHandler: 参数处理器, 默认的实现类只做了一个功能, 利用java类型挑选对应的TypeHandler, 然后进行sql参数的设置, 其实就是完成了之前我们jdbc编程下调用PreparedStatement的setXXX方法而已

经过上面组件的分析, 我们可以联想到, StatementHandler其实就是用来完成jdbc编程的, 通用的流程应该是通过prepare方法创建Statement, 调用parameterize进行sql参数的映射, sql参数的映射在PreparedStatement情况下就是调用对应的setXXX方法, 而parameterize完成参数映射其实是利用ParameterHandler完成的, 所以需要用getParameterHandler方法获取参数处理器, 默认的ParameterHandler即利用java类型挑选对应的TypeHandler, 然后执行不同的setXXX方法, 因为参数处理器只有一个, 而且代码非常简单, 这里就不展开说明, 大家有兴趣可以去看看(利用typehandler完成功能)

分析完接口的功能后, 我们再来看继承体系就清晰多了, 如下图所示, BaseStatementHandler就是实现了上面我们说的这些操作, 因为这些操作是通用的, 如果是PreparedStatement那么就是完整的上述流程, 如果是普通的Statement, 那么parameterize方法就是空实现等等

PreparedStatementHandler则是利用PreparedStatement完成sql功能, SimpleStatementHandler则是创建最普通的Statement完成sql执行, CallableStatementHandler则是存储过程的调用, 非常清晰的三个实现类, 这些handler除了sql执行外还有对结果集的处理, 后面我们也会清晰的看到

RoutingStatementHandler与BaseStatementHandler处于同一级别, 其完成的是路由的功能, 根据当前的sql类型执行分别创建上面我们说的三个handler, 然后执行逻辑, 所以mybatis在创建StatementHandler的时候就是创建的RoutingStatementHandler, 然后通过不同的sql类型真正创建不同的StatementHandler来完成功能

public class RoutingStatementHandler implements StatementHandler {
  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
    }
  }

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    return delegate.prepare(connection, transactionTimeout);
  }
  ..............
}
复制代码

所以真正的功能操作RoutingStatementHandler啥也没做, 都是交给委派对象完成的

03_StatementHandler继承体系.png

3.3.4、SimpleExecutor的doQuery方法源码分析

再次回到我们的doQuery方法:

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    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);
    }
}
复制代码

可以看到, 通过MappedStatement拿到mybatis最顶级的配置对象Configuration, 然后创建StatementHandler, 根据我们对StatementHandler的分析, 这里其实创建的是RoutingStatementHandler

private Statement prepareStatement(StatementHandler handler, Log statementLog) {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}
复制代码

prepareStatement方法非常简单, 获取Connection, 调用StatementHandler的prepare方法创建Statement, 然后进行参数化, 如果是PreparedStatement, 则大概完成的操作是:

PreparedStatement statement = connection.prepareStatement(sql);
然后利用typehandler调用statement.setString(xxx)这样的方法完成参数的设置
复制代码

doQuery最后调用了StatementHandler的query方法, 我们以PreparedStatementHandler来进行分析

public <E> List<E> query(Statement statement, ResultHandler resultHandler) {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}
复制代码

非常简单, 就是强转为PreparedStatement, 然后调用execute方法执行sql, 最后将ResultSet映射为java对象

3.3.5、getConnection方法分析

protected Connection getConnection(Log statementLog) {
    Connection connection = transaction.getConnection();
}

非常简单, 就是利用transaction来完成连接的获取, 我们来看看Transaction:
public interface Transaction {

  Connection getConnection() throws SQLException;

  void commit() throws SQLException;

  void rollback() throws SQLException;

  void close() throws SQLException;

  Integer getTimeout() throws SQLException;
}
复制代码

Transaction事务接口提供了获取连接、提交事务、回滚事务、关闭连接等等功能, 可以看到, 在这里我们就已经有 transaction对象了, 而这个对象是在创建sqlsession的时候创建的, 在这一部分, 只需要知道我们能拿到对应 的数据库连接就可以了!!至于这个连接怎么在这里可以直接拿到, 我们到sqlsession那一部分再来分析

4、总结

Executor接口即为执行器, 根据MappedStatement(存储了sql以及结果映射等功能, 即执行一个sql除了参数外的所有东西)以及参数来完成jdbc操作, 获取返回值, Executor从接口层面规定了需要实现一层缓存, 这一层缓存的默认实现原理即利用一个Map来完成, Executor需要进行一定的方法调度来完成jdbc操作

BaseExecutor完成了所有子类公共的功能(缓存、嵌套查询等等), SimpleExecutor是我们真正要使用的执行器, 以查询为例子, 在doQuery方法中完成了功能, 获取StatementHandler, 即获取的是RoutingStatementHandler, 然后利用这个路由执行器根据sql的类型创建对应的StatementHandler并委托其完成对应的查询功能, 通常情况下是委托PreparedStatementHandle来完成的, 先是prepare创建Statement, 然后是进行参数映射, 最后调用execute方法完成查询, 最后进行结果集映射

到此为止, 我们对整个sql执行的流程进行了分析, 其中跳过了结果集如何映射的, 参数如何利用typehandler完成设置的, 这些东西大家有兴趣可以深入研究下, 对整体有了一定认知后去研究这些会非常轻松, 再次提醒大家, getConnection方法就是spring整合mybatis中对数据源进行同步的核心功能, 后面我们分析相关代码的时候再来看看如何联动的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值