装饰者模式(一)
前言
最近在复习之前写过的博客,看到Mybatis缓存的部分,想起了Cache的设计用到了装饰者模式,那么刚好我们就来好好看看装饰者模式。
从Mybatis的Cache设计说起
我们之前已经说过了,要想让Mybatis的二级缓存生效,需要在Mapper文件中加入如下配置。
<cache/>
对于这个配置的作用,我们直接引用mybatis官方文档。
这个简单语句的效果如下:
- 映射语句文件中的所有 select 语句将会被缓存。
- 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
- 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
- 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
所有的这些属性都可以通过缓存元素的属性来修改。比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会 导致冲突。
可用的收回策略有:
- LRU – 最近最少使用的:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认的是 LRU。
flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
Mybatis是如何加载Cache配置的
在Mybatis中,对Mapper配置文件的解析由XMLMapperBuilder来完成。
configurationElement
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
我们先看使用默认配置情况下的解析
cacheElement(context.evalNode("cache"));
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
}
}
我们可以看到在cacheElement中,有对cache节点下的属性的值进行读取并作出相应的处理。
然后通过这些处理过的信息调用 builderAssistant.useNewCache方法来创建Cache实例并注册到Configuration中。
我们来看MapperBuilderAssistant中的useNewCache方法
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
Properties props) {
//如果在配置文件中没有显式指定 就使用PerpetualCache.class
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
//如果没有显式指定,LruCache.class
evictionClass = valueOrDefault(evictionClass, LruCache.class);
//开始创建一个使用了层层装饰者模式包裹的Cache实例
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.properties(props)
.build();
//将cache加入到configuration中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
方法不难理解,就是先创建一个使用了装饰者模式得到的Cache实例,然后将这个实例注入到configuration中。
所以我们主要看是怎么创建的。
CacheBuilder
//默认情况下是PerpetualCache.class,这个一般不会在配置文件中更改
public CacheBuilder implementation(Class<? extends Cache> implementation) {
this.implementation = implementation;
return this;
}
//默认的是LruCache 对应的属性是eviction 即设置回收策略
/*
LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
*/
public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
if (decorator != null) {
this.decorators.add(decorator);
}
return this;
}
//设置缓存失效时间,默认是不设置 ,以毫秒为单位
public CacheBuilder clearInterval(Long clearInterval) {
this.clearInterval = clearInterval;
return this;
}
//对应的size属性 默认是1024
public CacheBuilder size(Integer size) {
this.size = size;
return this;
}
//对应readOnly属性 设置缓存是否为只读
public CacheBuilder readWrite(boolean readWrite) {
this.readWrite = readWrite;
return this;
}
//设置相应的自定义配置,通过属性文件
public CacheBuilder properties(Properties properties) {
this.properties = properties;
return this;
}
public Cache build() {
//使用单个String形参的构造方法来初始化PerpetualCache,构造方法的入参是namesapceName
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
//如果有自定义的参数,带上
setCacheProperties(cache);
//添加用于设置回收策略的Cache,这里开始了第一次封装Cache
if (PerpetualCache.class.equals(cache.getClass())) { // issue #352, do not apply decorators to custom caches
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
//这里实现了cache装饰类的层层封装
cache = setStandardDecorators(cache);
}
//最后还是封装成一个LoggingCache类型的Cache,这个Cache已经经过层层装饰了
else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
来稍微看一下具体的装饰步骤
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
try {
return cacheConstructor.newInstance(base);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
}
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//如果设置了缓存失效时间,加一个ScheduledCache装饰类
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
//如果并不是只读,加一个SerializedCache装饰类
if (readWrite) {
cache = new SerializedCache(cache);
}
//然后加上具有日志功能的装饰类
cache = new LoggingCache(cache);
//然后加上具有同步功能的装饰类
cache = new SynchronizedCache(cache);
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
通过这个分析,我们可以得到基本的Cache的装饰链
LoggingCache-> SynchronizedCache->SerializedCache->LoggingCache->ScheduledCache->LRUCache->PerpetualCache。
也就是说其实最后最终调用的还是PerpetualCache里的方法。
最后说一下:加入到configuration的过程。 configuration.addCache(cache);
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
cache.getId(),id是在初始化PerpetualCache这个Cache的时候通过构造方法传入的,id的值是这个Mapper的命名空间名。所以这样我们才能在MappedStatement中找对应的cache.
我们再回头看看我们曾经说过的Mybatis二级缓存。
我们直接看CacheExecutor的query方法。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//获取该Mapper命名空间下的缓存
Cache cache = ms.getCache();
//如果有,开始准备使用缓存了
if (cache != null) {
//如果是select语句,默认情况下不清缓存,但是如果在Mapper文件中有在select配置中配置属性fluchCache为true的话,会执行缓存清理的动作。具体对这个是否需要请缓存配置的解析在XMLStatementBuilder类的parseStatementNode方法中。
flushCacheIfRequired(ms);
//如果确定这个select语句使用了Cache并且方法参数中没有resultHandler类型的参数。就使用Cache了
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
//从缓存中找
List<E> list = (List<E>) tcm.getObject(cache, key);
//如果没找到,去查询
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询的结果放入Cache中
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
}
//返回结果
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
方法的大概流程已经在代码块中解释了。
我们主要看下这个过程中对缓存的使用。
先看是否执行缓存清理的方法
flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
//如果需要清理缓存,update的时候默认清理缓存
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
tcm对应的是一个TransactionalCacheManager对象,可以把他看做一个TransactionalCache池。在CachingExecutor中是这样初始化的。
private TransactionalCacheManager tcm = new TransactionalCacheManager();
所以在初始化Executor执行器的时候,tcm也就存在了。
TransactionalCacheManager中的getTransactionalCache方法,这个方法很明显是构造一下cache和TransactionalCache的关系,存入Map中,而且TransactionalCache将cache作为构造方法的入参。
所以这里,每一个cache就对应着一个包含cache的TransactionalCache对象。
TransactionalCache的clear方法
@Override
public void clear() {
reset();
clearOnCommit = true;
}
private void reset() {
clearOnCommit = false;
entriesToRemoveOnCommit.clear();
entriesToAddOnCommit.clear();
}
clear方法最后把clearOnCommit置为true.
并且把entriesToRemoveOnCommit和entriesToAddOnCommit这两个Map中的元素都清空了、。
tcm.putObject(cache, key, list);
将结果放入到缓存的方法。
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
调用的是TransactionalCache的putObject方法
@Override
public void putObject(Object key, Object object) {
//移除entriesToRemoveOnCommit中key为当前的CacheKey的元素
entriesToRemoveOnCommit.remove(key);
//将当前的CacheKey为keey,并且构造一个包含cache,CacheKey和查询结果的AddEntry为Value存进entriesToAddCommit
entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
}
所以到这里呢,putObject方法呢,实际上只是把元素存在位于TranscationCache中的entriesToRemoveOnCommit这个Map中。
注意,此时并没有将查询结果放入到我们之前说的装饰器模式构造的cache中。
tcm.getObject(cache, key);
在缓存中查找是否有当前CacheKey对应的值。
调用的是ransactionalCache的getObject方法
@Override
public Object getObject(Object key) {
if (clearOnCommit) return null; // issue #146
return delegate.getObject(key);
}
当clearOnCommit为true的时候就直接返回了,即代表着如果是udpate语句或者设置了flushCache属性为true的话,就甭想着在cache里面找,赶紧再跟数据库做交互呀。
如果不为true,那么就要在cache中去寻找当前CacheKey对应的值了。
而你会发现,之前的putObject并没有把值存入到cache中呀,所以这里根本不用分析了,肯定是找不到的。是的,不信你做个试验,你会发现第二次查询的时候依然是直接跟数据库做交互,不会在缓存中取。
事实上,如果你想要让Mybatis的二级缓存失效,你需要手动的在查询后执行session.commit操作。
public void commit() {
commit(false);
}
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
/*
当调用无参的commit方法时,force指定是false
使用DefaultSqlSessionFactory的无参openSession()方法时,autoCommit为false
如果使用的是有参的openSession(boolean autoCommit)方法,那就取决于传入的参数了
所以也就是说,如果没有显式的将dirty设置为true的时候方法返回false
***注意 在调用update方法的时候,会把dirty设置为true,这时候方法就会返回true了。
*/
private boolean isCommitOrRollbackRequired(boolean force) {
return (!autoCommit && dirty) || force;
}
我们可以看到,其实session.commit()实际上调用的是executor的commit方法。而具体传什么参数给executor的commit方法是由isCommitOrRollbackRequired方法决定的。
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
//BaseExecutor
public void commit(boolean required) throws SQLException {
if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
//清理一级缓存
clearLocalCache();
//BatchExecutor有重写这个方法里的具体方法,目前没有用到,先不分析
flushStatements();
//这里就执行 connection.commit();操作 非查询数据的时候会用到
if (required) {
transaction.commit();
}
}
我们这里不去过多的分析executor的commit()方法。
而是要分析
tcm.commit();
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
调用这个会话中的TransactionalCache池里的所有TransactionalCache的commit()方法。
public void commit() {
//如果clearOnCommit为true,那么就清理掉所有的放在cache里的数据
if (clearOnCommit) {
delegate.clear();
} else {
//将要从cache中要移除的元素移除了
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
//将要添加到cache中的元素添加到cache中
for (AddEntry entry : entriesToAddOnCommit.values()) {
entry.commit();
}
reset();
}
AddEntry是TransactionalCache的一个静态内部类
private static class AddEntry {
private Cache cache;
private Object key;
private Object value;
public AddEntry(Cache cache, Object key, Object value) {
this.cache = cache;
this.key = key;
this.value = value;
}
public void commit() {
cache.putObject(key, value);
}
}
很明显他的commit方法就是在调用cache的putObject方法。
那么这个putObject方法就是我们要看的。
LoggingCache
@Override
public void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
我们一开始说了,我们得到的cache是一个LoggingCache实例。他的delegate属性是SynchronizedCache,他保证放入数据时候的线程安全。而如果没有设置readOnly为true的时候,会接着调用SerializedCache来做一个序列化的操作。如果设置了缓存过时时间,会调用ScheduledCache来做一个查看是否缓存失效的判断。接着就到了设置回收策略的Cache,我们这里默认是LRUCache,即最近最少使用,内部是使用LinkedHashMap实现的,并不复杂,回收策略是必须的,因为缓存不可能无限大,一定要设置一个max值。然后才到PerpetualCache,这才到了我们把CacheKey和查询结果存放起来的类。
private Map<Object, Object> cache = new HashMap<Object, Object>();
public void putObject(Object key, Object value) {
cache.put(key, value);
}
很简单,就是把键值对放到cache这个Map中。
而对于delegate.getObject(key);的具体过程,当然是和delegate.putObject(key, object);相似的,就不细说了。
这里我们看到了每一个Cache都有自己的功能,而PerpetualCache只有最原始的存放数据和得到数据的功能。为了让Cache的功能更强大,我们使用了装饰者模式来一步步的丰富Cache的功能。