Executor组件与模板模式
一、Executor组件
- Executor是Mybatis中用于执行数据库操作的核心接口,定义了数据库操作的基本方法。我们知道SqlSession是对用户暴露的核心接口,用户通过SqlSession来操作数据库,但实际上底层调用的就是Executor组件的接口。在数据库的查询操作过程中,Mybatis中定义的整体操作流程是差不多的,以查询为例:大体分为SqlSession执行sql->获取sql信息->拼装缓存key->判断是否需要清空一级缓存->查找一级缓存->根据是否命中判断是否需要查询数据库->查询结果->缓存会写->执行完毕,但是具体的执行会有所不同,比如有simple、reuse、batch三种方式,但是从这个固定的步骤来看,非常适合使用模板模式来实现,由父类实现固定的方法流程骨架,具体的不同的步骤留给子类重写。Executor组件就使用到了模板方法,具体关于模板方法可以参考:03-行为型模式(上)
- 三种类型对比
类型 | 描述 |
---|---|
simple | 默认;使用PreparedStatement访问数据库,每次访问都创建新的PreparedStatement对象 |
reuse | 使用预编译的PreparedStatement访问数据库,会重用Statement对象 |
batch | 批量执行 |
- 实际上除了上面几种之外,还有存储过程的执行,具体在后面分析
二、使用方式
- 三种类型如下:
//Executor操作数据库的方式
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
//打开姿势;openSession支持多种传参方式,主要包括事物提交参数和执行类型参数
SqlSession sqlSession = SqlSessionFactoryUtil.getSqlSessionFactoryInstaceByConfig(CONFIG_FILE_PATH).openSession(ExecutorType.BATCH);
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
}
三、目录结构
- 包:org.apache.ibatis.executor
类名 | 作用 |
---|---|
Executor | 是顶层接口定义了基本的数据库操作方法 |
BaseExecutor | 抽象类,继承自Executor并实现了大部分方法,主要实现了缓存管理和事物管理的方法,其他的方法需要不同类型的子类实现 |
SimpleExecutor | 继承自BaseExecutor,是默认配置使用PreparedStatement访问数据库,每次访问都创建新的PreparedStatement对象 |
ReuseExecutor | 继承自BaseExecutor,使用预编译的PreparedStatement访问数据库,会重用Statement对象 |
BatchExecutor | 继承自BaseExecutor,提供批量执行Sql语句的能力 |
CachingExecutor | 装饰器类,内部持有具体实现类的对象,实现了Executor接口,添加了二级缓存的功能 |
四、源码解析
4.1 Executor接口
- 顶层接口,定义了数据库操作的基本方法,
/**
* @author Clinton Begin
* 顶层接口,定义了数据库操作的基本方法,
*/
public interface Executor {
//部分已经省略
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
}
4.2 BaseExecutor抽象类
- 继承自Executor并实现了大部分方法,主要实现了缓存管理和事物管理的方法。因为源码很多,我们省去部分源码,重点看看查询方法
4.2.1 BaseExecutor#query()
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//1.获取sql语句信息,包括占位符,参数等信息
BoundSql boundSql = ms.getBoundSql(parameter);
//2.拼装缓存的key值
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//检查当前executor是否关闭
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//3.非嵌套查询,并且FlushCache配置为true,则需要清空一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//查询层次加一
queryStack++;
//4.查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//针对调用存储过程的结果处理
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//5.缓存未命中,从数据库加载数据,如果加载到数据,会保存到一级缓存
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
//延迟加载处理
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
//如果当前sql的一级缓存配置为STATEMENT,查询完既清空一集缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
4.2.1 BaseExecutor#queryFromDatabase
- BaseExecutor#queryFromDatabase是查询数据库的方法,里面可能会有多种方式,比如simple、reuse、batch,因此这里的第二步的doQuery方法是在子类实现的,这就是典型的模板方法。 这里 如果查询到数据会将数据写到一级缓存。
/**
* 查询数据库
* */
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//1.到一级缓存占一个位置
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//2.查询数据库,调用抽象方法doQuery,方法查询数据库并返回结果,可选的实现包括:simple、reuse、batch
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//3.在缓存中删除占位符
localCache.removeObject(key);
}
//4.将查询结果放入一级缓存
localCache.putObject(key, list);
//5.如果是调用存储过程
if (ms.getStatementType() == StatementType.CALLABLE) {
//6.缓存输出类型结果参数
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
- 在第二步中的doQuery在BaseExecutor是没有实现的抽象方法,由后面的三个子类实现,在BaseExecutor主要是实现了主体的流程骨架,这就是模板模式的体现。
4.3 三种子类
4.3.1 SimpleExecutor
- SimpleExecutor继承自BaseExecutor,默认情况就是该Executor,该模式下每次访问数据库都创建新的PreparedStatement对象。我们看看主要的doQuery方法实现。
public class SimpleExecutor extends BaseExecutor {
//省略了部分方法和属性
/**
* 查询的实现
* */
@Override
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();
//1.创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//2.用StatementHandler对象创建stmt,并使用StatementHandler对占位符进行处理
stmt = prepareStatement(handler, ms.getStatementLog());
//3.通过statementHandler对象调用ResultSetHandler将结果集转化为指定对象返回
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
/**
* 创建Statement
* */
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//1.获取connection对象的动态代理,添加日志能力;(这里参考日志模块的代理模式)
Connection connection = getConnection(statementLog);
//2.使用StatementHandler,利用connection创建(prepare)Statement
stmt = handler.prepare(connection, transaction.getTimeout());
//3.使用StatementHandler处理占位符
handler.parameterize(stmt);
return stmt;
}
}
- 如下图所示,在prepareStatement获取PreparedStatement的时候,每一次都是获取的新的,并且返回的其实是一个代理对象,都是具备日志能力的JDBC组件,这里可以参考 11-Mybatis源码和设计模式-2(日志模块和适配器模式,代理模式)
4.3.2 ReuseExecutor
- ReuseExecutor使用预编译的PreparedStatement访问数据库,并且会重用Statement对象,这是和SimpleExecutor最大的区别
public class ReuseExecutor extends BaseExecutor {
/**
* 查询的实现,和SimpleExecutor的doQuery是一样的
* */
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
//1.创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//2.用StatementHandler对象创建stmt,并使用StatementHandler对占位符进行处理
Statement stmt = prepareStatement(handler, ms.getStatementLog());
//3.通过statementHandler对象调用ResultSetHandler将结果集转化为指定对象返回
return handler.<E>query(stmt, resultHandler);
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
//1.判断statementMap是否已经保存过sql,保存过直接取出,sql是key,value就是之前缓存好的Statement对象
if (hasStatementFor(sql)) {
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
//2.没有保存,那么就生成新的,并且保存到statementMap,这一步的逻辑和SimpleExecutor是一样的,
//相比于SimpleExecutor,ReuseExecutor最大特点就是会重用sql
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
//3.使用StatementHandler处理占位符
handler.parameterize(stmt);
return stmt;
}
}
- prepareStatement方法是ReuseExecutor和SimpleExecutor最大的区别。该方法里面我们可以看到他会使用一个Map缓存下来sql的预编译得到的Statement对象,在Map中sql是key,Statement对象是value。如果缓存中没有,则编译之后还会放进去。而doQuery方法和SimpleExecutor是一样的。
4.3.3 BatchExecutor
- BatchExecutor提供批量执行Sql语句的能力。我们看到在七内部维护了2个List集合分别存储批量执行的Statement对象和BatchResult结果对象。在每次的查询方法都会将这两个集合清除因此批量语句里面不要有查询(这里到时候通过代码调试一下)。
- 另外批量更新的语句是在哪个时机调用的,这里调试一下,因为看源码doFlushStatements的注释2的地方会调用执行语句,但是doUpdate里面并没有调用doFlushStatements,,到时候调试看看
public class BatchExecutor extends BaseExecutor {
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
//保存批量执行的Statement
private final List<Statement> statementList = new ArrayList<Statement>();
//保存批量执行的BatchResult
private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
@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.判断当前使用sql和statement是否是上一次的statement和sql
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
//2.如果是则取出
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
//3.StatementHandler占位符赋值
handler.parameterize(stmt);//fix Issues 322
BatchResult batchResult = batchResultList.get(last);
//batchResult里面添加批量执行的参数列表
batchResult.addParameterObject(parameterObject);
} else {
//3.如果不是则创建一个Statement
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
// StatementHandler占位符赋值
handler.parameterize(stmt); //fix Issues 322
//记录下当前的sql和Statement,下一个语句会对比这两个对象
currentSql = sql;
currentStatement = ms;
//将Statement添加到list
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// handler.parameterize(stmt);
//不管是那种逻辑,这个语句都要执行,因此都使用batch方法处理
handler.batch(stmt);
//返回值是-2147482646,据说可以防止无限循环
return BATCH_UPDATE_RETURN_VALUE;
}
/**
* batchExecutor查询实现
* */
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
flushStatements();//会调用doFlushStatements方法刷新两个集合
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.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
/**
* 刷新Statement,记录执行次数
*/
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<BatchResult>();
if (isRollback) {
return Collections.emptyList();
}
//1.如果进行了批量处理,size不为0
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
//2.执行批处理,并跟新处理后的结果数组
batchResult.setUpdateCounts(stmt.executeBatch());
//3.获取MappedStatement,它是保存sql语句的数据结构
MappedStatement ms = batchResult.getMappedStatement();
//4.获取参数
List<Object> parameterObjects = batchResult.getParameterObjects();
//5.获取KeyGenerator,处理主键会写
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
//6.1 Jdbc3KeyGenerator的情况
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
//6.2 SelectKeyGenerator的情况
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
//记录操作
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
//刷新完毕要清除集合
statementList.clear();
batchResultList.clear();
}
}
}
4.4 装饰器CachingExecutor
- CachingExecutor使用了装饰器模式来给具体的Executor添加缓存功能,他实现了Executor接口,同时内部持有某一个Executor的具体实现类(4.3中的某一个),在主流程方法的执行过程中添加缓存相关的读取流程。CachingExecutor再实现的缓存是二级缓存,因此在整个Mybatis的查询过程中,是嫌查询二级缓存的再查询一级缓存的,因为二级缓存是通过这个装饰器类实现的,那么会在真正的Executor查询之前访问二级缓存,如果没有命中,那么就会会真正的走Executor的查询流程(比如SimpleExecutor的),在SimpleExecutor里面才会再去查询一级缓存(流程是在BaseExecutor中实现的)。下面是CachingExecutor代码。
/**
* 二级缓存实现
* CachingExecutor使用装饰器模式实现二级缓存,内部持有真正的Executor对象(SimpleExecutor,ReuseExecutor或者BatchExecutor)
* 它继承Executor类,并重写了全部的Executor方法,内部调用对应Executor的方法,前后增加了一些缓存读取相关的逻辑
*/
public class CachingExecutor implements Executor {
//继承Executor并重写了全部Executor的方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取sql语句信息,包括占位符,参数等信息
BoundSql boundSql = ms.getBoundSql(parameterObject);
//拼装缓存的key值
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
/**
* 查询的真正控制流程
* */
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
//1.先尝试读取缓存,缓存为空则直接走最后的BaseExecutor.query
if (cache != null) {
//2.如果需要就清除缓存
flushCacheIfRequired(ms);
//3.如果配置允许使用缓存,才访问缓存
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
//4.从二级缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//5.二级缓存为空,才会调用BaseExecutor.query
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//6.查询到数据则放入缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//如果缓存为null,就走真正的Executor实现类的查询方法,(BaseExecutor.query)
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//其他的继承自Executor的方法,基本上都是调用delegate的方法,有些稍微家里一点简单的控制,就不展示了
}
4.5 辅助类
- 在前面的三种子类的代码中我们看到了很多辅助类,在不了解这些辅助类的作用时,有些代码会有点困难,其实先看4.4再回过头看4.3会好很多。最主要的三个辅助类是StatementHandler,ParameterHandler和ResultHandler。
4.5.1 StatementHandler
- StatementHandler是一个接口,它完成Mybatis最核心的工作,代表使用数据库的Statement或者PrepareStatement执行操作,也是Executor实现的基础。包括创建Statement对象,为sql绑定参数,执行sql,结果集映射等。它是一个接口,使用不同的实现类来处理不同的情况,并且也采用了模板模式,使用BaseStatementHandler来完成基本骨架,子类继承然后实现具体方法细节。更多关于StatementHandler的部分,请参考: 17-Mybatis源码分析(StatementHandler数据库访问)
4.5.2 ParameterHandler
- ParameterHandler对预编译的sql进行参数赋值,更多关于ParameterHandler的部分,请参考: 17-Mybatis源码分析(ParameterHandler参数读取)
4.5.3 ResultHandler
- ResultHandler将数据库返回记录封装为用户指定的实体类,更多关于ResultHandler的部分,请参考: 18-Mybatis源码分析(ResultHandler结果集映射)
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。