博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
装饰者模式(一)
阅读量:2350 次
发布时间:2019-05-10

本文共 13961 字,大约阅读时间需要 46 分钟。


前言

最近在复习之前写过的博客,看到Mybatis缓存的部分,想起了Cache的设计用到了装饰者模式,那么刚好我们就来好好看看装饰者模式。

从Mybatis的Cache设计说起

我们之前已经说过了,要想让Mybatis的二级缓存生效,需要在Mapper文件中加入如下配置。

对于这个配置的作用,我们直接引用mybatis官方文档。

这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句将会被缓存。
  • 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
  • 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
  • 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
  • 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
  • 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

所有的这些属性都可以通过缓存元素的属性来修改。比如:

这个更高级的配置创建了一个 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
typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class
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
typeClass, Class
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
implementation) { this.implementation = implementation; return this; }//默认的是LruCache 对应的属性是eviction 即设置回收策略/*LRU – 最近最少使用的:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。*/ public CacheBuilder addDecorator(Class
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
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
cacheClass, Cache base) { Constructor
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 
List
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
list = (List
) tcm.getObject(cache, key); //如果没找到,去查询 if (list == null) { list = delegate.
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.
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();  }//BaseExecutorpublic 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
cache = new HashMap
(); public void putObject(Object key, Object value) { cache.put(key, value); }

很简单,就是把键值对放到cache这个Map中。

而对于delegate.getObject(key);的具体过程,当然是和delegate.putObject(key, object);相似的,就不细说了。

这里我们看到了每一个Cache都有自己的功能,而PerpetualCache只有最原始的存放数据和得到数据的功能。为了让Cache的功能更强大,我们使用了装饰者模式来一步步的丰富Cache的功能。

转载地址:http://idmvb.baihongyu.com/

你可能感兴趣的文章
exception in c++
查看>>
java并发编程lock
查看>>
阿里云技术教程系列-ECS远程连接 Linux 实例
查看>>
Linux新建用户并允许docker
查看>>
Drools Workbench 7.5.0.Final安装运行
查看>>
Docker快速部署Redis
查看>>
Spring boot shiro session cache ecache redis 共存配置
查看>>
一看就懂的设计模式--设计模式分类
查看>>
一看就懂的设计模式--模板方法
查看>>
一看就懂的设计模式--享元模式
查看>>
一看就懂的设计模式--策略模式
查看>>
spring Cloud 组建图
查看>>
腾讯云
查看>>
什么服务器比较好?
查看>>
阿里云+腾讯云采购季优惠攻略
查看>>
PCB设计容易出错的地方都有哪些?
查看>>
挠性电路板和刚性电路板的区别,以及柔性电路板焊接方法操作步骤
查看>>
如何做好一块PCB板,大神从以下几个方面做了论述
查看>>
学习笔记1之static
查看>>
学习笔记2之继承
查看>>