• 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

  • 相关阅读:
    搭建非域AlwaysOn win2016+SQL2016
    从0开始搭建SQL Server AlwaysOn 第四篇(配置异地机房节点)
    从0开始搭建SQL Server AlwaysOn 第二篇(配置故障转移集群)
    从0开始搭建SQL Server AlwaysOn 第三篇(配置AlwaysOn)
    从0开始搭建SQL Server AlwaysOn 第一篇(配置域控)
    四、基于Windows 2012配置SQL Server 2014 AlwaysOn
    三、安装SQLserver 2014(For AlwaysOn)
    二、 Windows 2012配置故障转移(For SQLServer 2014 AlwaysOn)
    Mybatis-SQL语句构建器类及日志
    Mybatis-JavaAPI
  • 原文地址:https://www.cnblogs.com/UYGHYTYH/p/12995060.html
Copyright © 2020-2023  润新知