文章目录
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的时候建立的。