mybatis源码分析(3)Executor执行器分析

1、执行器介绍

mybatis中按照功能区分有三种执行器,这三种执行器都是继承自BaseExecutor,在BaseExecutor上实现了一些基本功能,然后在这三种执行器上分别进行了不同的修饰

  • SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

在缓存的基础上还有一个缓存执行器为CachingExecutor,这中执行器是包裹在上面三种执行器的外面的,CachingExecutor执行器就是实现了mybatis的二级缓存,如果开启了二级缓存,而Executor会使用CachingExecutor来装饰,添加缓存功能。

2、执行器源码分析
2.1、执行器是如何生成的

我们在源码分析2中知道调用代理类函数就相当于调用MapperProxy这个类里的invoke函数,在MapperProxy里有函数名和MapperMethodInvoker对象的缓存,而MapperMethodInvoker中有MapperMethod对象,这个对象利用MappedStatement和实际调用的sql进行绑定,从而可以调用代理类的接口函数来执行sql语句。
在这里插入图片描述
那么执行器的作用域就在域Mappermethod来执行sql语句的区间内。

在调用执行器前必须要讲一下这个执行器是什么时候被生成的,他又是归属于哪一个单位的,是归属于SqlSessionFactory这个大boss级别还是SqlSession这个小boss级别。mybatis是在构造SqlSessionFactory的对象时来读取配置文件,SqlSessionFactory对象可以构造SqlSession对象,也就是说在生成SqlSessionFactory对象后,就已经读取了所有的xml文件的信息及其映射文件中的sql语句信息。但是我们可以发现在Configuration中没有对应的Executor对象,因为我们在一开始构建SqlSessionFactory对象去配置文件中读取的数据几乎都是放在了Configuration这个类对象中。但是在DefaultSqlSession这个唯一实现了SqlSession接口的类中有一个成员变量为Executor,说明了一个执行器是唯一对应一个SqlSession的,那么这个执行器是什么时候被构造的呢?

答案就是在构造sqlSession的过程中就会生成对应的执行器。

/*生成sqlSession */
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE)

/*openSessionFromDataSource是实际调用的函数*/
public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
  }
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }

可以看到一开始是是获取在构造SqlSessionFactory时从配置文件中读取的参数,然后利用configuration.newExecutor(tx, execType)来生成一个执行器,然后返回一个DefaultSqlSession对象,这样生成的执行器就包含在了DefaultSqlSession对象中。下面来看下newExecutor函数中到底做了什么?

public Executor newExecutor(Transaction transaction, ExecutorType 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);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

一开始是设置执行器类型,如果没有设置,默认的执行器类型就是SIMPLE类型。前面在判断完是否是BATCH,REUSE 还是SIMPLE类型后还包了一层CachingExecutor,在executor = new CachingExecutor(executor)语句中可以看到,所以我们在外面看到的执行器的类型实际上都是CachingExecutor类型的,但是里面具体执行器还是BATCH,REUSE 还是SIMPLE类型这三种,只是CachingExecutor在这三种执行器上添加了一些功能。
在这里插入图片描述

2.2、执行器是如何进行调用的

在前面已经说到调用代理类的接口函数会执行到相应的MapperMethodInvoker的invoke,再接着调用相应的MapperMethod来执行execute函数,在这个函数里会对INSERT,UPDATE,DELETE,SELECT这些情况进行分情况调用不同的函数,具体大家可以到org.apache.ibatis.binding.MapperMethod#execute这个方法中取查看,因为函数有点长,这里就不在贴出来了。我们选择其中一个函数来进行分析

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

我们选择select功能中的executeForMany函数,这个函数会返回执行相应的select后的数据,首先他会从传入的参数args中构建出符合sql形式的对象参数,然后判断这个sql是否是动态sql语句,就是是否有if这种动态标签,会根据传入对象参数条件来动态组装成合适的sql语句。动态sql部分将在独立成一篇文章来分析,这里就先不讨论了。接着实际上调用的是sqlSession.selectList语句来执行select命令。最后会判断是否需要将结果组装成array类型还是collection类型来返回。

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();
    }
  }

在根据statement的id号获取到对应的MappedStatement对象后我们就已经知道了这一个sql语句对应的所有信息,进而调用executor.query来进行查询。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
	/*获取sql语句信息和参数映射关系信息*/
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    /*这个和mybatis的二级缓存相关,这在源码分析4中进行分析*/
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

前面已经分析了一开始包裹的都是CachingExecutor,所以上面的executor.query实际上调用的是CachingExecutor类的query函数。BoundSql boundSql = ms.getBoundSql(parameterObject);函数是利用MappedStatement对象和parameterObject这个传入的对象参数来动态构建一个BoundSql对象

public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }

这边首先是从sqlSource中获取到初步的boundSql对象,这个对象信息在一开始获取xml配置信息时进行生成和组装。在一个MappedStatement对象中已经存储了parameterMappings属性,将parameterMap.getParameterMappings()获取到的映射表添加到boundSql对象中,最后还需要检查参数映射中的嵌套结果映射。
在这里插入图片描述
可以看到一个boundSql主要是存储了sql语句,这个语句还是有?的占位符。parameterMappings属性是java对象类型和数据库参数类型的映射表,parameterObject是传入的参数对象。

具体执行到executor的过程图如下
在这里插入图片描述
这边在流程图中显示了SimpleExecutor,其实还有ReuseExecutor和BatchExecutor,就是和图中SimpleExecutor的调用方式一样。

2.3、执行器是如何调用jdbc来操作数据库的
2.3.1、如何手动操作数据库方法
package com.study.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class DBUtil {
    
    private static final String URL="jdbc:mysql://localhost:3306/demo_jdbc";
    private static final String NAME="root";
    private static final String PASSWORD="root";

    public static void main(String[] args) throws Exception{
        
        //1.加载驱动程序
        Class.forName("com.mysql.jdbc.Driver");
        //2.获得数据库的连接
        Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD);
        //3.通过数据库的连接操作数据库,实现增删改查
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("select user_name,age from imooc_goddess");//选择import java.sql.ResultSet;
        while(rs.next()){//如果对象中有数据,就会循环打印出来
            System.out.println(rs.getString("user_name")+","+rs.getInt("age"));
        }
    }
}

我们知道jdbc是Java语言访问数据库的一种规范,是一套API。所有操作数据数据库都是通过调用jdbc这一个API来实现的。本质上流程如下
在这里插入图片描述

2.3.2、SimpleExecutor如何调用JDBC

上面我们看到是BaseExecutor通过queryFromDatabase函数调用到SimpleExecutor的doQuery函数

 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对象*/
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      /*加载驱动,建立连接,预编译sql语句都是在这里实现的*/
      stmt = prepareStatement(handler, ms.getStatementLog());
      /*实际上还是通过stmt这个对象来执行操作*/
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

在SimpleExecutor的doQuery函数中,首先是去创建了对应的Statement对象,在手动操作数据里我们先是加载驱动和连接数据库,但驱动和数据库信息我们已经可以从Configuration对象中直接读取到,但是Statement对象是需要动态生成的。我们看到mybatis中构建了StatementHandler这个接口。
在这里插入图片描述
利用configuration.newStatementHandler函数默认情况下回生成一个RoutingStatementHandler,这个类上没有实际实现,他只是SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler这三个实现类的分发类。为具体的执行器选择适合的StatementHandler。

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;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

可以看到RoutingStatementHandler类上有一个StatementHandler delegate这个实际实现类的载体对象,而具体调用哪一个是根据ms.getStatementType()这个参数来决定的。而StatementHandler类就是对操作statement对象进而操作数据库的最后一层封装。在stmt = prepareStatement(handler, ms.getStatementLog());这个语句是是真正做了操作数据库前的所有事情。其中还包括了sql语句的预编译部分,这个语句中的实现很复杂,对sql预编译的分析就放在之后进行分析了。最后通过handler.query(stmt, resultHandler)这个来执行操作,其实这个语句里面还是通过statement对象来操作数据库,这个和手动操作本质上是一致的。

2.4、SimpleExecutor,ReuseExecutor,BatchExecutor源码区别分析
  • SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

再将文章开头的一部分内容放到这里,可以方便对照的分析。

SimpleExecutor执行器
查看源码可以看到SimpleExecutor重写了doUpdate,doQuery,doQueryCursor,doFlushStatements函数,所以能够作用到的也只是update或select操作,再来看下doQuery函数

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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

可以看到每一次调用doQuery函数都会创建一个Statement对象,结束后调用closeStatement来关闭Statement对象。

ReuseExecutor执行器
ReuseExecutor执行器也是重写了doUpdate,doQuery,doQueryCursor,doFlushStatements函数,所以能够作用到的也只是update或select操作。

public <E> List<E> 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);
  }

可以看到这里每一次调用doQuery函数并没有创建一个Statement对象,而是通过prepareStatement函数得到一个Statement对象,具体是如何得到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);
      applyTransactionTimeout(stmt);
    } else {
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
  }

首先是StatementHandler是对操作jdbc的statment对象进而操作数据库的最后一层封装,那么在StatementHandle一定是得到了sql语句,利用handler.getBoundSql().getSql()可以得到,boundSql是存储sql语句各个参数的对象。然后调用getStatement函数,在ReuseExecutor执行器中有一个sql语句和Statement的映射表Map<String, Statement> statementMap,这样每一个sql语句都对应于一个Statement(注意这个是jdbc中的Statement,不是mybatis中根据namespace和id号唯一匹配一个sql语句的Statement对象)。然后其余的和SimpleExecutor执行器一致。

private boolean hasStatementFor(String sql) {
    try {
      Statement statement = statementMap.get(sql);
      return statement != null && !statement.getConnection().isClosed();
    } catch (SQLException e) {
      return false;
    }
  }

BatchExecutor执行器

BatchExecutor执行器也是重写了doUpdate,doQuery,doQueryCursor,doFlushStatements函数,但是doQuery中的实现几乎和SimpleExecutor执行器中的实现一致。就多了些清空缓存的操作。具体函数可以和SimpleExecutor中的实现对比看下

public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      flushStatements();
      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);
    }
  }

具体我们来一起看下doUpdate中的实现

 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)) {
      int last = statementList.size() - 1;
      //获取最后一次创建statement
      stmt = statementList.get(last);
      //设置事务超时时间
      applyTransactionTimeout(stmt);
      //设置stmt参数
      handler.parameterize(stmt);// fix Issues 322
      //获取对应的批量结果
      BatchResult batchResult = batchResultList.get(last);
      //将参数对象添加到参数列表中
      batchResult.addParameterObject(parameterObject);
    } else {
      //和上一次创建的SQL不同,则需要重新创建PrepareStatement
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    // fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      /*数据库返回结果存放的地方*/
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    /*利用jdbc来对数据库进行批处理操作*/
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

其实BatchExecutor本质上就是如果sql等于currentSql同时MappedStatement与currentStatement相同, 就是同一条SQL,但是参数可能不同,这样就不需要重复创建PrepareStatement。就是对一些仅仅是参数不同其余都相同的sql语句进行批量处理,可以减少网络交互次次数。

从源码中我们可以看到BatchExecutor的doUpdate函数构建Statement的方式是采用ReuseExecutor中的方式的。同时对sql和MappedStatement进行比对后进行参数设置,最后调用handler.batch(stmt)来操作jdbc进行批处理。获取的结果放入到batchResultList中,这个关系是在创建PrepareStatement的时候建立的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值