Declarative Caching Services for Spring声明式缓存服务

摘要

  可伸缩性、可靠性和高性能是现代J2EE应用程序所必须具有的特性。无论客户端属于哪种类型,其请求处理通常包含一些对性能有负面影响的活动,例如从异构数据源收集信息和执行复杂计算等。缓存是用于提升企业应用程序性能的最重要的做法之一。每一个应用程序都有自己的缓存要求,必须经常加以调整,才能确保不会发生性能衰退。企业应用程序需要找到一种方式,在不触及应用程序代码的情况下轻松添加和调优缓存功能。本文将介绍与基于Spring应用程序的代码无关的缓存框架——Declarative Caching Services for Spring。

缓存概述

  简而言之,缓存涉及对检索开销大的数据的临时性保存,它通过避免从原始数据源检索附加数据,从而实现了对客户端的快速响应。例如,缓存避免了应用服务器对数据库的调用,若应用服务器与数据库不在一处,则此类调用可能要涉及网络交互,因此,缓存将带来可观的性能获益。使用缓存,应用程序响应时间将大大优化,这是因为降低了数据库工作负载并释放了网络带宽。

  缓存服务非常适用于分布式系统。远程调用的速度极慢,而且要占用宝贵的网络带宽,这就带来了性能问题。对远程服务的调用从创建请求开始,这是通过将其参数编组为一种可通过线路传输的格式(如XML或字节流等)而完成的。一旦接收到请求,必须在服务器端解组以便开始处理。类似的过程也适用于服务所生成的响应,这又增加了额外的开销。缓存能够消除(至少是减少)远程调用,因而能够大大提升性能。

  尽管有着种种好处,但缓存也会使企业应用程序的设计、开发和部署的复杂程度大大提高。不应在没有清楚了解业务需求的情况下使用缓存,此外,还必须使用测试结果和其他可靠的证据来帮助决策。

  缓存的基本指导原则如下:

  • 通常,添加缓存来减少远程调用是值得的。这将通过避免网络交互而提升性能。
  • 只读、引用数据的缓存是必要的优化措施。例如,缓存美国各州列表可避免重复检索那些基本上不会发生变化的数据。
  • 对于给定的参数值集合总是返回相同信息的服务响应非常适合应用缓存技术。
  • 若应用程序能够容纳陈旧数据,则可实现动态数据——即读/写数据——的缓存。新闻Web站点是一个不错的例子,缓存为其带来的性能提升使其在几秒内即可显示新闻。
  • 要缓存的数据量应在可控制的范围之内,否则将占用过多的内存空间。
  • 在整个集群内缓存动态数据并在每个节点处同步数据是非常困难的。考虑选用第三方缓存解决方案不失为明智之举。
  • 应避免缓存实时数据(如股市报价)和敏感数据(如口令和社会保险号码)。
  • 缓存可能会造成一些与安全性和审计相关的问题。缓存可能截获请求并不经安全检查地处理这些请求,从而导致安全屏蔽失效。

  实现自己的缓存并非易事。您或许不得不应对一些复杂问题,如线程管理、陈旧数据清除和集群支持等。目前有许多高品质的第三方缓存实现,包括开源实现和商业实现,它们可使您不必为创建(更重要的是)维护自己的缓存而烦恼。

Declarative Caching Services for Spring

  Spring Framework 是一个开源应用框架,其功能之一就是为普通java对象提供企业服务。使用Spring创建的应用程序大多为多层的数据中心型企业应用程序,且具有多个并发用户。此类应用程序可能还会与某种类型的远程处理技术(如远程EJB、Web services、RMI或CORBA等)相集成。企业应用程序对于核心业务非常重要,人们期望这些应用程序能交付高性能。

   Declarative Caching Services for Spring为那些Spring驱动的应用程序提供了声明式缓存。声明式缓存不涉及任何编程,因此是应用和调优缓存服务的更轻松、快捷的方式。缓存服务的配置可完全在Spring loC容器内完成。声明式缓存:

  • 提供对不同类型的缓存提供程序的支持,如 EHCacheJBoss CacheJava Caching System (JCS)OSCache 和 Tangosol Coherence
  • 提供了一个比大多数前述API更简单易用的统一API,它可通过编程方式使用缓存服务。请注意,如果没有特殊的需求,我们并不推荐您通过编程方式使用缓存。
  • 声明式缓存服务还提供了声明式缓存刷新,以避免保存陈旧数据。
  • 支持不同的声明式配置策略。
  • 可轻松扩展声明式缓存服务以支持附加的缓存提供程序。

声明式缓存服务的优点

  为更好地理解声明式缓存及其优点,让我们查看以下代码段。这是一个客户管理程序的简单实现,它根据给定的客户ID从数据源中检索客户信息。为提升性能,从数据访问层中检索到的Customer对象编程地保存在一个Coherence缓存内。此外,为避免保存陈旧数据,每更新一位客户的信息,包含客户信息的缓存就被刷新一次:

public class CustomerManager {
   private CustomerDao customerDao;
   private CacheKeyGenerator keyGenerator;
   public Customer load(long customerId) {
     validateCustomerId(customerId);
     Customer customer = null;
     Serializable key = keyGenerator.generateKey(customerId);
     NamedCache cache = getCache();
     customer = (Customer) cache.get(key);
     if (customer == null) {
         customer = customerDao.load(customerId);
         cache.put(key, customer);
     }
     return customer;
   }
   public void update(Customer customer) {
     customerDao.update(customer);
     Serializable key = keyGenerator.generateKey(customer.getId());
     NamedCache cache = getCache();
     cache.remove(key);
   }
   private NamedCache getCache() {
     return CacheFactory.getCache("customerCache");
   }// rest of class implementation
}

  尽管缓存的使用提升了应用程序的性能,它也带来了额外的(不必要的)复杂性。编程式地使用缓存带来了以下问题:

  • 代码更难以理解。要了解核心功能尤为困难。在我们的示例中,只有那些以粗体显示的代码行实现了方法的真正功能。
  • 代码更难以维护。缓存提供程序的调用为嵌入式的,并且延伸到整个应用程序中。因此,任何涉及缓存的维护工作都很可能会影响系统的核心功能。此外,对缓存功能的任何更改都要求访问代码段的每一个副本,更糟糕的是,彼时我们可能并不知道它们究竟在哪里。
  • 代码更难以测试。在我们的示例中,我们需要一个Coherence CacheFactory,或者至少是一个模拟它的模仿对象,这样才能测试方法的业务逻辑。
  • 代码更难以重用。在缓存需求不同或根本不需要缓存的应用程序中,重用CustomerManager类将是一项艰难(甚至完全不可能)的任务。

  声明式缓存封装了缓存的执行方法,并从Java代码内消除了对缓存实现的依赖。引入声明式缓存后,上例中的方法仅实现其核心需求:

public class CustomerManager {
   private CustomerDao customerDao;
   public Customer load(long customerId) {
     validateCustomerId(customerId);
     return customerDao.load(customerId);
   }
   public void update(Customer customer) {
     customerDao.update(customer);
   }
   // rest of class implementation
}

  以下的XML代码段阐明了在Spring loC容器中(而不是在Java代码中)如何为CustomerManager实例配置缓存服务:

<bean id="customerDaoTarget"
class="org.springmodules.cache.samples.dao.CustomerDao" />
<!-- Properties -->
</bean>
<coherence:proxy id="customerDao" refId="customerDaoTarget">
   <coherence:caching methodName="load" cacheName="customerCache" />
   <coherence:flushing methodName="update" cacheNames="customerCache" />
</coherence:proxy>
<bean id="customerManager" class="org.springmodules.cache.samples.business.CustomerManager" />
   <property name="customerDao" ref="customerDao" />
</bean>

  声明式缓存配置有效地将缓存功能从应用程序的核心需求中分离出来,解决了上文提及的众多问题,还带来了以下优点:

  • 更明晰的责任分离。系统中的模块仅负责其核心需求,不再负责缓存功能。这将带来更出色的可追溯性。
  • 更高的模块化程度。将缓存功能从核心模块中分离出来减少了重复代码(支持“一次且仅一次”原则),并有助于避免代码混乱。
  • 设计决策的后期绑定。使用声明式缓存,开发人员可将与缓存实现和调优有关的决策制订延后。开发人员可将精力集中在应用程序的当前核心需求上。声明式配置支持 YAGNI(“you aren't gonna need it”的缩写,意为“您将不会需要它)原则,仅在开发人员确实需要的时候才允许他们为应用程序添加缓存,而且不需要进行系统级的更改。

声明式缓存服务的工作原理

  声明式缓存服务使用运行时方法拦截为Spring托管的bean交付透明、代码无关的缓存和刷新。配置只涉及选择应截获哪些方法以及应如何将这些缓存服务应用于这些方法。下面给出了声明式缓存服务的工作原理概要。

  图1中的活动图展示了缓存方法拦截器的工作原理:


图 1. 运行中的缓存

  缓存方法拦截器首先使用所截获方法的签名和所提供的参数值生成一个惟一键。随后,拦截器在缓存中搜索保存在所生成的键下的对象。若找到了对象,就立即返回给客户端,绕过所截获方法的执行。若拦截器未在缓存中找到任何值,则执行所截获的方法,并将返回值保存在缓存中(使用生成的键),然后将缓存的对象返回给客户端。缓存条目的键是通过可插入的键生成器自动生成的。目前仅提供了一条策略,HashCodeCacheKeyGenerator,它根据所截获方法的散列码及其参数创建惟一键。可通过实现CacheKeyGenerator接口轻松创建键生成器。

  图2中的活动图展示了缓存刷新方法拦截器的工作原理:

图 2. 运行中的缓存刷新
图 2. 运行中的缓存刷新

  缓存刷新方法拦截器可配置为在执行所截获方法之前或之后刷新缓存。一个拦截器可刷新一个缓存的一个或多个区域,也可刷新整个缓存,具体取决于其配置方式及所使用的缓存提供程序。

配置声明式缓存服务

  声明式缓存服务提供了一种简单而直观的配置。各缓存提供程序都有其需在Spring loC容器配置中声明的名称空间。以下示例展示了如何声明coherence名称空间以及如何指定其位置:

< xml version="1.0" encoding="UTF-8" >
<beans xmlns=http://www.springframework.org/schema/beans
xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xmlns:coherence=http://www.springmodules.org/schema/coherence
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springmodules.org/schema/coherence http://www.springmodules.org/schema/cache/springmodules-tangosol.xsd">

  在设置将作为缓存服务目标的Spring bean之前,我们需要将一些系统级的配置设置包含进来,它们将影响缓存服务的行为方式:

<coherence:config failQuietly="false" serializableFactory="XSTREAM" />

  缓存的首要目标是要透明地提升一个应用程序的性能。缓存不应影响核心业务逻辑,也不应改变应用程序的行为。若缓存出现故障,应用程序不应停止执行,特别是在生产中更应如此。从另外一个角度来说,在某些情况下,您可能希望应用程序在出现任何故障时停止运行,包括缓存故障在内——例如,在执行开发期间的集成测试时,必须在将应用程序投入生产之前找到配置错误。

   failQuietly属性赋予开发人员决定缓存故障是否应停止执行应用程序的能力。将此属性设置为true时,任何缓存失败都不会对应用程序造成任何影响。其默认值为false。

  一旦缓存准备好开始运行,您将会发现,在某些情况下,需要在文件系统中保存缓存对象,或者将缓存中的更改复制到一个集群的所有节点中。缓存提供程序能够在Java串行化的协助下执行这些功能,这就意味着要保存在缓存中的对象应实现java.io.Serializable接口。

  当需要将那些不可串行化和不受控制的对象(例如,由JAXB生成的对象)保存在缓存中时,这种需求将引发问题。serializableFactory属性提供了一种解决方案,它对不可串行化的对象强制执行串行化,从而解决了这个问题。它仅提供了一种策略,XStreamSerializableFactory,它使用XStream 将对象串行化为XML,之后才将其保存在缓存中。在您阅读本文时,一种使用 JBoss Serialization 的新策略应已问世。

  强制执行对象串行化的一种替代方案就是使用AspectJ的内部类型声明让类实现java.io.Serializable。以下示例假设类Customer不可串行化,且需要保存在一个集群缓存中:

public aspect SerializationAspect {
   declare parents : Customer implements java.io.Serializable;
}

  如需获得内部类型声明的更多信息或AspectJ的简介,请参见AspectJ文档

  声明式缓存服务提供了不同的配置策略,使开发人员有机会选择最适合于其应用程序需求的策略。下面几节介绍了所提供的各种配置策略。

按bean配置

  此配置策略非常简单,且极易设置。缓存服务的配置在一个proxy元素内执行,此元素拥有一个指向单一Spring托管bean的引用。必须在refId属性内提供要引用的bean的ID。还需要选择应用缓存服务要拦截的方法。cachingflushing元素分别指定哪些方法应触发数据缓存以及哪些方法应触发缓存刷新。这两个元素提供了methodName属性,指定要拦截的方法名称。根据不同的缓存提供程序,这些元素还包含不同的其它属性,用于指定特定缓存的缓存方式及缓存刷新方式。以下示例使用Coherence展示了按bean配置:

<bean id="customerDaoTarget"
class="org.springmodules.cache.samples.dao.CustomerDao" />
<!-- Properties -->
</bean>
<coherence:proxy id="customerDao" refId="customerDaoTarget">
<coherence:caching methodName="load" cacheName="customerCache" />
<coherence:flushing methodName="update" cacheNames="customerCache" when="after" />
</coherence:proxy>

  请注意,应使用选定缓存提供程序的名称空间限制每一个与缓存相关的XML元素。在示例中,我们设置缓存服务来将CustomerDao.load方法的返回值以customerCache为名称保存在Coherence缓存中,并在执行完CustomerDao.update方法后刷新同名缓存。这里要重点提示一下,要配置依赖于CustomerDao的bean,应使用proxy元素的id,而不是原始bean的id:

<bean id="customerManager" class="org.springmodules.cache.samples.business.CustomerManager"/>
<property name="customerDao" ref=" customerDao" />
</bean>

  另一种替代方案是在proxy元素内声明bean定义,这将使配置更简洁:

<coherence:proxy id="customerDao">
<beanclass="org.springmodules.cache.samples.dao.CustomerDao" /> <!-- Properties -->
</bean><coherence:caching methodName="load" cacheName="customerCache" />
<coherence:flushing methodName="update" cacheNames="customerCache" when="after" />
</coherence:proxy>

  按bean配置提供了一种设置缓存服务的简便方法。由于需要为每个bean定义一个beanRef元素,因此最好在应用程序中需要应用缓存服务的对象数量不多时使用这种方法。

源代码级元数据属性

  尽管按bean配置提供了一种配置缓存服务的直观方式,但若应用程序中包含大量Spring托管的bean,则很有可能会得到大量与配置相关的XML。为简化配置,可以使用源代码级元数据属性来“修饰”应用程序,这些属性指出应拦截哪些方法以应用缓存服务。声明式缓存支持 Commons Attributes 和 J2SE 5.0注释

  Commons Attributes允许使用J2SE 1.4或更早版本向应用程序添加元数据属性。由于Java编译器不识别这种类型的元数据属性,因此需要一个额外的编译过程。Commons Attributes元数据的编译会生成必须使用应用程序的源文件编译的源代码。不存在对Commons Attributes元数据的IDE支持,这表明重构之类的常见软件开发实践也不受支持。以下示例展示了Commons Attributes的使用方法。请注意,以下配置与前一节中的配置等效:

public class CustomerDao {

/*** @@Cached(modelId="cachingModel")*/
public Customer load(long customerId) {
// method implementation
}
/*** @@FlushCache(modelId="flushingModel")*/
public void update(Customer customer) {
// method implementation
}// rest of class implementation
}

  源代码级元数据属性仅限于描述是否应该应用缓存服务,而不是描述应如何执行缓存。应如何执行缓存是在Spring loC容器中描述的。在我们的示例中,元数据仅指明了应缓存CustomerDao.load方法的返回值,然后是指定在id为cachingModel的模型中的设置。此模型是在Spring loC容器中声明的,其中包含特定于缓存的设置,描述了缓存中保存值的方式。同样的规则也适用于刷新。以下示例展示了元数据属性所引用的模型的声明:

<coherence:commons-attributes>
<coherence:caching id="cachingModel"cacheName="customerCache" /> 
<coherence:flushing id="flushingModel"
cacheNames="customerCache" when="after" />
</coherence:commons-attributes>
<bean id="customerDao"
class="org.springmodules.cache.samples.dao.CustomerDao" />
<!-- Properties -->
</bean>

  id为cachingModel的模型指明了对象应使用customerCache名称保存在Coherence缓存中。Id为flushingModel的模型以同样的方式指明了执行完截获的方法后应刷新同一缓存。任何与缓存提供程序相关的更改都应该仅在Spring loC容器内执行,且不应影响与缓存相关的元数据属性。此类更改不需重新编译Java源代码。此外还要注意,要处理与缓存相关的元数据,还需在Spring容器中声明一个以源代码级元数据修饰的类的实例(在我们的示例中为customerDao bean)。

  J2SE 5.0注释是一种更为结构化的为应用程序添加元数据属性的格式。由于注释是内置语言属性,Java编译器将其识别为不同的语言结构,从而克服了与Commons Attributes相关的问题。使用J2SE 5.0注释的缓存服务配置几乎与使用Commons Attributes的配置完全相同。实际上,这只需替换Java源代码中的几行代码:

@Cacheable(modelId = "cachingModel")
public Customer load(long customerId) {
// method implementation
}

@CacheFlush(modelId = "flushingModel")
public void update(Customer customer) {
// method implementation
}
// rest of class implementation
}

  如需了解有关J2SE 5.0注释的更多信息,请参阅 JDK 1.5文档。在Spring loC容器中从Commons Attributes切换到J2SE 5.0注释仅需以annotations元素替换commons-attributes元素:

<coherence:annotations>
<coherence:caching id="cachingModel" cacheName="customerCache" />
<coherence:flushing id="flushingModel" cacheNames="customerCache" when="after" />
</coherence:annotations>
<bean id="customerDao" lass="org.springmodules.cache.samples.dao.CustomerDao" />
<!-- Properties -->
</bean>

  在Spring loC容器中,使用源代码级元数据属性的缓存服务仅设置一次。您可以按照自己的需要添加尽可能多的bean,而无需进行额外配置。因此,在需要对大量对象应用缓存服务时,使用源代码级元数据属性进行配置是理想的解决方案。

BeanNameAutoProxyCreator

  类似于元数据属性方法,BeanNameAutoProxyCreator允许在Spring loC容器内为大量bean配置声明式服务,而无需进行额外配置。与元数据属性方法不同的是,BeanNameAutoProxyCreator不需要对Java源代码进行任何更改。

  BeanNameAutoProxyCreator允许对大量bean应用一个或多个方法拦截器。仅需配置缓存服务一次,随后向代理创建器提供bean的名称即可。以下示例展示了如何在BeanNameAutoProxyCreator中指定与缓存相关的方法拦截器:

<coherence:methodMapInterceptors cachingInterceptorId="cachingInterceptor" flushingInterceptorId="flushingInterceptor">
<coherence:caching methodFQN="org.springmodules.cache.samples.dao.CustomerDao.load" cacheName="customerCache" />
<coherence:flushing methodFQN="org.springmodules.cache.samples.dao.CustomerDao.update" cacheNames="customerCache" />
</coherence:methodMapInterceptors>
<bean lass="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
      <list>
            <idref local="customerDao" />
      </list>
</property>
<property name="interceptorNames">
      <list>
            <value>cachingInterceptor</value>
            <value>flushingInterceptor</value>
      </list>
</property>
</bean>
<bean id="customerDao" lass="org.springmodules.cache.samples.dao.CustomerDao" />
<!-- Properties -->
</bean>

  如您所料,上例中显示的配置与按bean配置和源代码级元数据属性两节中的配置结果完全相同。Spring Framework提供BeanNameAutoProxyCreator。如需了解更多信息,请参阅Spring 在线文档

下载

  下载本文示例代码: caching.zip

结束语

  尽管缓存能大大提升企业应用程序的性能,但它也会为应用程序生命周期带来严重的复杂性。本文介绍了Declarative Caching Services for Spring,这是一个封装此类复杂性的框架,它为由Spring驱动的应用程序提供了代码无关的透明式缓存和缓存刷新。

  以Spring的精神为导向,声明式缓存为多种开源和商业缓存提供程序提供了开箱即用的支持。它还提供了不同的配置策略。由开发人员来选择最适于项目需求的组合方案。

  使用声明式缓存,开发人员可以将精力集中在其应用程序的核心需求上,最终得到易于理解、维护、测试和重用的代码。由于缓存服务是在Spring loC容器内配置的,因此可仅在真正需要的时候添加它们,而且无需进行系统级更改。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值