如其名称所示,你可以使用 @Cacheable 来标记可缓存的方法——即方法的结果会存储在缓存中,以便后续(使用相同参数的)调用时,无需实际调用方法即可返回缓存中的值。最简单的形式是,注解声明需要指定与注解方法关联的缓存名称,如下例所示
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在前面的代码片段中,findBook 方法与名为 books 的缓存相关联。每次调用该方法时,都会检查缓存,以查看该调用是否已经执行过,从而无需重复。虽然大多数情况下只声明一个缓存,但该注解允许指定多个名称,以便使用多个缓存。在这种情况下,在调用方法之前会检查每个缓存——如果至少有一个缓存命中,则返回相关联的值。
所有其他不包含该值的缓存也会被更新,即使实际并未调用被缓存的方法。
以下示例在 findBook 方法上使用带有多个缓存的 @Cacheable
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
默认 Key 生成
由于缓存本质上是键值存储,每个被缓存方法的调用都需要转换为适合缓存访问的键。缓存抽象使用一个基于以下算法的简单 KeyGenerator
如果未提供参数,返回 SimpleKey.EMPTY。
如果只提供一个参数,返回该实例。
如果提供多个参数,返回包含所有参数的 SimpleKey。
只要参数具有自然键并实现了有效的 hashCode() 和 equals() 方法,这种方法适用于大多数用例。如果不是这种情况,你需要改变策略。
要提供不同的默认 key 生成器,你需要实现 org.springframework.cache.interceptor.KeyGenerator 接口。
默认 key 生成策略随着 Spring 4.0 的发布而改变。Spring 的早期版本使用一种 key 生成策略,对于多个 key 参数,它只考虑参数的 hashCode() 而不考虑 equals()。这可能导致意外的 key 冲突(有关背景信息,请参阅 spring-framework#14870)。新的 SimpleKeyGenerator 在这种场景下使用复合 key。
如果你想继续使用以前的 key 策略,可以配置已弃用的 org.springframework.cache.interceptor.DefaultKeyGenerator 类,或创建自定义的基于 hash 的 KeyGenerator 实现。
自定义 Key 生成声明
由于缓存是通用的,目标方法很可能具有各种签名,这些签名无法轻易地映射到缓存结构上。当目标方法有多个参数,其中只有部分适合缓存(而其余的仅由方法逻辑使用)时,这一点往往变得很明显。考虑以下示例
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然这两个 boolean 参数影响查找书籍的方式,但它们对缓存没有任何用处。此外,如果只有其中一个重要而另一个不重要怎么办?
对于这种情况,@Cacheable 注解允许你通过其 key 属性指定如何生成 key。你可以使用 SpEL 来选取感兴趣的参数(或其嵌套属性),执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。与 默认生成器 相比,这是推荐的方法,因为随着代码库的增长,方法的签名往往差异很大。虽然默认策略可能适用于某些方法,但很少适用于所有方法。
以下示例使用各种 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 的算法过于特定,或者需要共享,可以在操作上定义一个自定义的 keyGenerator。为此,指定要使用的 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") (1)
public Book findBook(ISBN isbn) {...}
1
指定 anotherCacheManager。
你也可以完全替换 CacheResolver,方式类似于替换 key 生成。每次缓存操作都会请求解析,允许实现在运行时根据参数实际解析要使用的缓存。以下示例展示了如何指定 CacheResolver
@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
1
指定 CacheResolver。
从 Spring 4.1 开始,缓存注解的 value 属性不再是强制性的,因为这些特定信息可以由 CacheResolver 提供,而无需依赖注解的内容。
与 key 和 keyGenerator 类似,cacheManager 和 cacheResolver 参数是互斥的,同时指定两者会导致异常,因为自定义的 CacheManager 会被 CacheResolver 实现忽略。这可能不是你期望的结果。
同步缓存
在多线程环境中,某些操作可能针对相同参数(通常在启动时)被并发调用。默认情况下,缓存抽象不会锁定任何东西,并且相同的值可能会被计算多次,这违背了缓存的目的。
对于这些特定情况,你可以使用 sync 属性来指示底层缓存提供程序在计算值时锁定缓存条目。这样,只有一个线程忙于计算值,而其他线程则被阻塞,直到缓存中的条目被更新。以下示例展示了如何使用 sync 属性
@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
1
使用 sync 属性。
这是一项可选功能,你喜欢的缓存库可能不支持它。核心框架提供的所有 CacheManager 实现都支持它。有关更多详细信息,请查阅你的缓存提供程序的文档。
使用 CompletableFuture 和 Reactive 返回类型进行缓存
从 6.1 版本开始,缓存注解考虑了 CompletableFuture 和 reactive 返回类型,会自动相应地调整缓存交互。
对于返回 CompletableFuture 的方法,该 future 生成的对象将在完成后被缓存,并且缓存命中时的缓存查找将通过 CompletableFuture 检索
@Cacheable("books")
public CompletableFuture
对于返回 Reactor Mono 的方法,该 Reactive Streams 发布者发出的对象将在可用时被缓存,并且缓存命中时的缓存查找将以 Mono 的形式检索(由 CompletableFuture 提供支持)
@Cacheable("books")
public Mono
对于返回 Reactor Flux 的方法,该 Reactive Streams 发布者发出的对象将被收集到一个 List 中,并在该列表完成后被缓存,并且缓存命中时的缓存查找将以 Flux 的形式检索(由 CompletableFuture 提供支持,用于缓存的 List 值)
@Cacheable("books")
public Flux
这种 CompletableFuture 和 reactive 适配也适用于同步缓存,在并发缓存未命中的情况下,只计算一次值
@Cacheable(cacheNames="foos", sync=true) (1)
public CompletableFuture
1
使用 sync 属性。
为了使这种安排在运行时工作,配置的缓存需要能够进行基于 CompletableFuture 的检索。Spring 提供的 ConcurrentMapCacheManager 会自动适应这种检索方式,而 CaffeineCacheManager 在启用其异步缓存模式时本地支持它:在你的 CaffeineCacheManager 实例上设置 setAsyncCacheMode(true)。
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification(...);
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
最后但同样重要的是,请注意,注解驱动的缓存不适合涉及组合和背压的复杂 reactive 交互。如果你选择在特定的 reactive 方法上声明 @Cacheable,请考虑其相对粗粒度的缓存交互的影响,它仅存储 Mono 发出的对象,或者甚至存储 Flux 预先收集的对象列表。
条件缓存
有时,方法可能不适合始终缓存(例如,它可能取决于给定的参数)。缓存注解通过 condition 参数支持此类用例,该参数接受一个 SpEL 表达式,该表达式求值为 true 或 false。如果为 true,则方法被缓存。如果不是,则其行为如同方法未被缓存(也就是说,无论缓存中有哪些值或使用哪些参数,都会每次调用该方法)。例如,以下方法仅在参数 name 的长度小于 32 时才会被缓存
@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
1
在 @Cacheable 上设置条件。
除了 condition 参数,你还可以使用 unless 参数来阻止将值添加到缓存中。与 condition 不同,unless 表达式在方法调用后才被求值。以上一个示例为基础,也许我们只想缓存平装书,如下例所示
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
1
使用 unless 属性阻止精装书。
缓存抽象支持 java.util.Optional 返回类型。如果 Optional 值是 present(存在),它将被存储在相关联的缓存中。如果 Optional 值不存在,则 null 将被存储在相关联的缓存中。#result 始终指向业务实体,而不是支持的包装器,因此前面的示例可以重写如下
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional
注意,#result 仍然指向 Book 而不是 Optional
可用的缓存 SpEL 求值上下文
每个 SpEL 表达式都会针对一个专用的 上下文 进行求值。除了内置参数外,框架还提供了专用的缓存相关元数据,例如参数名称。下表描述了上下文中可用的项,以便你可以将它们用于 key 和条件计算
表 1. SpEL 表达式中可用的缓存元数据
名称
位置
描述
示例
methodName
Root 对象
正在调用的方法名称
#root.methodName
method
Root 对象
正在调用的方法
#root.method.name
target
Root 对象
正在调用的目标对象
#root.target
targetClass
Root 对象
正在调用的目标类
#root.targetClass
args
Root 对象
用于调用目标的参数(作为对象数组)
#root.args[0]
caches
Root 对象
执行当前方法的缓存集合
#root.caches[0].name
参数名
评估上下文
特定方法参数的名称。如果名称不可用(例如,因为代码编译时没有使用 -parameters 标志),也可以使用 #a<#arg> 语法访问单个参数,其中 <#arg> 代表参数索引(从 0 开始)。
#iban 或 #a0(您也可以使用 #p0 或 #p<#arg> 符号作为别名)。
结果
评估上下文
方法调用的结果(要缓存的值)。仅在 unless 表达式、cache put 表达式(用于计算 key)或 cache evict 表达式(当 beforeInvocation 为 false 时)中可用。对于支持的包装器(例如 Optional),#result 指的是实际对象,而不是包装器。
#result