8 缓存抽象
8.1 介绍
从版本3.1开始,Spring Framework提供了对现有的Spring应用程序透明地添加缓存的支持。 与事务支持类似,缓存抽象允许一致地使用各种缓存解决方案,而对代码的影响最小。
从Spring 4.1开始,通过JSR-107注解和更多定制选项的支持,缓存抽象得到了显着改善。
8.2 理解缓存抽象
缓冲和缓存
术语“缓冲”和“缓存”倾向于交替使用; 但请注意,它们代表不同的事物 缓冲区传统上被用作快速实体和慢速实体之间的数据的中间临时存储。 由于一方将不得不等待其他影响性能,缓冲区通过允许整个数据块立即移动而不是以小块移动来减轻这种影响。 数据只能从缓冲区写入和读取一次。 而且,缓冲区对于至少一个知道它的方是可见的。
另一方面,根据定义,高速缓存是隐藏的,双方都不知道高速缓存发生了。它也提高了性能,但是通过允许相同数据以快速方式被多次读取来实现。
在这里可以找到两者之间差异的进一步解释。
其核心是抽象将缓存应用于Java方法,从而减少基于缓存中可用信息的执行次数。 也就是说,每次调用目标方法时,抽象都会应用缓存行为来检查是否已经为给定的参数执行了方法。 如果有,则返回缓存的结果而不必执行实际的方法; 如果没有,则执行方法,将结果缓存并返回给用户,以便下一次调用方法时,返回缓存的结果。 这样,对于一组给定的参数,昂贵的方法(无论是CPU还是IO绑定)只能执行一次,并且结果被重用,而不必再次实际执行该方法。 缓存逻辑是透明应用的,不会对调用者产生任何干扰。
显然,这种方法只适用于保证为给定输入(或参数)返回相同输出(结果)的方法,无论它执行多少次。
其他与缓存相关的操作由抽象提供,例如更新缓存内容的能力或删除所有条目之一。 如果缓存处理在应用程序过程中可以改变的数据,这些是有用的。
就像Spring框架中的其他服务一样,缓存服务是一个抽象(而不是缓存实现),并且需要使用实际存储来存储缓存数据 - 也就是说,抽象使开发人员不必写缓存逻辑 但不提供实际的存储。 这个抽象是由org.springframework.cache.Cache和org.springframework.cache.CacheManager接口实现的。
开箱即用的抽象实现有几个实现:基于JDK java.util.concurrent.ConcurrentMap的缓存,Ehcache 2.x,Gemfire缓存,Caffeine和兼容JSR-107的缓存(例如Ehcache 3.x)。 有关插入其他缓存存储库/提供程序的更多信息,请参阅插入不同的后端缓存。
缓存抽象没有多线程和多进程环境的特殊处理,因为这些特性是由缓存实现来处理的。。
如果您有多进程环境(即在多个节点上部署应用程序),则需要相应地配置缓存提供程序。 根据您的使用情况,在多个节点上复制相同的数据可能就足够了,但如果在应用程序过程中更改数据,则可能需要启用其他传播机制。
缓存一个特定的项目是一个典型的get-if-not-found-then-proceed-and-put-finally代码块的直接等价物,这个代码块通过编程式缓存交互被发现:不应用锁定,并且多个线程可能会尝试加载相同的项目同时。 驱逐也是如此:如果有几个线程试图同时更新或清除数据,则可以使用陈旧的数据。 某些缓存提供程序在该区域提供高级功能,请参阅您正在使用的缓存提供程序的文档以获取更多详细信
要使用缓存抽象,开发人员需要关注两个方面:
- 缓存声明 - 确定需要缓存的方法及其策略
- 缓存配置 - 数据存储和读取的后端缓存
8.3 基于声明式注解的缓存
为了缓存声明,抽象提供了一组Java注释:
- @Cacheable 触发缓存人口
- @CacheEvict 触发缓存删除
- @CachePut 更新缓存而不会干扰方法的执行
- @Caching 重新组合要在方法上应用的多个缓存操作
- @CacheConfig 在类级别共享一些常见的与缓存相关的设置
让我们仔细看看每个注解:
8.3.1. @Cacheable注解
顾名思义,@Cacheable用于划分可缓存的方法 - 也就是将结果存储到缓存中的方法,以便在随后的调用中(具有相同的参数),缓存中的值不必 实际执行该方法。 以最简单的形式,注解声明需要与注解方法关联的缓存的名称:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在上面的代码片段中,findBook方法与名为books的缓存相关联。 每次调用该方法时,都会检查缓存以查看调用是否已经执行并且不必重复执行。 在大多数情况下,只声明一个缓存,注释允许指定多个名称,以便使用多个缓存。 在这种情况下,将在执行方法之前检查每个缓存 - 如果至少有一个缓存被命中,则返回相关的值:
所有其他不包含该值的缓存也将被更新,即使缓存的方法没有被实际执行。
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
生成默认Key
由于缓存本质上是键值存储,所以缓存方法的每次调用都需要被转换为适合缓存访问的Key。 开箱即用,缓存抽象使用基于以下算法的简单KeyGenerator:
- 如果没有参数,则返回SimpleKey.EMPTY。
- 如果只给出一个参数,则返回该实例。
- 如果有多个参数,返回一个包含所有参数的SimpleKey。
这种方法适用于大多数使用情况; 只要参数具有自然主键并实现有效的hashCode()和equals()方法。 如果情况并非如此,则需要改变策略。
为了提供不同的默认Key生成器,需要实现org.springframework.cache.interceptor.KeyGenerator接口。
默认的Key生成策略随着Spring 4.0的发布而改变。 Spring的早期版本使用了一个Key生成策略,对于多个关键参数,只考虑参数的hashCode()而不考虑equals(); 这可能会导致意外的关键冲突(请参见SPR-10237的背景)。 新的“SimpleKeyGenerator”为这种情况使用了一个复合键。
如果要继续使用以前的Key策略,可以配置已经不推荐使用的org.springframework.cache.interceptor.DefaultKeyGenerator类或创建基于散列的“KeyGenerator”实现。
自定义Key生成声明
由于缓存是通用的,所以目标方法很可能具有不能简单映射到缓存结构顶部的各种签名。 当目标方法有多个参数,其中只有一些适用于缓存(而其余的仅由方法逻辑使用)时,这往往会变得很明显。 例如:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然两个布尔论值影响了本书的查找方式,但它们对缓存没有用处。 更进一步,如果只有一个是重要的而另一个不是呢?
对于这种情况,@Cacheable注解允许用户指定如何通过其关键属性来生成Key。 开发人员可以使用SpEL来选择感兴趣的参数(或它们的嵌套属性),执行操作,甚至可以调用任意方法,而无需编写任何代码或实现任何接口。 这是默认生成器的推荐方法,因为方法与代码库增长的方式在签名方面会有很大差异; 而默认策略可能适用于某些方法,但并不适用于所有方法。
下面是各种SpEL声明的一些例子 - 如果你不熟悉它,请阅读Spring Expression Language:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
上面的代码片断展示了选择某个参数,它的一个属性,甚至是一个任意的(静态)方法是多么容易。
如果负责生成Key的算法过于具体,或者需要共享,则可以在操作中定义一个自定义Key生成器。 为此,请指定要使用的KeyGenerator bean实现的名称:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
key和keyGenerator参数是互斥的,指定两者的操作将导致异常。
默认缓存解析器
开箱即用,缓存抽象使用一个简单的CacheResolver,它使用配置的CacheManager检索在操作级别定义的缓存。
为了提供不同的默认缓存解析器,需要实现org.springframework.cache.interceptor.CacheResolver接口。
自定义缓存解析器
默认缓存解析器非常适合使用单个CacheManager的应用程序,而且不需要复杂的缓存解析要求。
对于使用多个缓存管理器的应用程序,可以将cacheManager设置为使用每个操作:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}
也可以完全以与Key生成类似的方式替换CacheResolver。 每个缓存操作都要求解析,给予实现基于运行时参数实际解析要使用的缓存的机会:
@Cacheable(cacheResolver="runtimeCacheResolver")
public Book findBook(ISBN isbn) {...}
自Spring 4.1以来,缓存注释的值属性不再是强制性的,因为无论注释的内容如何,CacheResolver都可以提供此特定信息。
与key和keyGenerator类似,cacheManager和cacheResolver参数是互斥的,指定两者的操作将导致一个异常,因为CacheResolver实现将忽略定制的CacheManager。 这可能不是你所期望的。
同步缓存
在多线程环境中,某些操作可能会同时调用相同的参数(通常在启动时)。 默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而导致缓存的目的。
对于这些特定情况,可以使用sync属性指示基础缓存提供者在计算值时锁定缓存条目。 结果,只有一个线程忙于计算值,而其他线程被阻塞,直到在缓存中更新条目。
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
这是一个可选功能,您最喜欢的缓存库可能不支持它。 核心框架提供的所有CacheManager实现都支持它。 查看缓存提供者的文档以获取更多详细信息。
条件式的缓存
有时候,一个方法可能并不适合于所有的缓存(例如,它可能取决于给定的参数)。 缓存注解通过条件参数来支持这样的功能,该条件参数采用被评估为真或假的SpEL表达式。 如果为true,则该方法被缓存 - 如果不是,则其行为就像该方法没有被缓存,每当无论缓存中的值或使用什么参数时都执行该方法。 一个简单的例子 - 只有参数名称的长度小于32时才会缓存以下方法:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
另外还可以使用条件参数except参数来否决向缓存添加值。 与条件不同,除非在方法被调用之后计算表达式。 扩展前面的例子 - 也许我们只想缓存平装书:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)
缓存抽象支持java.util.Optional,只有当它存在时才将其内容用作缓存值。 #result始终引用业务实体,永远不会在受支持的包装器上,所以前面的示例可以重写为:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
请注意,结果仍指Book而不是Optional。 因为它可能是空的,我们应该使用安全的导航操作符。
可用的缓存SpEL计算上下文
每个SpEL表达式都会重新评估一个专用的上下文。 除了内置参数外,框架还提供了与参数名称相关的专用缓存相关元数据。 下表列出了可用于上下文的项目,因此可以将它们用于Key和条件计算:
表10.缓存SpEL可用元数据
Name | Location | Description | Example |
---|---|---|---|
methodName | root object | 被执行的方法名 | #root.methodName |
method | root object | 被执行的方法 | #root.method.name |
target | root object | 被执行的目标对象 | #root.target |
targetClass | root object | 被执行的目标类 | #root.targetClass |
args | root object | 被执行的目标的参数 | #root.args[0] |
caches | 对当前方法执行的缓存集合 | ||
argument name | evaluation Context | 任何方法参数的名称。 如果由于某些原因名称不可用(例如,没有调试信息),则参数名称也可在#a <#arg>下使用,其中#arg代表参数索引(从0开始)。 | #iban或#a0(也可以使用#p0或#p <#arg>表示法作为别名)。 |
result | evaluation Context | 方法调用的结果(要缓存的值)。 只有在表达式,缓存put表达式(计算Key)或缓存evict表达式(当beforeInvocation为false时)时才可用。 对于受支持的包装(如可选),#result引用实际对象,而不是包装。 | #result |
8.3.2 @CachePut注解
对于需要在不干扰方法执行的情况下更新缓存的情况,可以使用@CachePut注解。 也就是说,这个方法总是被执行,并且它的结果放入缓存(根据@CachePut选项)。 它支持与@Cacheable相同的选项,应该用于缓存填充而不是方法流程优化:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
请注意,在同一个方法上同时使用@CachePut和@Cacheable注释通常是非常不鼓励的,因为它们有不同的行为。 后者导致使用缓存跳过方法执行,前者强制执行以执行缓存更新。 这会导致意想不到的行为,除了特定的情况(例如具有排除他们的条件的注解)之外,应该避免这种声明。 还要注意,这样的条件不应该依赖于结果对象(即#result变量),因为它们是预先验证的以确认排除。
8.3.3 @CacheEvict注解
缓存抽象不仅允许存储缓存,而且还可以清空缓存。 此过程对于从缓存中删除过时或未使用的数据很有用。 与@Cacheable相反,注解@CacheEvict划定了执行缓存删除的方法,即充当从缓存中移除数据的触发器的方法。 就像其兄弟一样,@CacheEvict需要指定一个(或多个)受操作影响的缓存,允许指定自定义的缓存和Key解析或条件,但另外还有一个额外的参数allEntries, 需要进行广泛的删除,而不是仅仅是一个条目(基于Key):
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
当需要清除整个缓存区域时,这个选项会派上用场,而不是逐条清除每个条目(这会耗费很长的时间,因为效率不高),所有的条目在一个操作中被删除,如上所示。 请注意,该框架将忽略在此方案中指定的任何键,因为它不适用(整个缓存不仅仅是一个条目)。
也可以指出是否应该在(默认)之后或通过beforeInvocation属性执行该方法之前进行清空。 前者提供与其余注释相同的语义 - 一旦方法成功完成,就执行缓存中的一个动作(在这种情况下是清空)。 如果该方法没有执行(因为它可能被缓存)或抛出异常,清空不会发生。 后者(beforeInvocation = true)导致清空始终发生在方法被调用之前 - 这在清空不需要与方法结果绑定的情况下是有用的。
需要注意的是void方法可以和@CacheEvict一起使用 - 由于这些方法充当触发器,所以返回值被忽略(因为它们不与缓存交互) - @Cacheable不是这种情况,它将添加/更新数据到缓存中, 并因此需要结果。
8.3.4. @Caching 注解
在某些情况下,需要指定相同类型的多个注解(例如@CacheEvict或@CachePut),例如因为条件或Key表达式在不同的缓存之间不同。 @Caching允许在同一个方法上使用多个嵌套的@Cacheable,@CachePut和@CacheEvict:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
8.3.5. @CacheConfig 注解
到目前为止,我们已经看到,缓存操作提供了许多定制选项,可以在操作基础上进行设置。 但是,如果某些自定义选项适用于该类的所有操作,则可能会进行繁琐的配置。 例如,指定要用于该类的每个缓存操作的缓存的名称可以由单个类级别的定义替换。 这是@CacheConfig使用的地方。
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
@CacheConfig是一个类级注解,允许共享缓存名称,自定义KeyGenerator,自定义CacheManager和自定义CacheResolver。 将该注解放在类上不会启用任何缓存操作。
操作级别的自定义配置制将始终覆盖@CacheConfig上的定制设置。 这给每个缓存操作三个级别的自定义:
- 全局配置,可用于CacheManager,KeyGenerator
- 在类级别,使用@CacheConfig
- 在操作级别
8.3.6. 启用缓存注解
需要注意的是,即使声明缓存注解并不会自动触发它们的动作 - 就像Spring中的许多东西一样,这个功能必须被声明性地启用(这意味着如果你怀疑缓存有问题的话,你可以通过删除 只有一个配置行,而不是代码中的所有注解)。
要启用缓存注解,请将注解@EnableCaching添加到您的@Configuration类之中:
@Configuration
@EnableCaching
public class AppConfig {
}
另外对于XML配置,使用cache:annotation-driven
元素
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven />
</beans>
cache:annotation-driven
元素和@EnableCaching注解都允许指定各种选项,以影响缓存行为通过AOP添加到应用程序的方式。 这个配置与@Transactional的配置有点相似:
用于处理缓存注解的默认建议模式是“代理”,其允许通过代理仅拦截调用; 同一类的本地调用不能被这样的拦截。 对于更高级的拦截模式,考虑切换到“aspectj”模式结合编译时或加载时织入。
用Java配置的高级定制需要实现CachingConfigurer:请参考javadoc了解更多细节。
表11. 缓存注解配置
XML属性 | 注解属性 | 默认 | 描述 |
---|---|---|---|
cache-manager | N/A(参见CachingConfigurer的javadocs) | cacheManager | 要使用的缓存管理器的名称。 一个默认的CacheResolver将在这个缓存管理器的后台被初始化(或者cacheManager如果没有设置)。 为了更细致地管理缓存解析,请考虑设置“cache-resolver”属性。 |
cache-resolver | N/A(参见CachingConfigurer的javadocs) | 使用配置的CacheManager的SimpleCacheResolver。 | CacheResolver用于解析后缓存。 此属性不是必需的,可以用“cache-manager”属性的替代。 |
key-generator | N/A(参见CachingConfigurer的javadocs) | SimplekeyGenerator | 要使用的自定义Key生成器的名称 |
error-handler | N/A(参见CachingConfigurer的javadocs) | SimpleCacheErrorHandler | 要使用的自定义缓存错误处理程序的名称。 默认情况下,在缓存相关操作期间抛出的任何异常都会在客户端抛出。 |
mode | mode | proxy | 默认模式“proxy”处理带注解的bean,使用Spring的AOP框架进行代理(遵循代理语义,如上所述,仅适用于通过代理进入的方法调用)。 替代模式“aspectj”用Spring的AspectJ缓存方面织入受影响的类,修改目标类字节代码以应用于任何类型的方法调用。 AspectJ织入要求classpath中的spring-aspects.jar以及启用加载时织入(或编译时织入)。 (有关如何设置加载时织入的详细信息,请参阅Spring配置。) |
proxy-target-class | proxyTargetClass | false | 仅适用于代理模式。 控制为使用@Cacheable或@CacheEvict注解进行注解的类创建哪种类型的缓存代理。 如果proxy-target-class属性设置为true,则创建基于类的代理。 如果proxy-target-class为false或者该属性被省略,则创建标准的基于JDK接口的代理。 (请参阅代理机制以详细了解不同的代理类型。) |
order | order | Ordered.LOWEST_PRECEDENCE | 定义应用于@Cacheable或@CacheEvict注解的bean的缓存Advice的顺序。 (有关与AOP Advice的排序有关的规则的更多信息,请参阅Advice ordering。)没有指定排序意味着AOP子系统确定Advice的顺序。 |
<cache:annotation-driven />只在它定义的同一应用程序上下文中寻找@Cacheable / @CachePut / @CacheEvict / @Caching。这意味着如果你把<cache:annotation-driven/>放入 一个DispatcherServlet的WebApplicationContext,它只检查你的Controller的bean,而不是你的服务。 有关更多信息,请参阅
方法可见性和缓存注解
使用代理时,应将缓存注解仅应用于具有公开可见性的方法。 如果使用这些注解标注受保护的,私有的或包可见的方法,则不会引发错误,但注释的方法不会显示已配置的缓存设置。 如果你需要注解非公共方法,考虑使用AspectJ(见下文),因为它改变了字节码本身。
Spring建议您仅使用@Cache *注解来注解具体类(以及具体类的方法),而不是注解接口。 您当然可以将@Cache *注解放在一个接口(或一个接口方法)上,但是这只能在您使用基于接口的代理的情况下使用。 Java注解没有从接口继承的事实意味着,如果您使用的是基于类的代理(proxy-target-class =“true”)或基于织入的aspect(mode =“aspectj”),那么缓存设置是 没有被代理和织入基础架构识别,并且该对象不会被包装在缓存代理中,这肯定是不好的。
在代理模式下(这是默认模式),只拦截通过代理进入的外部方法调用。 这意味着,即使被调用的方法被标记为@Cacheable,实际上,调用目标对象内目标对象的另一个方法的自调用也不会导致实际的缓存运行 - 考虑使用aspectj模式 这个案例。 此外,代理必须完全初始化,以提供预期的行为,所以您不应该在初始化代码中依赖此功能,即@PostConstruct。
8.3.7. 使用自定义注解
自定义注解和AspectJ
此功能只能使用基于代理的方法,但可以通过使用AspectJ进行额外的工作来启用。
Spring-aspects模块仅为标准注解定义了一个Aspect。 如果您定义了自己的注解,则还需要为这些注解定义一个Aspect。 检查AnnotationCacheAspect为例。
缓存抽象允许您使用自己的注解来确定触发缓存填充或清空的方法。 这是一个模板机制,因为它消除了重复缓存注解声明(特别是在指定键或条件时很有用)或者不允许在你的代码库中允许外部导入(org.springframework)的需要。 类似于其他的构造型注解,@Cacheable,@CachePut,@CacheEvict和@CacheConfig可以用作元注解,也就是可以注解其他注解的注解。 换句话说,让我们用我们自己的自定义注解替换一个常见的@Cacheable声明:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
上面,我们已经定义了我们自己的SlowService注解,它本身是用@Cacheable注解的 - 现在我们可以替换下面的代码:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
替换为:
@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
尽管@SlowService不是Spring注解,但是容器会在运行时自动获取它的声明并理解其含义。 请注意,如上所述,需要启用注释驱动的行为。
8.4. JCache (JSR-107) 注解
从Spring Framework 4.1开始,缓存抽象完全支持JCache标准注释:这些是@CacheResult,@CachePut,@CacheRemove和@CacheRemoveAll以及@CacheDefaults,@CacheKey和@CacheValue协同工具。 这些注解可以正确使用,无需将缓存存储迁移到JSR-107:内部实现使用Spring的缓存抽象,并提供与规范兼容的默认CacheResolver和KeyGenerator实现。 换句话说,如果您已经在使用Spring的缓存抽象,那么您可以切换到这些标准注释,而无需更改缓存存储(或配置)。
8.4.1. 特性概要
对于那些熟悉Spring的缓存注释的人来说,下表描述了Spring注释和JSR-107对应的主要区别:
表12 Spring和JSR-107缓存注解对比
Spring | JSR-107 | Remark |
---|---|---|
@Cacheable | @CacheResult | 相当相似。 无论缓存的内容如何,@CacheResult都可以缓存特定的异常并强制执行该方法。 |
@CachePut | @CachePut | 当Spring使用方法调用的结果更新缓存时,JCache需要将它作为使用@CacheValue注解的参数传递。 由于这种差异,JCache允许在实际方法调用之前或之后更新缓存。 |
@CacheEvict | @CacheRemove | 相当相似。 如果方法调用导致异常,@CacheRemove支持有条件的清除缓存。 |
@CacheEvict(allEntries=true) | @CacheRemoveAll | 参见@CacheRemove |
@CacheConfig | @CacheDefaults | 允许以类似的方式配置相同的概念。 |
JCache具有和javax.cache.annotation.CacheResolver相同的概念,与Spring的CacheResolver接口相同,只是JCache只支持单个缓存。 默认情况下,一个简单的实现根据注释中声明的名称检索要使用的缓存。 需要注意的是,如果注解中没有指定缓存名称,则会自动生成一个默认值,请查看@CacheResult#cacheName()的javadoc以获取更多信息。
CacheResolver实例由CacheResolverFactory提供。 可以自定义每个缓存操作的工厂:
@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class)
public Book findBook(ISBN isbn)
对于所有引用的类,Spring试图找到给定类型的bean。 如果存在多个匹配项,则会创建一个新实例,并可以使用常规bean生命周期回调,如依赖注入。
Keys 由一个javax.cache.annotation.CacheKeyGenerator生成,它的作用与Spring的KeyGenerator相同。 默认情况下,除非至少有一个参数使用@CacheKey注解,否则所有的方法参数都会被考虑在内。 这与Spring的自定义Key生成声明类似。 例如,这些是相同的操作,一个使用Spring的抽象,另一个使用JCache:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@CacheResult(cacheName="books")
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed)
也可以使用与CacheResolverFactory类似的方式在操作上指定要使用的CacheKeyResolver。
JCache可以管理注解方法抛出的异常:这可以防止更新缓存,但也可以缓存异常作为失败的指示符,而不是再次调用方法。 我们假设如果ISBN的结构无效,则抛出InvalidIsbnNotFoundException。 这是一个永久的失败,没有书可以用这样的参数检索。 以下缓存该异常,以便使用相同的无效ISBN的进一步调用将直接抛出缓存的异常,而不是再次调用该方法。
@CacheResult(cacheName="books", exceptionCacheName="failures"
cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)
8.4.2. 启用 JSR-107 支持
除了Spring的声明性注解支持外,没有什么具体的需要做JSR-107的支持。 如果JSR-107 API和spring-context-support模块都存在于类路径中,则@EnableCaching和cache:annotation-driven元素将自动启用JCache支持。
根据你的使用情况,选择基本上是你的。 您甚至可以使用JSR-107 API和其他使用Spring自己的注解来混合和匹配服务。 但请注意,如果这些服务正在影响相同的缓存,则应该使用一致且相同的Key生成实现。
8.5. 基于XML的声明性缓存配置
如果注解不是一个选项(不能访问源代码或没有外部代码),可以使用XML进行声明式缓存。 所以不是注解缓存的方法,而是从外部指定目标方法和缓存指令(类似于声明式事务管理通知)。 前面的例子可以翻译成:
<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
<cache:caching cache="books">
<cache:cacheable method="findBook" key="#isbn"/>
<cache:cache-evict method="loadBooks" all-entries="true"/>
</cache:caching>
</cache:advice>
<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
<!-- cache manager definition omitted -->
在上面的配置中,bookService是可缓存的。 要应用的缓存语义被封装在cache:advice定义中,该定义指示用于将数据放入缓存的方法findBooks,而用于清除数据的方法loadBooks。 这两个定义都是针对book缓存的。
aop:config定义通过使用AspectJ切入点表达式将缓存Advice应用于程序中的适当点(有关面向Aspect的Spring编程的更多信息)。 在上面的示例中,将考虑BookService中的所有方法,并将缓存Advice应用于它们。
声明式XML缓存支持所有基于注解的模型,因此在两者之间切换应该相当容易 - 两者都可以在同一个应用程序中使用。 基于XML的方法不会触及目标代码,但它本质上更加冗长; 当处理重载的缓存目标方法的类时,鉴别正确的方法确实需要额外的努力,因为方法论不是一个好的鉴别器 - 在这些情况下,AspectJ切入点可以用来选择目标方法并应用适当的 缓存功能。 然而,通过XML,应用一个包/组/接口范围的缓存(再次归因于AspectJ切入点)并创建类似于模板的定义更为容易(正如我们在上面的示例中所做的那样,通过使用cache:definitionscache属性来定义目标缓存)。
8.6. 配置缓存存储
开箱即用,缓存抽象提供了多种存储集成。 要使用它们,需要简单地声明一个适当的CacheManager - 一个控制和管理Caches的实体,可以用来检索这些实体以进行存储。
8.6.1. 基于JDK ConcurrentMap缓存
基于JDK的Cache实现位于org.springframework.cache.concurrent包下。 它允许使用ConcurrentHashMap作为后备Cache存储。
<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
</set>
</property>
</bean>
上面的代码片断使用SimpleCacheManager为两个名为default和books的嵌套的ConcurrentMapCache实例创建一个CacheManager。 请注意,名称是针对每个缓存直接配置的。
由于缓存是由应用程序创建的,因此它被绑定到其生命周期,使其适用于基本的用例,测试或简单的应用程序。 缓存扩展性好,速度非常快,但是不提供任何管理或持久性功能,也不提供清除协义。
8.6.2 基于Ehcache的缓存
Ehcache 3.x完全兼容JSR-107,不需要专门的支持。
Ehcache 2.x实现位于org.springframework.cache.ehcache包下。 同样,要使用它,只需要声明适当的CacheManager:
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
<!-- EhCache library setup -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
这个设置引导了Spring IoC中的ehcache库(通过ehcache bean),然后将其连接到专用的CacheManager实现中。 请注意,整个ehcache特定的配置是从ehcache.xml中读取的。
8.6.3. Caffeine 缓存
Caffeine是对Guava缓存的Java 8重写,其实现位于org.springframework.cache.caffeine包下,并提供对Caffeine的几个功能的访问。
配置按需创建缓存的CacheManager非常简单:
<bean id="cacheManager"
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
也可以提供明确使用的缓存。 在这种情况下,只有Manager才能提供这些资料。
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="caches">
<set>
<value>default</value>
<value>books</value>
</set>
</property>
</bean>
Caffeine CacheManager还支持自定义Caffeine和CacheLoader。 有关这些的更多信息,请参阅Caffeine文档。
8.6.4 基于GemFire的缓存
GemFire是一个面向内存/磁盘备份,可弹性扩展,持续可用,活动(具有内置的基于模式的订阅通知),全局复制的数据库,并提供全功能的边缘缓存。 有关如何使用GemFire作为CacheManager(以及更多)的更多信息,请参阅Spring Data GemFire参考文档。
8.6.5. JSR-107 缓存
Spring的缓存抽象也可以使用兼容JSR-107的缓存。 JCache实现位于org.springframework.cache.jcache包下。
同样,要使用它,只需要声明适当的CacheManager:
<bean id="cacheManager" class="org.springframework.cache.jcache.JCacheCacheManager" p:cache-manager-ref="jCacheManager"/>
<!-- JSR-107 cache manager setup -->
<bean id="jCacheManager" .../>
8.6.6 处理没有后台存储的缓存
有时,在切换环境或进行测试时,可能会有缓存声明,而没有配置实际的后备缓存。 由于这是一个无效配置,因此在运行时将会抛出异常,因为缓存基础结构无法找到合适的存储。 在这种情况下,除了删除缓存声明(这可能很乏味)之外,可以通过简单的虚拟缓存来实现缓存 - 也就是强制缓存的方法每次都被执行:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="jdkCache"/>
<ref bean="gemfireCache"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
上面的CompositeCacheManager链接多个CacheManagers,另外还通过fallbackToNoOpCache标志添加了一个无操作缓存,该缓存对于所有未由配置的缓存管理器处理的定义。 也就是说,没有在jdkCache或gemfireCache(上面配置的)中找到的每个缓存定义都将由no op缓存处理,而不会存储导致每次执行目标方法的任何信息。
8.7 插入不同的后端缓存
显然有很多的缓存产品可以用作后台存储。 为了插入它们,需要提供一个CacheManager和Cache实现,因为不幸的是我们没有可用的标准。 这听起来可能听起来比较困难,因为在实践中,这些类往往是简单的适配器,将缓存抽象框架映射到存储API的顶层,就像ehcache类可以显示的一样。 大多数CacheManager类可以使用org.springframework.cache.support包中的类,例如AbstractCacheManager,它负责处理boiler-plate代码,只留下实际的映射。 我们希望及时提供与Spring集成的库可以填补这个小配置空白。
8.8。 如何设置TTL/TTI/清除策略/XXX功能?
直接通过您的缓存提供商。 缓存抽象是...好吧,抽象不是缓存实现。 您正在使用的解决方案可能支持各种数据策略和其他解决方案不支持的拓扑结构(例如,JDK ConcurrentHashMap) - 只是因为没有后台支持,所以在缓存抽象中是无用的。 这种功能应该通过支持缓存直接控制,当配置它或通过它的本地API。