欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

装饰者模式(一)

程序员文章站 2022-06-12 22:43:42
...

前言

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

相关标签: 设计