装饰者模式(一)


前言

最近在复习之前写过的博客,看到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的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值