官网介绍中文翻译如下:
8.缓存抽象
从 version 3.1 开始,Spring Framework 支持透明地向现有的 Spring application 添加缓存。与交易支持类似,缓存抽象允许一致使用各种缓存解决方案,而对 code 的影响最小。
从 Spring 4.1 开始,在JSR-107 注释和更多自定义选项的支持下,缓存抽象得到了显着改进。
8.1. 了解缓存抽象
缓存与缓冲区
术语“缓冲区”和“缓存”倾向于可互换使用。但请注意,它们代表不同的东西。传统上,缓冲区用作快速和慢速实体之间数据的中间临时 store。由于一方必须等待另一方(这会影响 performance),缓冲区通过允许整个数据块一次移动而不是小块来缓解这种情况。数据仅从缓冲区写入和读取一次。此外,缓冲区对于至少知道它的一方是可见的。
另一方面,高速缓存根据定义是隐藏的,并且任何一方都不知道发生了高速缓存。它还改善了 performance,但是通过快速方式多次读取相同的数据来实现这一点。
您可以找到缓冲区和缓存这里之间差异的进一步说明。
缓存抽象的核心是将缓存应用于 Java 方法,从而根据缓存中可用的信息减少执行次数。也就是说,每个 time 调用一个目标方法,抽象应用一个缓存行为,检查该方法是否已经为给定的 arguments 执行。如果已执行,则返回缓存的结果,而不必执行实际方法。如果该方法尚未执行,则执行该方法,并将结果缓存并返回给用户,以便在调用该方法的下一个 time 时,返回缓存的结果。这样,对于给定的一组参数,昂贵的方法(无论是 CPU-或 IO-bound)只能执行一次,并且重用结果而不必再次实际执行该方法。缓存逻辑是透明应用的,不会对调用者造成任何干扰。
此方法仅适用于保证为给定输入(或 arguments)返回相同输出(结果)的方法,无论它执行多少次。
缓存抽象提供其他 cache-related 操作,例如更新缓存内容或删除一个或所有条目的能力。如果缓存处理在 application 过程中可能发生变化的数据,这些非常有用。
与 Spring Framework 中的其他服务一样,缓存服务是抽象(不是缓存实现),并且需要使用实际存储来存储缓存数据 - 也就是说,抽象使您无需编写缓存逻辑,但是不提供实际数据 store。这种抽象由org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口实现。
Spring 提供了一些实现的抽象:基于 JDK java.util.concurrent.ConcurrentMap
的缓存,Ehcache 2.x,Gemfire 缓存,咖啡因和 JSR-107 兼容缓存(例如 Ehcache 3.x)。有关插入其他缓存存储和提供程序的更多信息,请参阅Plugging-in 不同的 Back-end 缓存。
缓存抽象对 multi-threaded 和 multi-process 环境没有特殊处理,因为 features 由 cache implementation 处理。 。
如果您有 multi-process 环境(即,在多个节点上部署了一个 application),则需要相应地配置缓存提供程序。根据您的使用情况,几个节点上的相同数据的副本就足够了。但是,如果在 application 过程中更改数据,则可能需要启用其他传播机制。
缓存特定 item 直接等效于程序化缓存交互中发现的典型 get-if-not-found-then - proceed-and-put-eventually code 块。没有应用锁,并且多个线程可能会尝试同时加载相同的 item。驱逐也是如此。如果多个线程试图同时更新或逐出数据,则可以使用陈旧数据。某些缓存提供程序在该区域中提供高级 features。有关更多详细信息,请参阅缓存提供程序的文档。
要使用缓存抽象,您需要注意两个方面:
-
缓存声明:确定需要缓存的方法及其 policy。
-
高速缓存 configuration:存储数据并从中读取数据的后备高速缓存。
8.2. 声明式 Annotation-based 缓存
对于缓存声明,Spring 的缓存抽象提供了一组 Java annotations:
-
@Cacheable
:触发缓存填充。 -
@CacheEvict
:触发缓存逐出。 -
@CachePut
:更新缓存而不会干扰方法执行。 -
@Caching
:重新组合要在方法上应用的多个缓存操作。 -
@CacheConfig
:在 class-level 分享一些 common cache-related 设置。
8.2.1. @Cacheable Annotation
正如 name 所暗示的那样,您可以使用@Cacheable
来划分可缓存的方法 - 即,将结果存储在缓存中的方法,以便在后续调用(具有相同的 arguments)时,返回缓存中的 value 而不必须实际执行该方法。在最简单的形式中,annotation 声明需要与带注释的方法关联的缓存的 name,如下面的 example 所示:
@Cacheable("books") public Book findBook(ISBN isbn) {...}
在前面的代码段中,findBook
方法与名为books
的缓存关联。调用该方法的每个 time 时,都会检查缓存以查看调用是否已经执行且不必重复。虽然在大多数情况下,只声明了一个缓存,但 annotation 允许指定多个名称,以便使用多个缓存。在这种情况下,在执行方法之前检查每个缓存 - 如果至少有一个缓存被命中,则返回关联的 value。
所有其他不包含 value 的高速缓存也会更新,即使实际上没有执行高速缓存的方法。
以下 example 在findBook
方法上使用@Cacheable
:
@Cacheable({"books", "isbns"}) public Book findBook(ISBN isbn) {...}
默认 Key Generation
由于高速缓存本质上是 key-value stores,因此需要将每个高速缓存方法的调用转换为适合高速缓存访问的 key。缓存抽象使用基于以下算法的简单KeyGenerator
:
-
如果没有给出参数,return
SimpleKey.EMPTY
。 -
如果只给出一个参数,则 return 该实例。
-
如果给出了更多的参数,则 return
SimpleKey
包含所有参数。
这种方法适用于大多数 use-cases,因为 long 因为参数具有自然键并实现有效的hashCode()
和equals()
方法。如果不是这种情况,则需要更改策略。
要提供不同的默认 key generator,需要实现org.springframework.cache.interceptor.KeyGenerator
接口。
随着 Spring 4.0 的发布,默认的 key 生成策略发生了变化。早期版本的 Spring 使用了 key 生成策略,对于多个 key 参数,只考虑
hashCode()
参数而不是equals()
。这可能会导致意外的 key 碰撞(请参阅背景的SPR-10237)。新的SimpleKeyGenerator
使用复合 key 来表示这种情况。
如果要继续使用以前的 key 策略,可以配置已弃用的org.springframework.cache.interceptor.DefaultKeyGenerator
class 或创建自定义 hash-based KeyGenerator
implementation。
自定义 Key Generation 声明
由于缓存是通用的,因此目标方法很可能具有各种签名,这些签名无法轻松映射到缓存结构之上。当目标方法具有多个 arguments 时,这往往变得明显,其中只有一些适合于缓存(而 rest 仅由方法逻辑使用)。考虑以下 example:
@Cacheable("books") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然两个boolean
arguments 影响了书的发现方式,但它们对缓存没有用处。如果两个中只有一个重要而另一个不重要怎么办?
对于这种情况,@Cacheable
annotation 允许您指定 key 如何通过其key
属性生成。您可以使用规划环境地政司来选择感兴趣的 arguments(或它们的嵌套 properties),执行操作,甚至调用任意方法,而无需编写任何 code 或实现任何接口。这是默认 generator的推荐方法,因为随着 code 基数的增长,签名在签名方面往往会有很大不同。虽然默认策略可能适用于某些方法,但它很少适用于所有方法。
以下示例是各种 SpEL 声明(如果您不熟悉 SpEL,请自己帮忙并阅读Spring 表达语言):
@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)
前面的片段显示了选择某个参数,其中一个 properties,甚至是一个任意(静态)方法是多么容易。
如果负责生成 key 的算法太具体或者需要共享,则可以在操作上定义自定义keyGenerator
。为此,请指定要使用的KeyGenerator
bean implementation 的 name,如下面的 example 所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
key
和keyGenerator
参数是互斥的,并且指定两者的操作都会导致 exception。
默认缓存分辨率
缓存抽象使用一个简单的CacheResolver
,它使用配置的CacheManager
检索操作 level 中定义的缓存。
要提供不同的默认缓存解析程序,需要实现org.springframework.cache.interceptor.CacheResolver
接口。
自定义缓存分辨率
默认缓存分辨率非常适合使用单个CacheManager
且没有复杂缓存分辨率要求的 applications。
对于适用于多个缓存 managers 的 applications,您可以设置cacheManager
用于每个操作,如下面的 example 所示:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1) public Book findBook(ISBN isbn) {...}
1 | 指定anotherCacheManager 。 |
您也可以以类似于替换key 代的方式完全替换CacheResolver
。为每个缓存操作请求解析,让 implementation 实际上解析基于 runtime arguments 使用的缓存。以下 example 显示了如何指定CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver") (1) public Book findBook(ISBN isbn) {...}
1 | 指定CacheResolver 。 |
从 Spring 4.1 开始,cache annotations 的
value
属性不再是必需的,因为无论 annotation 的内容如何,CacheResolver
都可以提供此特定信息。
与key
和keyGenerator
类似,cacheManager
和cacheResolver
参数是互斥的,指定两者的操作都会导致 exception。作为自定义CacheManager
被CacheResolver
implementation 忽略。这可能不是你所期望的。
同步缓存
在 multi-threaded 环境中,可能会为同一参数同时调用某些操作(通常在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的 value,从而无法实现缓存。
对于这些特定情况,您可以使用sync
属性来指示底层缓存提供程序在计算 value 时锁定缓存条目。因此,只有一个线程忙于计算 value,而其他线程则被阻塞,直到缓存中的条目更新为止。以下 example 显示了如何使用sync
属性:
@Cacheable(cacheNames="foos", sync=true) (1) public Foo executeExpensiveOperation(String id) {...}
1 | 使用sync 属性。 |
这是一个可选的 feature,您最喜欢的缓存 library 可能不支持它。核心 framework 提供的所有
CacheManager
_implement 都支持它。有关更多详细信息,请参阅缓存提供程序的文档。
条件缓存
有时,方法可能不适合缓存所有 time(对于 example,它可能取决于给定的 arguments)。 cache annotations 通过condition
参数支持此类功能,该参数采用SpEL
表达式,该表达式被评估为true
或false
。如果true
,则缓存该方法。如果不是,则其行为就好像该方法未被缓存(即,无论缓存中的值是什么,或者使用了哪些 arguments,该方法每 time 执行一次)。对于 example,仅当参数name
的长度小于 32 时,才会缓存以下方法:
@Cacheable(cacheNames="book", condition="#name.length() < 32") (1) public Book findBook(String name)
1 | 在@Cacheable 上设置条件。 |
除了condition
参数之外,您还可以使用unless
参数来否决向缓存添加 value。与condition
不同,unless
表达式在调用方法后进行计算。要扩展上一个 example,也许我们只想缓存平装书,如下例所示:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1) public Book findBook(String name)
1 | 使用unless 属性来阻止精装本。 |
高速缓存抽象支持java.util.Optional
,仅当它存在时才使用其内容作为高速缓存的 value。 #result
始终引用业务实体,从不支持 wrapper,因此可以按如下方式重写上一个 example:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback") public Optional<Book> findBook(String name)
请注意,result
仍然是指Book
而不是Optional
。因为它可能是null
,我们应该使用安全导航 operator。
可用缓存 SpEL Evaluation Context
每个SpEL
表达式都针对专用context进行求值。除 built-in 参数外,framework 还提供专用的 caching-related 元数据,例如参数名称。以下 table 描述了 context 可用的项目,以便您可以将它们用于 key 和条件计算:
名称 | 地点 | 描述 | 例 |
---|---|---|---|
methodName |
Root object | 要调用的方法的 name | #root.methodName |
method |
Root object | 正在调用的方法 | #root.method.name |
target |
Root object | 正在调用目标 object | #root.target |
targetClass |
Root object | 正在调用的目标的 class | #root.targetClass |
args |
Root object | arguments(as array)用于调用目标 | #root.args[0] |
caches |
Root object | 执行当前方法的高速缓存的集合 | #root.caches[0].name |
参数 name | Evaluation context | 任何方法 arguments 的 Name。如果名称不可用(可能由于没有调试信息),参数名称也可以在#a<#arg> 下获得,其中#arg 代表参数索引(从0 开始)。 |
#iban 或#a0 (您也可以使用#p0 或#p<#arg> 表示法作为别名)。 |
result |
Evaluation context | 方法调用的结果(要缓存的 value)。仅在unless 表达式,cache put 表达式(用于计算key )或cache evict 表达式(当beforeInvocation 为false 时)中可用。对于受支持的包装器(例如Optional ),#result 指的是实际的 object,而不是 wrapper。 |
#result |
8.2.2. @CachePut Annotation
当需要更新缓存而不干扰方法执行时,可以使用@CachePut
annotation。也就是说,始终执行该方法,并将其结果放入缓存中(根据@CachePut
选项)。它支持与@Cacheable
相同的选项,应该用于缓存填充而不是方法流优化。以下 example 使用@CachePut
annotation:
@CachePut(cacheNames="book", key="#isbn") public Book updateBook(ISBN isbn, BookDescriptor descriptor)
通常强烈建议不要在同一方法上使用
@CachePut
和@Cacheable
注释,因为它们具有不同的行为。虽然后者导致通过使用缓存跳过方法执行,但前者强制执行 order 以执行缓存更新。这会导致意外行为,并且由于特定 corner-cases 的 exception(例如 annotations 具有将它们彼此排除的条件),应该避免这样的声明。另请注意,此类条件不应依赖于结果 object(即#result
变量),因为这些条件已经过验证 up-front 以确认排除。
8.2.3. @CacheEvict annotation
缓存抽象不仅允许缓存 store 的填充,还允许驱逐。此 process 对于从缓存中删除过时或未使用的数据非常有用。与@Cacheable
相反,@CacheEvict
划分执行缓存逐出的方法(即,用作从缓存中删除数据的触发器的方法)。与其兄弟类似,@CacheEvict
需要指定受操作影响的一个或多个缓存,允许自定义缓存和 key 解析或指定条件,并 features 一个额外参数(allEntries
),指示 cache-wide 驱逐是否需要执行而不仅仅是一个条目驱逐(基于 key)。以下 example 清除books
缓存中的所有条目:
@CacheEvict(cacheNames="books", allEntries=true) (1) public void loadBooks(InputStream batch)
1 | 使用allEntries 属性驱逐缓存中的所有条目。 |
当需要清除整个缓存区域时,此选项会派上用场。而不是驱逐每个条目(这将花费 long time,因为它效率低),所有条目在一个操作中被移除,如前面的 example 所示。请注意,framework 忽略了此方案中指定的任何 key,因为它不适用(整个缓存被驱逐,而不仅仅是一个条目)。
您还可以通过使用beforeInvocation
属性指示在(默认)之后或方法执行之前是否应该进行逐出。前者提供与 annotations 的 rest 相同的语义:一旦方法成功完成,就会执行缓存上的操作(在本例中为驱逐)。如果方法未执行(因为它可能被缓存)或抛出 exception,则不会发生逐出。后者(beforeInvocation=true
)导致驱逐始终在调用方法之前发生。这在驱逐不需要与方法结果相关联的情况下非常有用。
请注意void
方法可以与@CacheEvict
一起使用 - 因为方法充当触发器,return 值被忽略(因为它们不与缓存交互)。 @Cacheable
不是这种情况,它将数据添加或更新到缓存中,因此需要一个结果。
8.2.4. @Caching Annotation
有时,需要指定多个相同类型的注释(例如@CacheEvict
或@CachePut
) - 例如,因为条件或 key 表达式在不同的缓存之间是不同的。 @Caching
允许在同一方法上使用多个嵌套的@Cacheable
,@CachePut
和@CacheEvict
注释。以下 example 使用两个@CacheEvict
注释:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) public Book importBooks(String deposit, Date date)
8.2.5. @CacheConfig annotation
到目前为止,我们已经看到缓存操作提供了许多自定义选项,您可以为每个操作设置这些选项。但是,如果某些自定义选项适用于 class 的所有操作,则可能需要配置一些自定义选项。例如,指定用于 class 的每个高速缓存操作的高速缓存的 name 可以由单个 class-level 定义替换。这是@CacheConfig
发挥作用的地方。以下示例使用@CacheConfig
来设置缓存的 name:
@CacheConfig("books") (1) public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...} }
1 | 使用@CacheConfig 设置缓存的 name。 |
@CacheConfig
是注释,允许共享缓存名称,自定义KeyGenerator
,自定义CacheManager
和自定义CacheResolver
。将此 annotation 放在 class 上不会打开任何缓存操作。
operation-level 自定义始终会覆盖@CacheConfig
上的自定义集。因此,这为每个缓存操作提供了三个级别的自定义:
-
全局配置,可用于
CacheManager
,KeyGenerator
。 -
在 class level 中,使用
@CacheConfig
。 -
在 level 操作。
8.2.6. 启用缓存注释
重要的是要注意,尽管声明缓存注释不会自动触发它们的操作 - 就像 Spring 中的许多内容一样,feature 必须以声明方式启用(这意味着如果您怀疑缓存是责任,您可以通过删除来禁用它只有一个 configuration line 而不是 code 中的所有 annotations。
要启用缓存注释,请将注释@EnableCaching
添加到@Configuration
classes 中的一个:
@Configuration
@EnableCaching
public class AppConfig {
}
或者,对于 XML configuration,您可以使用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
annotation 都允许您指定影响缓存行为通过 AOP 添加到 application 的方式的各种选项。 configuration 有意与@Transactional类似。
处理缓存注释的默认建议模式是
proxy
,它允许仅通过代理拦截 calls。同一 class 中的本地 calls 不能以这种方式截获。对于更高级的拦截模式,请考虑结合 compile-time 或 load-time 编织切换到aspectj
模式。
有关实现
CachingConfigurer
所需的高级自定义(使用 Java configuration)的更多详细信息,请参阅javadoc。
XML 属性 | Annotation 属性 | 默认 | 描述 |
---|---|---|---|
cache-manager |
N/A(参见CachingConfigurerjavadoc) | cacheManager |
要使用的缓存 manager 的 name。使用此缓存 manager 在后台初始化默认CacheResolver (如果未设置,则为cacheManager )。有关缓存分辨率的更多 fine-grained 管理,请考虑设置“cache-resolver”属性。 |
cache-resolver |
N/A(参见CachingConfigurerjavadoc) | A SimpleCacheResolver 使用配置的cacheManager 。 |
要用于解析后备高速缓存的 CacheResolver 的 bean name。此属性不是必需的,只需要指定为'cache-manager'属性的替代。 |
key-generator |
N/A(参见CachingConfigurerjavadoc) | SimpleKeyGenerator |
要使用的自定义 key generator 的名称。 |
error-handler |
N/A(参见CachingConfigurerjavadoc) | SimpleCacheErrorHandler |
要使用的自定义缓存错误处理程序的 name。默认情况下,在缓存相关操作期间抛出的任何 exception 都会返回到 client。 |
mode |
mode |
proxy |
默认模式(proxy )通过使用 Spring 的 AOP framework 处理带注释的 beans 代理(遵循代理语义,如前所述,仅适用于通过代理进入的方法 calls)。替代模式(aspectj )使用 Spring 的 AspectJ 缓存 aspect 编译受影响的 classes,修改目标 class byte code 以应用于任何类型的方法调用。 AspectJ 编织需要在 classpath 中启用spring-aspects.jar 以及启用 load-time 编织(或 compile-time 编织)。 (有关如何设置 load-time weaving.)的详细信息,请参阅Spring configuration |
proxy-target-class |
proxyTargetClass |
false |
仅适用于代理模式。控制为使用@Cacheable 或@CacheEvict 注释注释的 class 创建的缓存代理类型。如果proxy-target-class 属性设置为true ,则会创建 class-based 个代理。如果proxy-target-class 是false 或者省略了该属性,则会创建标准 JDK interface-based 代理。 (有关不同代理人的详细检查,请参阅代理机制 types.) |
order |
order |
Ordered.LOWEST_PRECEDENCE | 定义应用于使用@Cacheable 或@CacheEvict 注释的 beans 的缓存建议的 order。 (有关与排序 AOP 建议相关的规则的更多信息,请参阅建议订购 .)未指定 ordering 意味着 AOP 子系统确定建议的 order。 |
<cache:annotation-driven/>
仅在定义它的同一 application context 中的 beans 上查找@Cacheable/@CachePut/@CacheEvict/@Caching
。这意味着,如果您将<cache:annotation-driven/>
放在WebApplicationContext
中DispatcherServlet
,它只会在您的控制器中检查 beans,而不是您的服务。有关更多信息,请参见MVC 部分。
方法可见性和缓存注释
使用代理时,应将 cache annotations 仅应用于具有公共可见性的方法。如果使用这些注释注释 protected,private 或 package-visible 方法,则不会引发错误,但带注释的方法不会显示已配置的缓存设置。如果需要注释 non-public 方法,请考虑使用 AspectJ(参见本节的 rest),因为它会更改字节码本身。
Spring 建议您只使用
@Cache*
annotation 注释具体的 classes(以及具体 classes 的方法),而不是注释接口。您当然可以将@Cache*
annotation 放在接口(或接口方法)上,但这只能在您使用 interface-based 代理时按预期工作。 Java annotations 不是从接口继承的事实意味着,如果使用 class-based 代理(proxy-target-class="true"
)或 weaving-based aspect(mode="aspectj"
),代理和编织基础结构无法识别缓存设置,并且 object 不包含在缓存代理,这将是非常糟糕的。
在代理模式(默认)下,只拦截通过代理进入的外部方法 calls。这意味着 self-invocation(实际上,目标 object 中的一个方法 calls 目标 object 的另一个方法)在运行时不会导致实际的缓存,即使被调用的方法用
@Cacheable
标记。在这种情况下,请考虑使用aspectj
模式。此外,必须完全初始化代理以提供预期的行为,因此您不应该在初始化 code(即@PostConstruct
)中依赖此 feature。
8.2.7. 使用自定义注释
自定义 annotation 和 AspectJ
此 feature 仅适用于 proxy-based 方法,但可以通过使用 AspectJ 进行一些额外的工作。
spring-aspects
模块仅为标准 annotations 定义 aspect。如果您已经定义了自己的注释,则还需要为这些注释定义 aspect。检查AnnotationCacheAspect
是否为 example。
缓存抽象允许您使用自己的注释来标识触发缓存填充或驱逐的方法。这作为模板机制非常方便,因为它消除了复制缓存注释声明的需要,如果指定了 key 或条件或者 code 基础中不允许外部导入(org.springframework
),这尤其有用。与铅板 annotations 的 rest 类似,您可以使用@Cacheable
,@CachePut
,@CacheEvict
和@CacheConfig
作为meta-annotations(即可以注释其他注释的注释)。在下面的示例中,我们用自己的自定义 annotation 替换 common @Cacheable
声明:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
在前面的 example 中,我们定义了自己的SlowService
annotation,它本身用@Cacheable
注释。现在我们可以替换以下 code:
@Cacheable(cacheNames="books", key="#isbn") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
以下 example 显示了 custom annotation,我们可以使用它来替换前面的 code:
@SlowService public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
即使@SlowService
不是 Spring annotation,容器也会在运行时自动获取其声明并理解其含义。请注意,如上所述前,需要启用 annotation-driven 行为。
8.3. JCache(JSR-107)Annotations
从 version 4.1 开始,缓存抽象完全支持 JCache 标准 annotations:@CacheResult
,@CachePut
,@CacheRemove
和@CacheRemoveAll
以及@CacheDefaults
,@CacheKey
和@CacheValue
伴随。您可以使用这些注释而无需将缓存 store 迁移到 JSR-107。内部 implementation 使用 Spring 的缓存抽象,并提供符合规范的默认CacheResolver
和KeyGenerator
implementations。换句话说,如果您已经在使用 Spring 的缓存抽象,则可以切换到这些标准注释而无需更改缓存存储(或 configuration)。
8.3.1. Feature 摘要
对于那些熟悉 Spring 的缓存注释的人,下面的 table 描述了 Spring annotations 和 JSR-107 对应物之间的主要区别:
弹簧 | JSR-107 | 备注 |
---|---|---|
@Cacheable |
@CacheResult |
非常相似。 @CacheResult 可以缓存特定的 exceptions 并强制执行该方法,而不管缓存的内容如何。 |
@CachePut |
@CachePut |
当 Spring 使用方法调用的结果更新缓存时,JCache 要求将其作为使用@CacheValue 注释的参数传递。由于这种差异,JCache 允许在实际方法调用之前或之后更新缓存。 |
@CacheEvict |
@CacheRemove |
非常相似。当方法调用导致 exception 时,@CacheRemove 支持条件驱逐。 |
@CacheEvict(allEntries=true) |
@CacheRemoveAll |
见@CacheRemove 。 |
@CacheConfig |
@CacheDefaults |
允许您以类似的方式配置相同的概念。 |
JCache 有javax.cache.annotation.CacheResolver
的概念,它与 Spring 的CacheResolver
接口相同,只是 JCache 只支持单个缓存。默认情况下,一个简单的 implementation 根据 annotation 上声明的 name 检索要使用的缓存。应该注意的是,如果在 annotation 上没有指定 cache name,则会自动生成默认值。有关更多信息,请参见@CacheResult#cacheName()
的 javadoc。
CacheResolverFactory
实例由CacheResolverFactory
检索。可以为每个缓存操作自定义工厂,如下面的示例所示:
@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) (1) public Book findBook(ISBN isbn)
1 | 为此操作自定义工厂。 |
对于所有引用的 classes,Spring 尝试找到具有给定类型的 bean。如果存在多个 match,则会创建一个新实例,并且可以使用常规 bean 生命周期回调,例如依赖项注入。
密钥由javax.cache.annotation.CacheKeyGenerator
生成,其作用与 Spring 的KeyGenerator
相同。默认情况下,除非至少有一个参数使用@CacheKey
注释,否则将考虑所有方法 arguments。这类似于 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)
您还可以在操作上指定CacheKeyResolver
,类似于指定CacheResolverFactory
的方式。
JCache 可以管理带注释方法抛出的 exceptions。这可以防止更新缓存,但它也可以将 exception 缓存为失败的指示器,而不是再次调用该方法。假设如果 ISBN 的结构无效,则抛出InvalidIsbnNotFoundException
。这是一个永久性的失败(没有用这样的参数检索书籍)。下面缓存 exception,以便进一步 calls 具有相同的无效 ISBN 直接抛出缓存的 exception 而不是再次调用该方法:
@CacheResult(cacheName="books", exceptionCacheName="failures" cachedExceptions = InvalidIsbnNotFoundException.class) public Book findBook(ISBN isbn)
8.3.2. 启用 JSR-107 支持
除了 Spring 的声明性 annotation 支持之外,您无需执行任何特定的操作来启用 JSR-107 支持。如果 class 路径中存在 JSR-107 API 和spring-context-support
模块,则@EnableCaching
和cache:annotation-driven
元素都会自动启用 JCache 支持。
根据您的使用情况,选择基本上是您的。您甚至可以在某些服务上使用 JSR-107 API 并在其他服务器上使用 Spring 自己的注释来混合和 match 服务。但是,如果这些服务影响相同的缓存,则应使用一致且相同的 key generation implementation。
8.4. 声明式 XML-based 缓存
如果 annotations 不是一个选项(可能是由于无法访问源或没有外部 code),您可以使用 XML 进行声明性缓存。因此,您可以在外部指定目标方法和缓存指令,而不是注释缓存方法(类似于声明式 transaction management 忠告)。上一节中的 example 可以转换为以下 example:
<!-- 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 -->
在前面的 configuration 中,bookService
可以缓存。要应用的缓存语义封装在cache:advice
定义中,这会导致findBooks
方法用于将数据放入缓存,而loadBooks
方法用于驱逐数据。这两个定义都适用于books
缓存。
aop:config
定义通过使用 AspectJ 切入点表达式将缓存建议应用于程序中的适当点(更多信息在使用 Spring 进行面向对象编程中可用)。在前面的 example 中,将考虑BookService
中的所有方法,并将缓存建议应用于它们。
声明性 XML 缓存支持所有 annotation-based model,因此在两者之间移动应该相当容易。此外,两者都可以在同一个 application 中使用。 XML-based 方法不会触及目标 code。然而,它本质上更加冗长。当处理具有重载方法的 classes 时,识别正确的方法确实需要额外的努力,因为method
参数不是一个好的鉴别器。在这些情况下,您可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。但是,通过 XML,更容易应用包或 group 或 interface-wide 缓存(同样,由于 AspectJ 切入点)并创建 template-like 定义(正如我们在前面的 example 中通过cache:definitions
cache
属性定义目标缓存所做的那样)。
8.5. 配置缓存存储
缓存抽象提供了几个存储 integration 选项。要使用它们,您需要声明一个适当的CacheManager
(一个控制和管理Cache
实例的实体,可用于检索这些实例以进行存储)。
8.5.1. JDK ConcurrentMap-based 缓存
JDK-based Cache
implementation 位于org.springframework.cache.concurrent
包下。它允许您使用ConcurrentHashMap
作为后台Cache
store。以下 example 显示了如何配置两个缓存:
<!-- 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
。请注意,名称是直接为每个缓存配置的。
由于缓存是由 application 创建的,因此它被绑定到其生命周期,使其适用于基本用例,测试或简单的应用程序。缓存可以很好地扩展并且非常快,但它不提供任何管理,持久性功能或逐出 contracts。
8.5.2. Ehcache-based 缓存
Ehcache 3.x 完全符合 JSR-107 标准,不需要专门的支持。
Ehcache 2.x implementation 位于org.springframework.cache.ehcache
包中。同样,要使用它,您需要声明适当的CacheManager
。以下 example 显示了如何执行此操作:
<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 library(通过ehcache
bean),然后将其连接到专用的CacheManager
implementation。请注意,从ehcache.xml
读取整个 ehcache-specific configuration。
8.5.3. Caffeine Cache
Caffeine 是 Guava 缓存的 Java 8 rewrite,它的 implementation 位于org.springframework.cache.caffeine
包中,可以访问 Caffeine 的几个 feature。
以下 example 配置CacheManager
,根据需要创建缓存:
<bean id="cacheManager"
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
您还可以提供明确使用的缓存。在这种情况下,只有那些由 manager 提供。以下 example 显示了如何执行此操作:
<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.5.4. GemFire-based 缓存
GemFire 是一个 memory-oriented,disk-backed,可弹性扩展,持续可用,active(具有 built-in pattern-based 订阅通知),全局复制数据库并提供 fully-featured 边缘缓存。有关如何将 GemFire 用作CacheManager
(及更多)的更多信息,请参阅Spring Data GemFire reference 文档。
8.5.5. JSR-107 缓存
Spring 的缓存抽象也可以使用 JSR-107-compliant 缓存。 JCache implementation 位于org.springframework.cache.jcache
包中。
同样,要使用它,您需要声明适当的CacheManager
。以下 example 显示了如何执行此操作:
<bean id="cacheManager" class="org.springframework.cache.jcache.JCacheCacheManager" p:cache-manager-ref="jCacheManager"/> <!-- JSR-107 cache manager setup --> <bean id="jCacheManager" .../>
8.5.6. 在没有支持 Store 的情况下处理缓存
有时,在切换环境或进行测试时,您可能会在没有配置实际的后备缓存的情况下进行缓存声明。由于这是一个无效的 configuration,因此在运行时抛出了 exception,因为缓存基础结构无法找到合适的 store。在这种情况下,而不是删除缓存声明(这可能证明是乏味的),您可以连接一个不执行缓存的简单虚拟缓存 - 也就是说,它强制每隔 time 执行缓存的方法。以下 example 显示了如何执行此操作:
<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
连接多个CacheManager
实例,并通过fallbackToNoOpCache
flag 为所有未由配置的缓存 managers 处理的定义添加 no-op 缓存。也就是说,jdkCache
或gemfireCache
(在 example 中先前配置)中找不到的每个缓存定义都由 no-op 缓存处理,该缓存不存储任何信息,导致每隔 time 执行一次目标方法。
8.6. Plugging-in 不同的 Back-end 缓存
显然,有很多缓存产品可以用作支持 store。要插入它们,您需要提供CacheManager
和Cache
implementation,因为遗憾的是,我们无法使用可用的标准。这可能听起来比实际上更难,因为在实践中,class 往往是简单的适配器,它将缓存抽象 framework 映射到存储 API 之上,就像ehcache
classes 那样。大多数CacheManager
classes 可以使用org.springframework.cache.support
包中的 classes(例如AbstractCacheManager
,它负责 boiler-plate code,只留下实际的映射完成)。我们希望,在 time 中,提供与 Spring 的整合的 libraries 可以填补这个小的 configuration 差距。
8.7. 如何设置 TTL/TTI/Eviction policy/XXX feature?
直接通过缓存提供程序。缓存抽象是抽象,而不是缓存实现。您使用的解决方案可能支持各种数据 policies 和其他解决方案不支持的不同拓扑(例如,JDK ConcurrentHashMap
- 暴露在缓存抽象中将是无用的,因为没有后备支持)。应该通过后备缓存(配置时)或通过其本机 API 直接控制此类功能。