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

原文地址:https://www.cnblogs.com/UYGHYTYH/p/12995060.html