• Mybatis源码手记-执行器体系


    今天将Mybatis的执行器部分做一下简单手记。

    一、java原生JDBC

    众所周知,Mybatis是一个半自动化ORM框架。其实说白了,就是将java的rt.jar的JDBC操作进行了适度的封装。所以落到根本,肯定离不开JDBC的基本操作。我们来一起复习一下JDBC的基本操作。这里以java.sql.PreparedStatement为例。

     1 public void jdbcTest() throws SQLException {
     2         // 1、获取连接
     3         connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
     4         // 2、预编译
     5         String sql = "SELECT * FROM users WHERE `name`=?";
     6         PreparedStatement sql1 = connection.prepareStatement(sql);
     7         sql1.setString(1, "了了在小");
     8         // 3、执行SQL
     9         sql1.execute();
    10         // 4、获取结果集
    11         ResultSet resultSet = sql1.getResultSet();
    12         while (resultSet.next()) {
    13             System.out.println(resultSet.getString(1));
    14         }
    15         resultSet.close();
    16         sql1.close();;
    17     }

    其实、总结一下,原生JDBC操作流程如图:

     其中,这里边的Connection、PreparedStatement、ResultSet这些API都是Java.sql包下约定的API。其中还有Statement、CallableStatement等API。不同的数据库驱动分别对其进行实现即可。本节既然讲执行器,这里简单罗列一下Java.sql包下所有的执行器接口定义:

     如图,在java.sql给出的 JDBC执行器规范接口中,Statement作为顶级接口,PreparedStatement、CallableStatement分别是基于Statemtment的增强和扩展。这三个API各有侧重点。如图标识。

    在使用层面:

      Statement可以支持重用执行多个静态SQL,并可以设置addBatch、setFetchSize等操作。Statement的每次执行都是给数据库发送一个静态SQL。多次执行,即发送多个静态SQL。

      PreparedStatement可以对SQL进行预编译,可以有效防止SQL注入(参数转义话在数据端执行,并非在Applicattion)。并且,每次执行都是给数据库发送一个SQL,加上若干组参数。

      CallableStatement集成以上两个接口的基础上,扩展了返回结果的读写。

    二、Mybatis执行体系

    Mybatis作为封装JDBC操作的半自动框架,肯定也离不开JDBC的基本流程,以及java.sql给出的规范。如下以Mysql为例。列举一下Mybatis的简明流程。

     对照JDBC的标准流程,Mybatis将Connection对象维护交由SqlSession这个环节来处理,将SQL预编译与执行交给Executor这个环节来处理,将结果集提取交给StatemntHandler来处理。今天我们重点来看下Executor这个环节。

    三、Mybatis执行器Executor

     Executor接口作为Mybatis执行器的顶级接口,约定了修改(增删改)、查询、提交、回滚、缓存的基本规范。其实现的子类根据分工对其做个差异实现。类图如图:

     BaseExecutor:作为Executor的基本抽象实现,里边提取了连接维护、一级缓存的公有功能,供子类复用。并放出了doQuery、doUpdate的抽象方法下放到子类做差异实现。

     CachingExecutor:作为BaseExecutor的一个装饰器,用来负责二级缓存功能。而JDBC相关操作都是丢给BaseExecutor来操作。

     SimpleExecutor、ReuseExecutor、BatchExecutor:三个具体的实现均是实际操作JDBC的对象,可以通过Mapper接口注解(@Options(statementType=StatementType.STATEMENT))来指定Statement。里边默认使用PreparedStatement来处理JDBC操作,如图:

      

    可以在mybatis-config.xml里边指定使用可重用执行器,默认为SimpleExecutor。

    1 <settings>
    2     <setting name="defaultExecutorType" value="REUSE"/>
    3 </settings>

    这里需要说明一下。Java的JDBC规范的执行器有三种:Statement、PreparedStatement、CallableStatement。而JDBC的执行器跟Mybatis的执行器不是一个概念。Mybatis的执行器有主要有三种,SimpleExecutor、ReuseExecutor、BatchExecutor。而且默认情况下,Mybatis的三种执行器都底层都调用PreparedStatement。可以通过Mapper接口方法增加参数来指定需要使用JDBC的哪种执行器(指定方式 ,见上一小节)。

    四、可重用执行器ReuseExecutor

    可重用执行器,其实底层就是维护了一个Map<String sql,Statement stmt> 来捕捉到相同的SQL,则直接取对应缓存的Statement进行执行,所以对于相同SQL(包括query、update),不同参数,则只进行一次预编译。就可以复用设置参数来执行。来,上源码:

     1 public class ReuseExecutor extends BaseExecutor {
     2     private final Map<String, Statement> statementMap = new HashMap();
     3     private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
     4         BoundSql boundSql = handler.getBoundSql();
     5         String sql = boundSql.getSql();
     6         Statement stmt;
     7         if (this.hasStatementFor(sql)) {
     8             stmt = this.getStatement(sql);
     9             this.applyTransactionTimeout(stmt);
    10         } else {
    11             Connection connection = this.getConnection(statementLog);
    12             stmt = handler.prepare(connection, this.transaction.getTimeout());
    13             this.putStatement(sql, stmt);
    14         }
    15 
    16         handler.parameterize(stmt);
    17         return stmt;
    18     }
    19 
    20     private boolean hasStatementFor(String sql) {
    21         try {
    22             return this.statementMap.keySet().contains(sql) && !((Statement)this.statementMap.get(sql)).getConnection().isClosed();
    23         } catch (SQLException var3) {
    24             return false;
    25         }
    26     }
    27 
    28     private Statement getStatement(String s) {
    29         return (Statement)this.statementMap.get(s);
    30     }
    31 
    32     private void putStatement(String sql, Statement stmt) {
    33         this.statementMap.put(sql, stmt);
    34     }
    35 }

    可以看出,ReuseExecutor也维护了一个Statement的缓存,这是Mybatis里边除了一级缓存、二级缓存以外的又一处缓存。一般来说将执行器指定为ReuseExecutor,也是一种提升性能的方案。

    五、批处理执行器BatchExecutor

    批处理执行器,其实底层依赖的就是JDBC的Statement.addBatch接口规范。所以,BatchExecutor的使用必须是以addBatch开始,并以doFlushStatement结束。不同的是,BatchExecutor并不是单调的直接使用addBatch,而是对其扩展了缓存,复用的能力。上源码:

     1 public class BatchExecutor extends BaseExecutor {
     2     public static final int BATCH_UPDATE_RETURN_VALUE = -2147482646;
     3     private final List<Statement> statementList = new ArrayList();
     4     private final List<BatchResult> batchResultList = new ArrayList();
     5     private String currentSql;
     6     private MappedStatement currentStatement;
     7     public BatchExecutor(Configuration configuration, Transaction transaction) {
     8         super(configuration, transaction);
     9     }
    10     public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    11         Configuration configuration = ms.getConfiguration();
    12         StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
    13         BoundSql boundSql = handler.getBoundSql();
    14         String sql = boundSql.getSql();
    15         Statement stmt;
    16         if (sql.equals(this.currentSql) && ms.equals(this.currentStatement)) {
    17             int last = this.statementList.size() - 1;
    18             stmt = (Statement)this.statementList.get(last);
    19             this.applyTransactionTimeout(stmt);
    20             handler.parameterize(stmt);
    21             BatchResult batchResult = (BatchResult)this.batchResultList.get(last);
    22             batchResult.addParameterObject(parameterObject);
    23         } else {
    24             Connection connection = this.getConnection(ms.getStatementLog());
    25             stmt = handler.prepare(connection, this.transaction.getTimeout());
    26             handler.parameterize(stmt);
    27             this.currentSql = sql;
    28             this.currentStatement = ms;
    29             this.statementList.add(stmt);
    30             this.batchResultList.add(new BatchResult(ms, sql, parameterObject));
    31         }
    32         handler.batch(stmt);
    33         return -2147482646;
    34     }
    35 }

    从源码可以看出,BatchExecutor维护了StatementList、batchResultList用来分别存放addBatch加入的Statement以及每个Statement执行后返回的结果集。另外每次执行一个Statement时都会去看上次执行的SQL(currentSql)与上次执行的Statement(currentStatement)能否复用,如果能复用就不在创建Statement,即省去了重复预编译过程。上一段测试代码:

     1 public void sessionBatchTest(){
     2         SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
     3         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
     4         mapper.setName(10,"道友友谊永存"); 
     5         User user = Mock.newUser();
     6         mapper.addUser(user);
     7         mapper.addUser(user);
     8         mapper.setName(user.getId(),"小鲁班");
     9         mapper.addUser(user);
    10         List<BatchResult> batchResults = sqlSession.flushStatements();
    11     }

    对于这段代码,通过源码分析,可以轻而易举的知道,batchResults.size()是4。这段测试用例,总共发起了5次调用,分别在第4、6、7、8、9行,其中第7行能复用第六行的Statement,所以第7行调用的时候就省去了预编译的步骤,而且将自己的结果集与第六行合并了。所以最终的结果集长度为4。这里如果将代码改成这样:

     1 public void sessionBatchTest(){
     2         SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
     3         User user = Mock.newUser();
     4         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
     5         mapper.setName(10,"道友友谊永存"); 
     6         mapper.setName(user.getId(),"小鲁班");
     7         mapper.addUser(user);
     8         mapper.addUser(user);
     9         mapper.addUser(user);
    10         List<BatchResult> batchResults = sqlSession.flushStatements();
    11 }

    那么,batchResult.size()就是2了,原因就不在赘述。这里简单总结一下:相同SQL && 相同Mapper && 连续执行 才能复用相同的JDBC Statement。

    六、Executor执行器的线程安全问题

    从ReuseExecutor、BatchExecutor的源码不难看出,里边涉及到大量JDBC的Statement缓存复用的逻辑,而且这些对象都是简单成员变量,并未做任何线程安全处理,所以Executor的操作是不可以跨线程的。Executor与SqlSession是一对一的关系,进而可以推导出,SqlSession也不能跨线程调用。但是一个线程是可以调用多个SqlSession、Executor的。

    以上、感谢源码阅读网-鲁班大叔,以及Mybatis源码J10集团军所有道友。

    源码地址:https://gitee.com/llzx/coderead_mybatis_executor.git

  • 相关阅读:
    WPF 自定义NotifyPropertyChanged
    深度学习(五)正则化之L1和L2
    深度学习(三) 反向传播直观理解
    javascript中的原型和原型链(二)
    javascript中的原型和原型链(一)
    javascript中创建对象的方式及优缺点(二)
    javascript中创建对象的方式及优缺点(一)
    JS实现深拷贝的几种方法
    json.stringify()的妙用,json.stringify()与json.parse()的区别
    Javascript你必须要知道的知识点
  • 原文地址:https://www.cnblogs.com/UYGHYTYH/p/12995060.html
Copyright © 2020-2023  润新知