MyBatis原理系列(一)-手把手带你阅读MyBatis源码
MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
MyBatis原理系列(七)-手把手带你了解如何自定义插件
MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制
上篇文章中我们对SqlSession有了比较详细的讲解,SqlSession执行的方法其实都交由Executor进行执行。Executor执行器作为MyBatis的重要组件之一,也是有着比较优秀的设计和复杂的细节。在这篇文章将会围绕Executor来进行抽丝剥茧,探讨Executor的原理。
1. Executor初识
Executor 作为一个接口,包含更新,查询,事务等一系列方法。每个SqlSession对象都会有一个Executor对象,SqlSession的操作都会由Executor执行器执行。Executor接口有个抽象实现BaseExecutor类,其中定义了一些模板方法,由子类实现。
Executor 接口中定义的方法如下:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
// 更新
int update(MappedStatement ms, Object parameter) throws SQLException;
// 先查询缓存,在查询数据库
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
// 查询
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
// 返回游标对象
Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
// 释放Statement
List flushStatements() throws SQLException;
// 事务提交
void commit(boolean required) throws SQLException;
// 事务回滚
void rollback(boolean required) throws SQLException;
// 创建缓存的键值对
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 缓存是否存在
boolean isCached(MappedStatement ms, CacheKey key);
// 清除一级缓存
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class> targetType);
// 获取事务对象
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
1.1 Executor继承关系
Executor接口有两个实现,一个是BaseExecutor抽象类,一个是CachingExecutor实现类。BaseExecutor抽象类有四个实现SimpleExecutor,BathExecutor, ReuseExecutor, ClosedExecutor。
BaseExecutor抽象类 采用模版方法的设计模式,定义了一些模版方法,即抽象方法。Executor接口的其它方法BaseExecutor都给出了默认实现,进行缓存管理和事务操作,从而降低了接口的实现的难度。
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List doFlushStatements(boolean isRollback) throws SQLException;
protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
四类Executor中,默认采用的是SimpleExectutor。每个Executor的特点如下:
- SimpleExecutor:默认的执行器,每次执行update或者select操作,都会创建一个Statement对象,执行结束后关闭Statement对象。
- ReuseExecutor:可重用执行器,重用的是Statement对象,第一次执行一条sql,会将这条sql的Statement对象缓存在key-value结构的map缓存中。下一次执行,就可以从缓存中取出Statement对象,减少了重复编译的次数,从而提高了性能。每个SqlSession对象都有一个Executor对象,因此这个缓存是SqlSession级别的,当SqlSession销毁时,缓存也会销毁。
- BatchExecutor:批量执行器,默认情况是每次执行一条sql,MyBatis都会发送一条sql。而批量执行器的操作是,每次执行一条sql,不会立马发送到数据库,而是批量一次性发送sql。
4.ClosedExecutor: ResultLoaderMap的内部类,用来进行处理懒加载相关,懒加载相关将在其它文章中展开介绍。
1.2 Executor 创建
Executor 是怎么创建出来的呢,在前几篇文章中,我们都没有显示的引用Executor,其实SqlSession引用着Executor对象。在我们从SqlSessionFactory获取SqlSession对象的时候,会调用到SqlSessionFactory的openSession()方法,其实调用了openSessionFromDataSource方法,在这个方法中,创建了事物,也创建了Executor对象。
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();
}
}
顺着configuration.newExecutor(tx, execType),我们继续往里看。根据ExecutorType来创建不同类型的执行器,默认创建的是SimpleExecutor这个执行器。如果一级缓存开启(默认是开启的),还会用CachingExecutor来包装SimpleExecutor执行器,在这里用到了装饰者设计模式。
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;
}
三种Executor的创建都是调用了BaseExecutor的构造函数,BaseExecutor的构造函数如下,涉及到事务读喜庆,懒加载,缓存的相关初始化。至此一个SqlSession就有了自己唯一的Executor对象了。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
1.3 Executor 执行
我们调用mapper接口的方法时,最终都会调用到SqlSession的方法,以DefaultSqlSession为例子,查询方法如下,最终调用也就是Executor.query()方法。Executor是SqlSession的傀儡无疑了。
@Override
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
2. SimpleExecutor
SimpleExecutor 是默认的执行器,也是最简单的执行器。它实现了BaseExecutor定义的四个抽象方法,doUpdate,doQuery,doQueryCursor和doFlushStatements四个方法。
在这里以doUpdate为例,介绍下SimpleExecutor的操作步骤
- 创建StatementHandler
- 创建Statement
- 执行sql操作
- 关闭Statement
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 1. 创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
// 2. 创建Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 3. 执行sql操作
return handler.update(stmt);
} finally {
// 2. 关闭Statement
closeStatement(stmt);
}
}
StatementHandler 是用来管理JDBC中的Statement对象,并进行和数据库的操作。StatementHandler和Statement的相关在其它文章中展开详述,在此不赘述。
3. ReuseExecutor
上面提到ReuseExecutor就是重用Statement对象,如果在一个SqlSession中多次执行一条sql,如果每次都去生成Statement对象,会造成一大笔资源浪费。因此ReuseExecutor在SimpleExecutor的基础上,对prepareStatement()方法进行了改进,将Statement对象缓存在内存中,并且免去了第四步:关闭Statement对象。
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
}
prepareStatement 做的就是四步骤:
- Sql是否命中缓存
- 命中缓存直接从缓存中获取到Statement对象
- 如果缓存中没有,则创建新的Statement对象
- 接第3步,以sql为key, Statement为value放到缓存中
private final Map statementMap = new HashMap<>();
/**
* 获取Statement对象
* @param handler
* @param statementLog
* @return
* @throws SQLException
*/
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 1。 sql中是否存在缓存中
if (hasStatementFor(sql)) {
// 2. 命中缓存直接从缓存中获取到
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
Connection connection = getConnection(statementLog);
// 3. 创建新的Statement对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 4. 以sql为key, Statement为value放到缓存中
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
/**
* 缓存map中是否存在sql的Statement对象
* @param sql
* @return
*/
private boolean hasStatementFor(String sql) {
try {
Statement statement = statementMap.get(sql);
return statement != null && !statement.getConnection().isClosed();
} catch (SQLException e) {
return false;
}
}
/**
* 从缓存中获取
* @param s
* @return
*/
private Statement getStatement(String s) {
return statementMap.get(s);
}
/**
* 以sql为key, Statement为value放到缓存中
* @param sql
* @param stmt
*/
private void putStatement(String sql, Statement stmt) {
statementMap.put(sql, stmt);
}
由于SqlSession都引用着自己的Executor对象,因此这个缓存是SqlSession级别的,如果SqlSession销毁了,对应的缓存也会将销毁。
4. BatchExecutor
BatchExecutor 相对于复杂一些,批量的意思是批量的发送sql到数据库,而不是一个个的发送。以doUpdate()为例,先将多个Statement对象存储到List中,然后再将执行结果放到一个List中。实际原理使用的是JDBC的批处理操作,在执行doUpdate方法时,sql不会立马执行,而是等到commit或者rollback,执行JDBC的executeBatch方法。
- doUpdate()返回的值是固定的,不是影响的行数
- 如果连续提交相同的sql,则只会执行一次
- 提交sql不会立马执行,而是等到commit时候统一执行
- 底层使用的是JDBC的批处理操作,addBatch()和executeBatch()操作。
// 批量更新处理的固定返回值,不是影响的行数
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
// Statement集合
private final List statementList = new ArrayList<>();
// 批量结果集合
private final List batchResultList = new ArrayList<>();
// 上一次Sql语句
private String currentSql;
// 上一次的MappedStatement对象
private MappedStatement currentStatement;
public BatchExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
@Override
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;
// 1. currentSql和currentStatement初始化为null,走else分支
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt);// fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt); // fix Issues 322
// 2. 设置currentSql和currentStatement为当前sql
currentSql = sql;
currentStatement = ms;
// 3. 将Statement加到list集合中。
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 4. 调用JDBC的addBatch()方法,添加到批处理中
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
在doFlushStatements中,会遍历statementList集合中的Statement,一条条执行,并将结果加载到结果集合中。
@Override
public List doFlushStatements(boolean isRollback) throws SQLException {
try {
List results = new ArrayList<>();
// 1. 回滚则直接返回
if (isRollback) {
return Collections.emptyList();
}
// 2. 遍历statementList集合
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
// 3. 执行sql
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List
5. CachingExecutor
在前面介绍创建Executor对象的时候,会判断是否开启了一级缓存,如果开启了,则用CachingExecutor来包装以上三种类型中的一种执行器,使用装饰者设计模式来增强执行器的缓存功能。
// 是否开启了一级缓存,默认开启
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
CachingExecutor 的构造函数如下
// 1. 委托执行器,也就是被包装的三种执行器的中的一种
private final Executor delegate;
// 2. 缓存管理类,用来管理TransactionalCache
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
// 3. 互相引用
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
在执行更新操作的时候,先清空缓存,再去执行实际执行器的update方法
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 清空缓存
flushCacheIfRequired(ms);
// 调用实际执行器的update方法
return delegate.update(ms, parameterObject);
}
在执行查询的时候,先从缓存中获取,如果缓存中没有再去调用实际执行器的query方法查询数据库,并放到缓存中返回。
- 获取缓存key
- 查询缓存,如果缓存命中直接返回
- 如果缓存中没有,或者缓存不存在,则查询数据库,并放到缓存中
TransactionalCacheManager 和 TransactionalCache涉及到缓存模块,也打算在其它文章中讲解,在此也就一笔带过。
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 1. 获取缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List list = (List) tcm.getObject(cache, key);
// 2. 如果缓存为空,则查询数据库放到缓存中
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
6. Executor 栗子
前面讲解了四种Executor,实际进行操作的是SimpleExecutor,ReuseExecutor,BatchExecutor三种,如果不指定执行器类型,默认是SimpleExecutor。如果开启了缓存,则会使用CachingExecutor进行包装,增加缓存逻辑。
接下来将用几个栗子来实际操作一番,也能将三种执行器的特点展现出来。
6.1 SimpleExecutor
在获取sqlSession时可以指定执行器的类型,先看看SimpleExecutor执行结果。
public static void main(String[] args) {
try {
// 1. 读取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 获取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 执行接口方法
for(long i = 1; i < 3; i++){
TTestUser userInfo = userMapper.selectByPrimaryKey(i);
System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
}
// 6. 提交事物
sqlSession.commit();
// 7. 关闭资源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
可以从日志打印看到我们执行了2次sql,Statement编译了2次。
6.2 ReuseExecutor
ReuseExecutor 重用的就是Statement对象,以key-value形式来缓存Statement对象,避免了同一个sql编译多次,从而提高性能。
public static void main(String[] args) {
try {
// 1. 读取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE);
// 4. 获取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 执行接口方法
for(long i = 1; i < 3; i++){
TTestUser userInfo = userMapper.selectByPrimaryKey(i);
System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
}
// 6. 提交事物
sqlSession.commit();
// 7. 关闭资源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
可以从日志打印看到我们执行了2次sql,Statement编译了1次。
6.3 BatchExecutor
BatchExecutor 就是在commit时一次性提交sql,而不是发送一次,执行一次。
try {
// 1. 读取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 4. 获取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 执行接口方法
for(long i = 1; i < 3; i++){
TTestUser userInfo = new TTestUser();
userInfo.setMemberId(2000+i);
userInfo.setNickname(2000+i+"_nick");
userInfo.setRealName(2000+i+"_real");
userMapper.insertSelective(userInfo);
// 模拟插入间隔
Thread.sleep(1000);
}
System.out.println("-------开始提交事务--------- ");
// 6. 提交事物
sqlSession.commit();
System.out.println("-------结束提交事务--------- ");
// 7. 关闭资源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
这个例子不太明显,但是可以看到虽然 Thread.sleep(1000)了,但是BatchExecutor中的操作是在最后commit时,才会插入数据到数据库中去,插入时间是一致的。
6.4 CachingExecutor
CachingExecutor不会执行具体的更新和查询操作,而是在执行更新操作的时候先清除下缓存,在执行查询操作的时候先从缓存中查找,如果命中缓存直接返回。
try {
// 1. 读取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 获取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 执行接口方法
for(long i = 1; i < 3; i++){
TTestUser userInfo = userMapper.selectByPrimaryKey(2001L);
System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
}
// 6. 提交事物
sqlSession.commit();
// 7. 关闭资源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
在这个栗子中,虽然我们执行了两次sql,但是参数和sql语句都是一样的,所以一次的查询结果会被缓存,第二次查询的时候直接从缓存中去取。
7. 总结
这篇文章介绍了Executor执行器的接口,继承关系,三种执行器,以及用了几个例子来介绍他们的区别,想必大家对Executor已经有了很全面的了解了吧。希望大家能够在评论区评论指正,一起进步。