1.什么是模板模式
模板方法模式定义了一个操作中的算法骨架,主要就是抽象方法,将某些步骤延迟到子类中实现,这样新的子类可以在不改变一个算法结构的前提下重新定义该算法的某些特定步骤。
核心: 处理某个流程的代码已经都具备,但是其中某个节点的代码暂时不能确定,因此,我们采用工程方法模式,将这个节点的代码实现转移给子类完成。即:处理步骤父类中定义好,具体实现延迟到子类中实现。
方法回调:好莱坞原则:Don’t call me ,we’ll call you back。当艺人将简历给公司后,只能等待,整个过程由娱乐公司控制,演员只能被动地服从安排,在需要的时候再由公司安排具体环节的演出。
在软件开发中,我们将call翻译为调用。子类不能调用父类,而通过父类调用子类,这些调用步骤已经在父类中写好了,完全由父类控制整个过程。 例如下面的代码
package pattern.template;
public abstract class BankTemplate {
//具体方法
public void takeNumber() {
System.out.println("取号排队");
}
public abstract void transact();//办理的具体业务,//钩子方法
public void evaluate() {
System.out.println("反馈评分");
}
public final void process() {
this.takeNumber();
this.transact();
this.evaluate();
}
}
子类可以通过继承该方法实现抽象函数来完成所需要的操作。例如:
class DrawMoney extends BankTemplate {
@Override
public void transact() {
System.out.println("我要取款");
}
}
class SaveMoney extends BankTemplate {
@Override
public void transact() {
System.out.println("我要存钱");
}
}
在调用的时候使用子类来完成需要的操作
SaveMoney drawMoney = new SaveMoney();
drawMoney.process();
事实上,很多时候是用匿名内部类来做的,代码这么写:
BankTemplate bankTemplate=new BankTemplate() {
@Override
public void transact() {
System.out.println("花钱旅游");
}
};
bankTemplate.process();
模板方法本质上就是多态。
什么时候用到模板方法?
实现一个算法时,整体步骤很固定,但是某些部分易变。易变部分可以抽象出来,供子类实现。
开发中的常见场景:
非常频繁,各个框架,类库中都有影子,例如:
- 数据库访问都封装
- Junit的单元测试
- servlet中的goGet/doPost方法调用
- spring中JDBCTemplet,HibernateTemplate等。
2.模板模式在执行器中的应用
2.1 Executor
Executor 是 MyBati s 的核心接口之一 , 其中定义了数据库操作的基本方法。在实际应用中
经常涉及的 SqISession 接口的功能,都是基于 Executor 接口实现的 。 Executor 接口中定义的方
法如下 :
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
//执行 update 、 insert, delete 三种类型的 SQL 语句
int update(MappedStatement ms, Object parameter) throws SQLException;
//执行 select 类型的 SQL 语句,返回位分为结采对象列表或游标对象
<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;
//批量执行 SQL 语句
List<BatchResult> flushStatements() throws SQLException;
//提交事务
void commit(boolean required) throws SQLException;
//回滚事务
void rollback(boolean required) throws SQLException;
//创建缓存 中用到的 CacheKey 对象
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);
}
MyBatis 提供的 Executor 接口实现如图所示,在这些 Executor 接口实现中涉及两种设计模式,分别是模板方法模式和装饰器模式。装饰器模式在前面已经介绍过了,很明显,这里的CachingExecutor 扮演了装饰器的角色,为 Executor 添加了二级缓存的功能。
几种执行器的区别是:
1.SimpleExcutor:每执行一次update或者select,就开启一个Statement对象
2.ReuseExecutor:执行update或者select,以sql作为key,查找Statement对象,存在就使用,否则创建,其实就是将其缓存在一个一个map里。
3.BatchExecutor,执行update时,将所有sql都添加到批处理中,等待统一执行,它缓存了多个Statement对象,等待统一执行。
这里为什么定义了一个BaseExecutor,然后再有四个子类呢?其实就是模板模式,方便业务按照统一的格式调用不同的执行器。
2.2 BaseExecutor,一级缓存和事务
BaseExecutor里涉及一级缓存和事务等很多核心的问题,有必要好好看一下。BaseExecutor 是一个实现了 Executor 接口的抽象类 ,它实现了 Executor 接口的大部分方法,其中就使用了模板方法模式。 BaseExecutor 中主要提供了缓存管理和事务管理的基本功能,继承 BaseExecutor 的子类只要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别
是 : doUpdate()方法、 doQue可()方法、 doQueryCursor()方法、 doFlushStatement()方法,其余的功能在 BaseExecutor 中实现。
2.2.1 一级缓存
使用缓存是一种比较有效的优化手段,使用缓存可以减少应用系统与数据库的网络交互、减少数据库访问次数、降低数据库的负担、降低重复创建和销毁对象等一系列开销,从而提高整个系统的性能 。 从另一方面来看,当数据库意外岩机时,缓存中保存的数据可以继续支持应用程序中的部分展示的功能,提高系统的可用性 。
MyBatis 作为一个功能强大的 ORM 框架,也提供了缓存的功能 , 其缓存设计为两层结构,分别为一级缓存和二级缓存。
一级缓存是会话级别的缓存,在 MyBatis 中每创建一个 Sq!Session 对象,就表示开启 一次数据库会话。在一次会话中,应用程序可能会在短时间内,例如一个事务内,反复执行完全相同的查询语句,如果不对数据进行缓存,那么每一次查询都会执行一次数据库查询操作,而多次完全相同的、时间间隔较短的查询语句得到的结果集极有可能完全相同,这也就造成了数据库资源的浪费。
MyBatis 中的 SqlSession 是通过本节介绍的 Executor 对象完成数据库操作的,为了避免上述问题,在 Executor 对象中会建立一个简单的缓存,也就是本小节所要介绍的“一级缓存飞它会将每次查询的结果对象缓存起来。在执行查询操作时,会先查询一级缓存,如果其中存在完全一样的查询语旬,则直接从一级缓存中取出相应的结果对象并返回给用户,这样不需要再访问数据库了 ,从而减小了数据库的压力 。
一级缓存的生命周期与 SqlSession 相同,其实也就与 SqISession 中封装的 Executor 对象的生命周期相同。当调用 Executor 对象的 close()方法时,该 Executor 对象对应的一级缓存就变得不可用。一级缓存中对象的存活时间受很多方面的影响,例如,在调用 Executor.update()方法时,也会先请空一级缓存。 一般情况下,不需要用户进行特殊配置 。
执行 select 语句查询数据库是最常用的功能, BaseExecutor.query()方法实现该功能的思路还是比较清晰的,如图所示 。
BaseExecutor. query()方法会首先创建 CacheKey 对象,并根据该 CacheKey 对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果缓存未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象。
除此之外,一级缓存还有第二个功能:在嵌套查询时,如果一级缓存中缓存了嵌套查询的结果对象,则可以从一级缓存中直接加载该结果对象。
BaseExecutor.update()方法负责执行 insert、 update、 delete 三类 SQL 语句,它是调用 doUpdate()
模板方法实现的。在调用 doUpdate()方法之前会清空缓存,因为执行 SQL 语句之后,数据库中的数据已经更新, 一 级缓存的内容与数据库中的数据可能己经不一致了,所以需要调用clearLoca!Cache()方法清空一级缓存中的“脏数据”。
2.2.2 事务相关操作
在 BatchExecutor 实现 (具体实现后面详细介绍)中,可以缓存多条 SQL 语句,等待合适的时机将缓存的多条 SQL 语句一并发送到数据库执行 。 Executor且ushStatements()方法主要是针对批处理多条 SQL 语句的,它会调用 doFlushStatements()这个基本方法处理 Executor 中缓存的多条 SQL 语句。在 BaseExecutor.commit()、 rollback()等方法中都会首先调用 flushStatements()方法,然后再执行相关事务操作…
BaseExecutor.commit()方法首先会清空一级缓存、调用 flushStatements ()方法,最后才根据参数决定是否真正提交事务。实现为:
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清空一级缓存
clearLocalCache();
flushStatements(); // 啥都没做,交给子类实现
if (required) {
transaction.commit();
}
}
BaseExecutor.rol lback()方法的实现与 commit()实现类似, 同样会根据参数决定是否真正回滚事务 ,区别是其中调用的是 flushStatements()方法的 isRollBack 参数为 true , 这就会导致Executor 中缓存的 SQL 语句全部被忽略(不会被发送到数据库执行) 。
2.3 SimpleExecutor
Simp leExecutor 继承了 BaseExecutor 抽象类 , 它是最简单的 Executor 接口实现。正如前面所说, Executor 使用了模板方法模式, 一级缓存等固定不变的操作都封装到了 BaseExecutor 中 ,在 SimpleExecutor 中就不必再关心一级缓存等操作,只需要专注实现 增删改查4 个基本方法的实现即可。
可以看到这里只有Update和Query方法,因为增删改都属于update。
2.4 ReuseExecutor
重用 Statement 对象是常用的一种优化手段,该优化手段可以减少SQL 预编译的开销以及创建和销毁 Statement 对象的开销,从而提高性能。
ReuseExecutor.doQuery()、 doQue叩Cursor()、 doUpdate()方法的实现与 SimpleExecutor 中 对应方法的实现一样,区别在于其中调用的 preparestatement()方法, SimpleExecutor 每次都会通过JDBC Connection 创建新的 Statement 对象,而 ReuseExecutor 则会先尝试重用 StaternentMap 中缓存的 Statement 对象 。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
if (hasStatementFor(sql)) {
// 根据SQL从缓存(Map)中获取 Statement
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
Connection connection = getConnection(statementLog);
// 用Connection创建一个Statement
stmt = handler.prepare(connection, transaction.getTimeout());
// 把Statement写入缓存
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
getStatement()的实现是:
private Statement getStatement(String s) {
return statementMap.get(s);
}
当事务提交或回被、连接关闭时,都需要关闭这些缓存的 Statement 对象 。前面介绍BaseExecutor.commit()、 rollback()和 close()方法时提到,其中都会调用doFlushStatements()方法,所以在该方法中实现关闭 Statement 对象的逻辑非常合适。
public List<BatchResult> doFlushStatements(boolean isRollback) {
for (Statement stmt : statementMap.values()) {
closeStatement(stmt);
}
statementMap.clear();
return Collections.emptyList();
}
2.5 BatchExecutor
应用系统在执行一条 SQL 语句时,会将 SQL 语句以及相关参数通过网络发送到数据库系统。对于频繁操作数据库的应用系统来说,如果执行一条 SQL 语句就向数据库发送一次请求,很多时间会浪费在网络通信上。使用批量处理的优化方式可以在客户端缓存多条 SQL 语句,并在合适的时机将多条 SQL 语句打包发送给数据库执行,从而减少网络方面的开销,提升系统的性能。
看材料说在批量执行多条 SQL 语句时,每次向数据库发送的 SQL 语句条数是有上限的,如果超过这个上限,数据库会拒绝执行这些 SQL 语句井抛出异常 ,但是代码里只看到一个list,不知道上限是多少。
JDBC 中的批处理只支持 insert、 update 、 delete 等类型的 SQL 语句,不支持 select 类型的SQL 语句。
2.6 CachingExecutor和二级缓存
CachingExecutor 是一个 Executor 接口的装饰器,它为 Executor 对象增加了二级缓存的相关功能 。 在开始介绍 CachingExecutor 的具体实现之前,先来简单介绍一下 MyBatis 中的二级缓存及其依赖的相关组件 。
2.6.1 二级缓存简介
MyBatis 中提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。与二级缓存相关的配置有三个:
(1)首先是 mybatis-config且nl 配置文件中的 cacheEnabled 配置,它是二级缓存的总开关。只有当该配置设置为 true 时,后面两项的配置才会有效果, cacheEnabled 的默认值为 true。
(2 )在前面介绍映射配置文件的解析流程时提到,映射配置文件中可以配置<cache>节点或<cached-ref>节点。
如果映射配置文件中配置了这两者中的任一一个节点,则表示开启了二级缓存功能。如果配置了<cache>节点,在解析时会为该映射配置文件指定的命名空间创建相应的 Cache 对象作为其二级缓存,默认是 PerpetualCache 对象,用户可以通过<cache>节点的 type 属性指定自 定义Cache 对象。
如果配置了<cache-ref>节点,在解析时则不会为当前映射配置文件指定的命名 空间创建独立的 Cache 对象,而是认为它与<cache-ref>节点的 namespace 属性指定的命名空间共享同一个Cache 对象 。
通过<cache>节点和<cache-ref>节点的配置,用户可以在命名空间的粒度上管理二级缓存的开启和关闭。
(3 )最后一个配置项是< select>节点中的 useCache 属性,该属性表示查询操作产生的结果对象是否要保存到二级缓存中。 useCache 属性的默认值是 true。
2.6.2 一级缓存与二级缓存的关系
二级缓存是针对命名空间层的,MyBatis的命名空间说的是POJO的XXx.xml文件中的
<mapper namespace=”” />
如下图,当应用程序通过 SqlSession2 执行定义在命名空间 namespace2 中的查询操作时, SqlSession2首先到 namespace2 对应的二级缓存中查找是否缓存了相应的结果对象。如果没有,则继续到SqlSession2 对应的一级缓存中查找是否缓存了相应的结果对象,如果依然没有,则访问数据库获取结果集并映射成结果对象返回。最后,该结果对象会记录到 SqISession 对应的一级缓存以及 namespace2 对应的二级缓存中,等待后续使用。另外需要注意的是,下图中的命名 空间namespace2 和 namespace3 共享了同一个二级缓存对象,所以通过 SqlSession3 执行命名 空间
namespace3 中的完全相同的查询操作(只要该查询生成的 CacheKey 对象与上述 SqlSession2 中的查询生成 CacheKey 对象相同即可〉时,可以直接从二级缓存中得到相应的结果对象 。
2.6.3 CachingExecutor 的实现
通过下图可以清晰地看到, CachingExecutor 中封装了一个用执行数据库操作的 Executor对象,以及一个用于管理缓存的 TransactionalCacheManager 对象 。
CachingExecutor.query()方法执行查询操作的步骤如下:
(1)获取 BoundSql 对象,创建查询语句对应的 CacheKey 对象 。
(2)检测是否开启了二级缓存,如果没有开启 二级缓存,则直接调用底层 Executor 对象的query()方法查询数据库。如果开启了 二级缓存,则继续后面的步骤 。
(3)检测查询操作是否包含输出类型的参数,如果是这种情况,则报错 。
(4)调用 TransactionalCacheManager.getObject()方法查询 二级缓存,如果二级缓存中查找到相应的结果对象,则直接将该结果对象返回。
(5)如果二级缓存没有相应 的结果对象,则调用底层 Executor 对象的 query()方法,正如前面介绍的 ,它会先查询一级缓存,一级缓存未命中时,才会查询数据库。最后还会将得到的结果对象放入 Transactiona!Cache.entriesToAddOnCornmit 集合中保存。