基于注解的声明式缓存
对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解
-
@Cacheable
: 触发缓存填充。 -
@CacheEvict
: 触发缓存逐出。 -
@CachePut
: 更新缓存,不影响方法执行。 -
@Caching
: 将多个缓存操作组合应用于一个方法。 -
@CacheConfig
: 在类级别共享一些常见的缓存相关设置。
@Cacheable
注解
如其名称所示,你可以使用 @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 参数,它只考虑参数的 如果你想继续使用以前的 key 策略,可以配置已弃用的 |
自定义 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 开始,缓存注解的 与 |
同步缓存
在多线程环境中,某些操作可能针对相同参数(通常在启动时)被并发调用。默认情况下,缓存抽象不会锁定任何东西,并且相同的值可能会被计算多次,这违背了缓存的目的。
对于这些特定情况,你可以使用 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<Book> findBook(ISBN isbn) {...}
对于返回 Reactor Mono
的方法,该 Reactive Streams 发布者发出的对象将在可用时被缓存,并且缓存命中时的缓存查找将以 Mono
的形式检索(由 CompletableFuture
提供支持)
@Cacheable("books")
public Mono<Book> findBook(ISBN isbn) {...}
对于返回 Reactor Flux
的方法,该 Reactive Streams 发布者发出的对象将被收集到一个 List
中,并在该列表完成后被缓存,并且缓存命中时的缓存查找将以 Flux
的形式检索(由 CompletableFuture
提供支持,用于缓存的 List
值)
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
这种 CompletableFuture
和 reactive 适配也适用于同步缓存,在并发缓存未命中的情况下,只计算一次值
@Cacheable(cacheNames="foos", sync=true) (1)
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
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<Book> findBook(String name)
注意,#result
仍然指向 Book
而不是 Optional<Book>
。由于它可能为 null
,我们使用 SpEL 的 安全导航运算符。
可用的缓存 SpEL 求值上下文
每个 SpEL
表达式都会针对一个专用的 上下文
进行求值。除了内置参数外,框架还提供了专用的缓存相关元数据,例如参数名称。下表描述了上下文中可用的项,以便你可以将它们用于 key 和条件计算
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
|
Root 对象 |
正在调用的方法名称 |
|
|
Root 对象 |
正在调用的方法 |
|
|
Root 对象 |
正在调用的目标对象 |
|
|
Root 对象 |
正在调用的目标类 |
|
|
Root 对象 |
用于调用目标的参数(作为对象数组) |
|
|
Root 对象 |
执行当前方法的缓存集合 |
|
参数名 |
评估上下文 |
特定方法参数的名称。如果名称不可用(例如,因为代码编译时没有使用 |
|
|
评估上下文 |
方法调用的结果(要缓存的值)。仅在 |
|
@CachePut
注解
当需要在不干扰方法执行的情况下更新缓存时,可以使用 @CachePut
注解。也就是说,方法总是被调用,其结果会被放入缓存中(根据 @CachePut
的选项)。它支持与 @Cacheable
相同的选项,应该用于缓存填充而不是方法流程优化。下面的示例使用了 @CachePut
注解
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
通常强烈不鼓励在同一个方法上同时使用 @CachePut 和 @Cacheable 注解,因为它们的行为不同。后者通过使用缓存来跳过方法调用,而前者强制执行调用以运行缓存更新。这会导致意料之外的行为,并且除了特殊的边缘情况(例如注解具有相互排除的条件)外,应避免此类声明。另请注意,此类条件不应依赖结果对象(即 #result 变量),因为这些变量是预先验证以确认排除的。 |
从 6.1 版本开始,@CachePut
会考虑 CompletableFuture
和响应式返回类型,只要生成对象可用,就会执行 put 操作。
@CacheEvict
注解
缓存抽象不仅允许填充缓存存储,还允许剔除。此过程对于从缓存中删除陈旧或未使用的数据很有用。与 @Cacheable
不同,@CacheEvict
标记执行缓存剔除的方法(即,充当从缓存中删除数据触发器的方法)。类似于其兄弟注解,@CacheEvict
需要指定受操作影响的一个或多个缓存,允许指定自定义缓存和 key 解析或条件,并具有一个额外参数(allEntries
),该参数指示是需要执行整个缓存范围的剔除,而不仅仅是条目剔除(基于 key)。以下示例清空 books
缓存中的所有条目
@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
1 | 使用 allEntries 属性清空缓存中的所有条目。 |
当需要清空整个缓存区域时,此选项非常方便。如前例所示,所有条目在一个操作中被移除,而不是逐个剔除条目(这会花费很长时间,因为它效率低下)。请注意,在此场景中,框架会忽略指定的任何 key,因为它不适用(清空的是整个缓存,而不仅仅是一个条目)。
您还可以使用 beforeInvocation
属性指示剔除应该在方法调用之后(默认)还是之前发生。前者提供与其余注解相同的语义:一旦方法成功完成,就会在缓存上运行一个操作(在本例中为剔除)。如果方法没有运行(因为它可能已被缓存)或抛出异常,则不会发生剔除。后者(beforeInvocation=true
)会使剔除总是在方法调用之前发生。这在剔除不需要与方法结果关联的情况下非常有用。
请注意,void
方法可以与 @CacheEvict
一起使用 - 由于这些方法充当触发器,返回值会被忽略(因为它们不与缓存交互)。@Cacheable
的情况并非如此,它会向缓存添加数据或更新缓存中的数据,因此需要一个结果。
从 6.1 版本开始,@CacheEvict
会考虑 CompletableFuture
和响应式返回类型,只要处理完成,就会执行调用后剔除操作。
@Caching
注解
有时,需要指定同一类型的多个注解(例如 @CacheEvict
或 @CachePut
)— 例如,因为不同缓存之间的条件或 key 表达式不同。@Caching
允许在同一个方法上使用多个嵌套的 @Cacheable
、@CachePut
和 @CacheEvict
注解。以下示例使用了两个 @CacheEvict
注解
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
@CacheConfig
注解
到目前为止,我们已经看到缓存操作提供了许多定制选项,并且您可以为每个操作设置这些选项。然而,如果某些定制选项适用于类的所有操作,则配置起来可能会很繁琐。例如,为类的每个缓存操作指定要使用的缓存名称,可以用单个类级别定义代替。这就是 @CacheConfig
发挥作用的地方。以下示例使用 @CacheConfig
设置缓存名称
@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
1 | 使用 @CacheConfig 设置缓存名称。 |
@CacheConfig
是一个类级别注解,允许共享缓存名称、自定义 KeyGenerator
、自定义 CacheManager
和自定义 CacheResolver
。将此注解放在类上不会开启任何缓存操作。
操作级别的定制总是会覆盖在 @CacheConfig
上设置的定制。因此,这为每个缓存操作提供了三个级别的定制
-
全局配置,例如通过
CachingConfigurer
:参见下一节。 -
在类级别,使用
@CacheConfig
。 -
在操作级别。
提供者特定的设置通常在 CacheManager bean 上可用,例如在 CaffeineCacheManager 上。这些实际上也是全局的。 |
启用缓存注解
需要注意的是,即使声明了缓存注解,也不会自动触发其动作 - 就像 Spring 中的许多功能一样,该特性必须通过声明方式启用(这意味着如果您怀疑是缓存导致的问题,可以通过仅移除一行配置来禁用它,而不是移除代码中的所有注解)。
要启用缓存注解,请将 @EnableCaching
注解添加到您的一个 @Configuration
类中,或者在 XML 中使用 cache:annotation-driven
元素
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification("...");
return cacheManager;
}
}
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
fun cacheManager(): CacheManager {
return CaffeineCacheManager().apply {
setCacheSpecification("...")
}
}
}
<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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven/>
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="cacheSpecification" value="..."/>
</bean>
</beans>
cache:annotation-driven
元素和 @EnableCaching
注解都允许您指定各种选项,这些选项会影响通过 AOP 将缓存行为添加到应用程序的方式。该配置故意与 @Transactional
的配置相似。
处理缓存注解的默认通知模式是 proxy ,它只允许通过代理拦截调用。同一类中的本地调用无法通过这种方式被拦截。对于更高级的拦截模式,请考虑切换到 aspectj 模式并结合编译时或加载时织入。 |
有关实现 CachingConfigurer 所需的高级定制(使用 Java 配置)的更多详细信息,请参阅 javadoc。 |
XML 属性 | 注解属性 | 默认值 | 描述 |
---|---|---|---|
|
不适用(参见 |
|
要使用的缓存管理器的名称。默认的 |
|
不适用(参见 |
一个使用配置的 |
用于解析后备缓存的 CacheResolver 的 bean 名称。此属性不是必需的,只需作为 'cache-manager' 属性的替代方案来指定即可。 |
|
不适用(参见 |
|
要使用的自定义 key 生成器的名称。 |
|
不适用(参见 |
|
要使用的自定义缓存错误处理器的名称。默认情况下,在缓存相关操作期间抛出的任何异常都会抛回给客户端。 |
|
|
|
默认模式 ( |
|
|
|
仅适用于代理模式。控制为用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义应用于使用 |
<cache:annotation-driven/> 只在其定义所在的同一应用上下文中的 bean 上查找 @Cacheable/@CachePut/@CacheEvict/@Caching 。这意味着,如果您将 <cache:annotation-driven/> 放在 DispatcherServlet 的 WebApplicationContext 中,它只检查您的控制器中的 bean,而不是服务中的 bean。有关更多信息,请参阅 the MVC section。 |
Spring 建议您仅使用 @Cache* 注解来注解具体类(以及具体类的方法),而不是注解接口。您当然可以在接口(或接口方法)上放置 @Cache* 注解,但这仅在使用代理模式(mode="proxy" )时有效。如果您使用基于织入的切面(mode="aspectj" ),织入基础设施将无法识别接口级别声明上的缓存设置。 |
在代理模式(默认)下,仅拦截通过代理进入的外部方法调用。这意味着自我调用(实际上是目标对象内部的方法调用目标对象的另一个方法)在运行时不会导致实际缓存,即使被调用的方法标有 @Cacheable 。在这种情况下,考虑使用 aspectj 模式。此外,代理必须完全初始化才能提供预期的行为,因此您不应在初始化代码(即 @PostConstruct )中依赖此功能。 |
使用自定义注解
缓存抽象允许您使用自己的注解来标识哪些方法触发缓存填充或剔除。这作为一种模板机制非常方便,因为它消除了重复缓存注解声明的需要,特别是在指定了 key 或条件时,或者在代码库中不允许使用外部导入(org.springframework
)时。类似于其余的 stereotype 注解,您可以使用 @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 注解,容器也会在运行时自动识别其声明并理解其含义。请注意,如前面所述,注解驱动的行为需要被启用。