基于声明式注解的缓存
对于缓存声明,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) {...}
默认键生成
由于缓存本质上是键值存储,因此每次调用缓存方法都需要转换为适合缓存访问的键。缓存抽象使用基于以下算法的简单 KeyGenerator
:
-
如果没有给出参数,则返回
SimpleKey.EMPTY
。 -
如果只给出一个参数,则返回该实例。
-
如果给出多个参数,则返回一个包含所有参数的
SimpleKey
。
只要参数具有自然键并实现有效的 hashCode()
和 equals()
方法,这种方法适用于大多数用例。如果不是这种情况,您需要更改策略。
要提供不同的默认键生成器,您需要实现 org.springframework.cache.interceptor.KeyGenerator
接口。
默认键生成策略随 Spring 4.0 的发布而改变。早期版本的 Spring 使用的键生成策略,对于多个键参数,只考虑参数的 |
自定义键生成声明
由于缓存是通用的,目标方法很可能具有各种签名,这些签名无法轻易映射到缓存结构之上。当目标方法有多个参数,其中只有一些适合缓存(而其余的仅由方法逻辑使用)时,这一点往往变得显而易见。请看以下示例:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然两个 boolean
参数影响了图书的查找方式,但它们对缓存没有用。此外,如果两个参数中只有一个很重要而另一个不重要呢?
对于此类情况,@Cacheable
注解允许您通过其 key
属性指定键的生成方式。您可以使用 SpEL 来选择感兴趣的参数(或其嵌套属性),执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。这是优于 默认生成器的推荐方法,因为随着代码库的增长,方法的签名往往大相径庭。虽然默认策略可能适用于某些方法,但它很少适用于所有方法。
以下示例使用各种 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)
前面的代码片段展示了选择特定参数、其属性之一甚至任意(静态)方法是多么容易。
如果负责生成键的算法过于特定或需要共享,您可以在操作上定义自定义 keyGenerator
。为此,请指定要使用的 KeyGenerator
bean 实现的名称,如下例所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
|
默认缓存解析
缓存抽象使用一个简单的 CacheResolver
,它通过配置的 CacheManager
检索在操作级别定义的缓存。
要提供不同的默认缓存解析器,您需要实现 org.springframework.cache.interceptor.CacheResolver
接口。
自定义缓存解析
默认的缓存解析非常适合使用单个 CacheManager
且没有复杂缓存解析要求的应用程序。
对于使用多个缓存管理器的应用程序,您可以为每个操作设置要使用的 cacheManager
,如下例所示:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") [id="CO1-1"]1
public Book findBook(ISBN isbn) {...}
<1> 指定 `anotherCacheManager`。
您还可以完全替换 CacheResolver
,其方式类似于替换 键生成。每次缓存操作都会请求解析,从而允许实现根据运行时参数实际解析要使用的缓存。以下示例展示了如何指定 CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver") [id="CO2-1"]1
public Book findBook(ISBN isbn) {...}
<1> 指定 `CacheResolver`。
自 Spring 4.1 起,缓存注解的 |
同步缓存
在多线程环境中,某些操作可能会针对相同的参数并发调用(通常在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且相同的值可能会计算多次,从而违背了缓存的目的。
对于这些特殊情况,您可以使用 sync
属性来指示底层缓存提供程序在计算值时锁定缓存条目。因此,只有一个线程忙于计算值,而其他线程则被阻塞,直到缓存中的条目更新。以下示例展示了如何使用 sync
属性:
@Cacheable(cacheNames="foos", sync=true) [id="CO3-1"]1
public Foo executeExpensiveOperation(String id) {...}
<1> 使用 `sync` 属性。
这是一个可选功能,您喜欢的缓存库可能不支持它。核心框架提供的所有 |
使用 CompletableFuture 和响应式返回类型进行缓存
自 6.1 版本起,缓存注解考虑了 CompletableFuture
和响应式返回类型,并相应地自动调整缓存交互。
对于返回 CompletableFuture
的方法,该 future 生成的对象将在完成时被缓存,并且缓存命中的缓存查找将通过 CompletableFuture
检索:
@Cacheable("books")
public CompletableFuture<Book> findBook(ISBN isbn) {...}
对于返回 Reactor Mono
的方法,该响应式流发布者发出的对象将在可用时被缓存,并且缓存命中的缓存查找将作为 Mono
检索(由 CompletableFuture
支持):
@Cacheable("books")
public Mono<Book> findBook(ISBN isbn) {...}
对于返回 Reactor Flux
的方法,该响应式流发布者发出的对象将被收集到一个 List
中并在该列表完成时被缓存,并且缓存命中的缓存查找将作为 Flux
检索(由用于缓存 List
值的 CompletableFuture
支持):
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
这种 CompletableFuture
和响应式适配也适用于同步缓存,在并发缓存未命中时只计算一次值:
@Cacheable(cacheNames="foos", sync=true) [id="CO4-1"]1
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
<1> 使用 `sync` 属性。
为了使这种安排在运行时起作用,配置的缓存需要能够进行基于 |
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification(...);
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
最后但同样重要的是,请注意注解驱动的缓存不适用于涉及组合和背压的复杂响应式交互。如果您选择在特定的响应式方法上声明 @Cacheable
,请考虑相当粗粒度的缓存交互的影响,它只是为 Mono
存储发出的对象,甚至为 Flux
存储预先收集的对象列表。
条件缓存
有时,一个方法可能并非总是适合缓存(例如,它可能取决于给定的参数)。缓存注解通过 condition
参数支持此类用例,该参数接受一个 SpEL
表达式,该表达式求值为 true
或 false
。如果为 true
,则缓存该方法。否则,它的行为就像该方法未缓存一样(即,无论缓存中有什么值或使用什么参数,该方法都会被每次调用)。例如,以下方法仅在参数 name
的长度小于 32 时才会被缓存:
@Cacheable(cacheNames="book", condition="#name.length() < 32") [id="CO5-1"]1
public Book findBook(String name)
<1> 在 `@Cacheable` 上设置条件。
除了 condition
参数之外,您还可以使用 unless
参数来否决将值添加到缓存中。与 condition
不同,unless
表达式在方法调用后进行评估。为了扩展前面的示例,也许我们只想缓存平装书,如下例所示:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") [id="CO6-1"]1
public Book findBook(String name)
<1> 使用 `unless` 属性阻止精装书。
缓存抽象支持 java.util.Optional
返回类型。如果 Optional
值_存在_,它将存储在关联的缓存中。如果 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
表达式都针对一个专用的 context
进行评估。除了内置参数之外,框架还提供了专用的缓存相关元数据,例如参数名称。下表描述了上下文中可用的项,以便您可以将它们用于键和条件计算:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
|
根对象 |
正在调用的方法的名称 |
|
|
根对象 |
正在调用的方法 |
|
|
根对象 |
正在调用的目标对象 |
|
|
根对象 |
正在调用的目标的类 |
|
|
根对象 |
用于调用目标的参数(作为对象数组) |
|
|
根对象 |
当前方法运行所针对的缓存集合 |
|
参数名称 |
评估上下文 |
特定方法参数的名称。如果名称不可用(例如,因为代码在没有 |
|
|
评估上下文 |
方法调用的结果(要缓存的值)。仅在 |
|
@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
需要指定一个或多个受该操作影响的缓存,允许指定自定义缓存和键解析或条件,并具有一个额外参数(allEntries
),该参数指示是否需要执行缓存范围的清除而不是仅清除条目(基于键)。以下示例清除 books
缓存中的所有条目:
@CacheEvict(cacheNames="books", allEntries=true) [id="CO7-1"]1
public void loadBooks(InputStream batch)
<1> 使用 `allEntries` 属性清除缓存中的所有条目。
当需要清除整个缓存区域时,此选项非常方便。如前面的示例所示,不是清除每个条目(这将花费很长时间,因为它效率低下),而是在一个操作中删除所有条目。请注意,在这种情况下,框架会忽略指定的任何键,因为它不适用(清除整个缓存,而不是仅清除一个条目)。
您还可以使用 beforeInvocation
属性指示清除应该在方法调用之后(默认)还是之前发生。前者提供与其余注解相同的语义:一旦方法成功完成,就会对缓存执行操作(在本例中为清除)。如果方法未运行(因为它可能已缓存)或抛出异常,则不会发生清除。后者(beforeInvocation=true
)会导致清除始终在方法调用之前发生。这在清除不需要与方法结果绑定时很有用。
请注意,void
方法可以与 @CacheEvict
一起使用——由于这些方法充当触发器,因此返回值将被忽略(因为它们不与缓存交互)。@Cacheable
的情况并非如此,它将数据添加到缓存或更新缓存中的数据,因此需要一个结果。
自 6.1 版本起,@CacheEvict
考虑了 CompletableFuture
和响应式返回类型,在处理完成后执行调用后清除操作。
@Caching
注解
有时,需要指定多个相同类型的注解(例如 @CacheEvict
或 @CachePut
)——例如,因为不同缓存之间的条件或键表达式不同。@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") [id="CO8-1"]1
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
<1> 使用 `@CacheConfig` 设置缓存名称。
@CacheConfig
是一个类级别注解,允许共享缓存名称、自定义 KeyGenerator
、自定义 CacheManager
和自定义 CacheResolver
。将此注解放置在类上不会启用任何缓存操作。
操作级别自定义总是会覆盖在 @CacheConfig
上设置的自定义。因此,这为每个缓存操作提供了三个级别的自定义:
-
全局配置,例如通过
CachingConfigurer
:请参阅下一节。 -
在类级别,使用
@CacheConfig
。 -
在操作级别。
提供程序特定的设置通常在 |
启用缓存注解
重要的是要注意,即使声明缓存注解也不会自动触发其操作——像 Spring 中的许多事情一样,该功能必须声明性地启用(这意味着如果您怀疑缓存有问题,可以通过仅删除一行配置而不是代码中的所有注解来禁用它)。
要启用缓存注解,请将 @EnableCaching
注解添加到您的一个 @Configuration
类中,或使用 XML 中的 cache:annotation-driven
元素:
cache:annotation-driven
元素和 @EnableCaching
注解都允许您指定各种选项,这些选项会影响通过 AOP 将缓存行为添加到应用程序的方式。该配置与 @Transactional
的配置有意相似。
处理缓存注解的默认建议模式是 |
有关实现 |
XML 属性 | 注解属性 | 默认值 | 描述 |
---|---|---|---|
|
N/A(请参阅 |
|
要使用的缓存管理器的名称。默认的 |
|
N/A(请参阅 |
使用配置的 |
要用于解析后端缓存的 CacheResolver 的 bean 名称。此属性不是必需的,仅在作为 'cache-manager' 属性的替代方案时才需要指定。 |
|
N/A(请参阅 |
|
要使用的自定义键生成器的名称。 |
|
N/A(请参阅 |
|
要使用的自定义缓存错误处理程序的名称。默认情况下,在缓存相关操作期间抛出的任何异常都会抛回给客户端。 |
|
|
|
默认模式( |
|
|
|
仅适用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义应用于使用 |
|
当您使用代理时,应仅将缓存注解应用于具有公共可见性的方法。如果您使用这些注解注解受保护、私有或包可见的方法,则不会引发错误,但被注解的方法不会显示配置的缓存设置。如果您需要注解非公共方法,请考虑使用 AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。
Spring 建议您只使用 |
在代理模式(默认)下,只有通过代理传入的外部方法调用才会被拦截。这意味着自调用(实际上是目标对象内调用目标对象另一个方法的方法)在运行时不会导致实际缓存,即使被调用的方法被标记为 |
使用自定义注解
此功能仅适用于基于代理的方法,但可以通过使用 AspectJ 稍加努力来启用。
spring-aspects
模块仅为标准注解定义了一个切面。如果您已定义自己的注解,则还需要为这些注解定义一个切面。请查看 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 注解,但容器会在运行时自动获取其声明并理解其含义。请注意,如 前所述,需要启用注解驱动的行为。