大家好,我是本周的带班编辑 子悠 (这个月来的早了点),本周由于轮班小哥懿在出差,所以就由我为大家排版并送出技术干货,大家可以在公众号后台回复“java”,获得作者 Java 知识体系/面试必看资料。
上一篇我们对 SqlSession 和 SqlSessionFactory 的创建过程有了一个详细的了解,可以去是看 MyBatis 基础搭建及架构概述但上述的创建过程只是为 SQL 执行和 SQL 映射做了基础的铺垫而已,就和我们 Spring 源码为 Bean 容器的加载进行许多初始化的工作相同,那么做好前期的准备工作接下来该做什么了呢?该做数据库连接驱动管理和 SQL 解析工作了!那么本篇本章就来讨论一下数据库驱动连接管理和SQL解析的管理组件之 Executor 执行器。
每一个 SqlSession 都会拥有一个 Executor 对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为 JDBC中Statement 的封装版。
如图所示,位于继承体系最顶层的是 Executor 执行器,它有两个实现类,分别是BaseExecutor
和 CachingExecutor
。
BaseExecutor
是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式之接口适配
的体现,是 Executor 的默认实现,实现了大部分 Executor 接口定义的功能,降低了接口实现的难度。BaseExecutor 的子类有三个,分别是SimpleExecutor
、ReuseExecutor
和BatchExecutor
。
SimpleExecutor: 简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个 Statement 对象,用完就直接关闭 Statement 对象(可以是 Statement 或者是 PreparedStatment 对象)
ReuseExecutor: 可重用执行器,这里的重用指的是重复使用 Statement,它会在内部使用一个 Map 把创建的 Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在 Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。因为每一个 SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的Statement 作用域是同一个 SqlSession。
BatchExecutor: 批处理执行器,用于将多个SQL一次性输出到数据库
CachingExecutor
: 缓存执行器,先从缓存中查询结果,如果存在,就返回;如果不存在,再委托给 Executor delegate 去数据库中取,delegate 可以是上面任何一个执行器
上面我们分析完 SqlSessionFactory 的创建过程的准备工作后,我们下面就开始分析会话的创建以及 Executor 的执行过程。
在创建完 SqlSessionFactory 之后,调用其openSession
方法:
SqlSession sqlSession = factory.openSession();
SqlSessionFactory 的默认实现是 DefaultSqlSessionFactory,所以我们需要关心的就是 DefaultSqlSessionFactory 中的 openSession() 方法
openSession 调用的是openSessionFromDataSource
方法,传递执行器的类型,方法传播级别,是否自动提交,然后在 openSessionFromDataSource 方法中会创建一个执行器
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 得到configuration 中的environment
final Environment environment = configuration.getEnvironment();
// 得到configuration 中的事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 获取执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 返回默认的SqlSession
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();
}
}
调用 newExecutor 方法,根据传入的 ExecutorType 类型来判断是哪种执行器,然后执行相应的逻辑
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// defaultExecutorType默认是简单执行器, 如果不传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);
}
// 如果允许缓存,则使用缓存执行器
// 默认是true,如果不允许缓存的话,需要手动设置
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 插件开发。
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
ExecutorType 的选择:
ExecutorType 来决定 Configuration 对象创建何种类型的执行器,它的赋值可以通过两个地方进行赋值:
可以通过 settings 标签来设置当前工程中所有的 SqlSession 对象使用默认的 Executor
另外一种直接通过Java对方法赋值的方式
session = factory.openSession(ExecutorType.BATCH);
ExecutorType 是一个枚举,它只有三个值 SIMPLE, REUSE, BATCH
创建完成 Executor 之后,会把 Executor 执行器放入一个 DefaultSqlSession 对象中来对四个属性进行赋值,他们分别是 configuration
、executor
、 dirty
、autoCommit
。
Executor 接口的方法还是比较多的,这里我们就几个主要的方法和调用流程来做一个简单的描述
Executor 中的大部分方法的调用链其实是差不多的,下面都是深入源码分析执行过程,如果你没有时间或者暂时不想深入研究的话,给你下面的执行流程图作为参考。
query 方法有两种形式,一种是直接查询;一种是从缓存中查询,下面来看一下源码
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;
当有一个查询请求访问的时候,首先会经过 Executor 的实现类 CachingExecutor,先从缓存中查询 SQL 是否是第一次执行,如果是第一次执行的话,那么就直接执行 SQL 语句,并创建缓存,如果第二次访问相同的 SQL 语句的话,那么就会直接从缓存中提取
CachingExecutor.java
// 第一次查询,并创建缓存
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
MapperStatement
维护了一条 select、update、delete、insert 节点的封装,包括资源(resource),配置(configuration),SqlSource(sql源文件)等。使用Configuration 的 getMappedStatement 方法来获取 MappedStatement 对象
BoundSql
这个类包括 SQL 的基本信息,基本的 SQL 语句,参数映射,参数类型等
上述的 query 方法会调用到 CachingExecutor 类中的 query 查询缓存的方法
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);
if (list == null) {
list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
}
return list;
}
}
// 委托模式,交给SimpleExecutor等实现类去实现方法。
return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
由 delegate 执行 query 方法,delegate 即是 BaseExecutor,然后由具体的执行器去真正执行 query 方法
注意:源码中一般以 do** 开头的方法都是真正加载执行的方法
// 经过一系列的调用,会调用到下面的方法(与主流程无关,故省略)
// 以SimpleExecutor简单执行器为例
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 获取环境配置
Configuration configuration = ms.getConfiguration();
// 创建StatementHandler,解析SQL语句
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
// 由handler来对SQL语句执行解析工作
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
由上面的源码可以看出,Executor 执行器所起的作用相当于是管理 StatementHandler 的整个生命周期的工作,包括创建、初始化、解析、关闭。
ReuseExecutor 完成的 doQuery 工作:几乎和 SimpleExecutor 完成的工作一样,其内部不过是使用一个 Map 来存储每次执行的查询语句,为后面的 SQL 重用作准备。
BatchExecutor 完成的 doQuery 工作和 SimpleExecutor 完成的工作一样。
在分析完上面的查询方法后,我们再来聊一下update()方法,update() 方法不仅仅指的是update() 方法,它是一条 update 链,什么意思呢?就是 *insert、update、delete 在语义上其实都是更新的意思,而查询在语义上仅仅只是表示的查询,那么我们来偷窥一下 update方法的执行流程,与 select 的主要执行流程很相似,所以一次性贴出。
// 首先在顶级接口中定义update 方法,交由子类或者抽象子类去实现
// 也是首先去缓存中查询是否具有已经执行过的相同的update语句
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
// 然后再交由BaseExecutor 执行update 方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
// 往往do* 开头的都是真正执行解析的方法,所以doUpdate 应该就是真正要执行update链的解析方法了
// 交给具体的执行器去执行
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
ReuseExecutor 完成的 doUpdate 工作:几乎和 SimpleExecutor完成的工作一样,其内部不过是使用一个Map来存储每次执行的更新语句,为后面的SQL重用作准备。
BatchExecutor 完成的 doUpdate 工作:和 SimpleExecutor 完成的工作相似,只是其内部有一个 List 列表来一次行的存储多个 Statement,用于将多个 sql 语句一次性输送到数据库执行。
我们查阅其源码的时候,在执行器的执行过程中并没有发现其与 query 方法有任何不同之处,但是在 doQueryCursor 方法中我们可以看到它返回了一个 cursor 对象,网上搜索cursor 的相关资料并查阅其基本结构,得出来的结论是:用于逐条读取 SQL 语句,应对数据量
// 查询可以返回Cursor类型的数据,类似于JDBC里的ResultSet类,
// 当查询百万级的数据的时候,使用游标可以节省内存的消耗,不需要一次性取出所有数据,可以进行逐条处理或逐条取出部分批量处理。
public interface Cursor extends Closeable, Iterable {
boolean isOpen();
boolean isConsumed();
int getCurrentIndex();
}
flushStatement() 的主要执行流程和 query,update 的执行流程差不多,我们这里就不再详细贴代码了,简单说一下 flushStatement() 的主要作用,flushStatement() 主要用来释放 statement,或者用于 ReuseExecutor 和 BatchExecutor 来刷新缓存。
createCacheKey() 方法主要由 BaseExecutor 来执行并创建缓存,MyBatis 中的缓存分为一级缓存和二级缓存,关于缓存的讨论我们将在 Mybatis 系列的缓存章节。
Executor 中还有其他方法,提交 commit,回滚 rollback,判断是否时候缓存 isCached,关闭 close,获取事务 getTransaction 立即清除本地缓存 clearLocalCache 等。
在上面的分析过程中我们了解到,Executor 执行器是 MyBatis 中很重要的一个组件,Executor 相当于是外包的 boss,它定义了甲方(SQL)需要干的活(Executor的主要方法),这个外包公司是个小公司,没多少人,每个人都需要干很多工作,boss 接到开发任务的话,一般都找项目经理(CachingExecutor),项目经理几乎不懂技术,它主要和技术leader(BaseExecutor)打交道,技术 leader 主要负责框架的搭建,具体的工作都会交给下面的程序员来做,程序员的技术也有优劣,高级程序员(BatchExecutor)、中级程序员(ReuseExecutor)、初级程序员(SimpleExecutor),它们干的活也不一样。一般有新的项目需求传达到项目经理这里,项目经理先判断自己手里有没有现成的类库或者项目直接套用(Cache),有的话就直接让技术leader拿来直接套用就好,没有的话就需要搭建框架,再把框架存入本地类库中,再进行解析。
文章参考:
https://www.jianshu.com/p/19686af69b0d
http://www.mybatis.org/mybatis-3/getting-started.html
https://www.cnblogs.com/virgosnail/p/10067964.html
编者按:透露一下昨天我们的知识星球发布了第一篇设计模式的文章,作者是江南一点雨的松哥,下面是小编偷偷给大家透露的一些福利,更多详情可以点击阅读原文或者识别下面二维码到星球中查看。后面我们的设计模式会每周更新,欢迎大家一起进来学习。
从今天开始,我们就要正式开始学习设计模式了。关于设计模式的重要性,不言而喻,你写的是代码还是诗,一个重要的考察维度就是代码的健壮性,可扩展性,这些都离不开设计模式的支持。本文,我们就从最简单的工厂方法模式开始,带大家揭开设计模式的神秘面纱。要学习工厂方法,我们得先来学习下简单工厂模式。工厂方法模式则是在简单工厂的基础上做的进一步优化。简单工厂模式也叫静态工厂方法模式,它是一种创建型模式。创建型模式还包括工厂方法模式、抽象工厂模式、建造者模式、单例模式以及原型模式。在简单工厂模式中,我们定义一个专门的类用来创建其他的实例,这个专门定义的类会根据不同的参数,返回不同的实例...
我们在知识星球,欢迎你加入
前1000人,50元/年
长按二维码▽
Java 极客技术公众号,是由一群热爱 Java 开发的技术人组建成立,专注分享原创、高质量的 Java 文章。如果您觉得我们的文章还不错,请帮忙赞赏、在看、转发支持,鼓励我们分享出更好的文章。