Declarative Annotation-based Caching
对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:
-
@Cacheable
: Triggers cache population. -
@CacheEvict
: Triggers cache eviction. -
@CachePut
:在不干扰方法执行的情况下更新缓存。 -
@Caching
:重新分组多个缓存操作以应用于一个方法。 -
@CacheConfig
:在类级别共享一些常见的缓存相关设置。
The @Cacheable
Annotation
顾名思义,你可以使用 @Cacheable
来界定可缓存的方法——即结果存储在缓存中的方法,这样在后续调用(使用相同的参数)时,会返回缓存中的值,而无需实际调用该方法。在最简单的形式中,该注解声明需要关联到带注解方法的缓存名称,如下面的示例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在前面的代码段中,findBook
方法与名为 books
的缓存关联。每当调用该方法时,都会检查缓存,以查看调用是否已运行且无需重复运行。尽管大多数情况下只声明了一个缓存,但注解允许指定多个名称,以便使用多个缓存。在这种情况下,在调用方法之前会检查每个缓存——如果至少命中一个缓存,则返回关联的值。
即使实际上未调用缓存方法,也不包含该值的所有其他缓存也会更新。 |
以下示例在 findBook
方法中对多个缓存使用 @Cacheable
:
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
Default Key Generation
由于缓存本质上是键值存储,因此需要将缓存方法的每次调用转换为适合缓存访问的键。缓存抽象使用基于以下算法的简单 KeyGenerator
:
-
如果未给出任何参数,则返回
SimpleKey.EMPTY
。 -
如果仅给出一个参数,则返回该实例。
-
如果给出了多个参数,则返回包含所有参数的
SimpleKey
。
只要参数有自然键并实现有效的 hashCode()
和 equals()
方法,这种方法对大多数用例都适用。如果不是这种情况,则需要更改该策略。
要提供一个不同的默认键生成器,你需要实现 org.springframework.cache.interceptor.KeyGenerator
接口。
默认密钥生成策略已随着 Spring 4.0 的发布而更改。Spring 的早期版本使用了一种密钥生成策略,对于多个键参数,只考虑参数的 |
Custom Key Generation Declaration
由于缓存是泛型的,因此目标方法很可能会具有无法直接映射到缓存结构的各种签名。当目标方法有多个参数,其中只有部分参数适合缓存(而其余参数仅由方法逻辑使用)时,这种趋势变得明显。考虑以下示例:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,尽管两个 boolean
参数会影响找到书的方式,但它们对缓存没有用。此外,如果其中一个很重要而另一个不重要会怎样?
对于此类情况,@Cacheable
注释允许您指定通过其 key
属性生成键的方式。您可以使用 SpEL 挑选感兴趣的参数(或它们的嵌套属性),执行操作,或甚至调用任意方法,而无需编写任何代码或实现任何接口。这是相对于 default generator 的推荐方法,因为随着代码库的增大,方法在签名方面往往会有很大差异。虽然默认策略可能适用于某些方法,但它很少适用于所有方法。
以下示例使用各种 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)
前面的代码段展示了选择某个参数、它的某个属性,甚至某个任意(静态)方法是多么容易。
如果负责生成键的算法过于具体或需要共享,则可以在操作中定义自定义 keyGenerator
。为此,请指定要使用的 KeyGenerator
bean 实现的名称,如下面的示例所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
|
Default Cache Resolution
缓存抽象使用了一个简单的 CacheResolver
,它使用已配置的 CacheManager
来检索在操作级别定义的缓存。
要提供一个不同的默认缓存解析器,你需要实现 org.springframework.cache.interceptor.CacheResolver
接口。
Custom Cache Resolution
对于使用单个 CacheManager
且没有复杂缓存解析要求的应用程序,默认缓存解析非常适合。
对于使用多个缓存管理器的应用程序,你可以为每个操作设置要使用的 cacheManager
,如下面的示例所示:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") 1
public Book findBook(ISBN isbn) {...}
1 | Specifying anotherCacheManager . |
您还可以以类似于替换 key generation 的方式完全替换 CacheResolver
。每次缓存操作都会请求解析,允许实现实际解析要使用的缓存,根据运行时参数进行解析。以下示例显示了如何指定 CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver") 1
public Book findBook(ISBN isbn) {...}
1 | Specifying the CacheResolver . |
从 Spring 4.1 开始,缓存注释的 |
Synchronized Caching
在多线程环境中,某些操作可能为相同的参数并发调用(通常在启动时)。默认情况下,缓存抽象不锁定任何东西,相同的值可以计算多次,从而违背缓存的目的。
对于这些特殊情况,你可以使用 sync
属性来指示底层缓存提供者在计算值时锁定缓存条目。结果,只有一个线程在忙于计算值,而其他线程则被阻塞,直到缓存中的条目被更新。以下示例展示了如何使用 sync
属性:
@Cacheable(cacheNames="foos", sync=true) 1
public Foo executeExpensiveOperation(String id) {...}
1 | Using the sync attribute. |
这是一个可选功能,并且您最喜爱的缓存库可能不支持它。核心框架提供的 |
Caching with CompletableFuture and Reactive Return Types
从 6.1 开始,缓存注释考虑 CompletableFuture
和反应式返回类型,从而自动相应调整缓存交互。
对于返回 CompletableFuture
的方法,当将来产生对象时,它将被缓存,并且缓存查找缓存命中将通过 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
(由缓存的 List
值的 CompletableFuture
支持)检索:
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
这样的 CompletableFuture
和响应式适配也适用于同步缓存,在并发缓存缺失的情况下只计算一次值:
@Cacheable(cacheNames="foos", sync=true) 1
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
1 | Using the sync attribute. |
为了让这样的一个安排在运行时起作用,配置的缓存需要能以 |
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification(...);
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
最后但并非最不重要的是,请注意,基于注释的缓存不适合复杂的反应式交互,涉及组合和反压。如果您选择在特定的反应式方法上声明 @Cacheable
,请考虑相当粗粒度的缓存交互的影响,它只是存储发出的对象以获取 Mono
,甚至是针对 Flux
预先收集的对象列表。
Conditional Caching
有时,一个方法可能不适合一直被缓存(比如它可能依赖于给定参数)。缓存注释通过 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
值,它将被存储在关联的缓存中。如果不存在 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 的 safe navigation operator。
Available Caching SpEL Evaluation Context
每个 SpEL
表达式均针对专门的 context
进行评估。除了内置参数外,框架还提供专门的与缓存相关的元数据,例如参数名称。下表描述了可用的项目,以便你可以将它们用于键和条件计算:
Name | Location | Description | Example |
---|---|---|---|
|
Root object |
正在调用的方法的名称 |
|
|
Root object |
The method being invoked |
|
|
Root object |
正在调用的目标对象 |
|
|
Root object |
正在调用的目标的类 |
|
|
Root object |
用于调用目标的参数(作为数组) |
|
|
Root object |
当前方法针对其运行的缓存集合 |
|
Argument name |
Evaluation context |
任何方法参数的名称。如果名称不可用(可能是因为没有调试信息),则参数名称也可以在 |
|
|
Evaluation context |
方法调用的结果(要缓存的值)。仅可在 |
|
The @CachePut
Annotation
当需要更新缓存而不对方法执行产生干扰时,你可以使用 @CachePut
注释。也就是说,该方法总是会被调用,并且它的结果会被放入缓存(根据 @CachePut
选项)。它支持与 @Cacheable
相同的选项,并且应该用于缓存填充,而不是方法流程优化。以下示例使用了 @CachePut
注释:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
总体上强烈不建议对同一个方法使用 @CachePut
和 @Cacheable
注释,因为它们具有不同的行为。而后者通过使用缓存来跳过方法调用,前者强制进行调用以运行缓存更新。这会导致意外的行为,并且,除了特定特例(如注释具有互相排除的条件),这样的声明应该避免。另请注意,这样的条件不应该依赖于结果对象(即,#result
变量),因为这些条件在最开始就已通过验证来确认排除。
从 6.1 开始,@CachePut
会考虑 CompletableFuture
和反应式返回类型,当产生的对象可用时执行 put 操作。
The @CacheEvict
Annotation
缓存抽象不仅允许填充缓存存储,还允许驱逐。此过程可用于从缓存中删除陈旧或未使用的数据。与 @Cacheable
相反,@CacheEvict
界定执行缓存驱逐的方法(即触发从缓存中删除数据的动作的方法)。与它类似,@CacheEvict
要求指定一个或多个受操作影响的缓存,允许指定一个自定义缓存和键解析器或条件,并具有一个额外的参数(allEntries
),该参数指示是否需要执行整个缓存驱逐,而不是仅仅执行基于键的条目驱逐。以下示例从 books
缓存驱逐所有条目:
@CacheEvict(cacheNames="books", allEntries=true) 1
public void loadBooks(InputStream batch)
1 | 使用 allEntries 属性从缓存中驱逐所有条目。 |
当整个缓存区域需要清除时,此选项非常有用。与逐个驱逐条目(由于效率低下,这将花费较长时间)不同,所有条目在一次操作中被移除,正如前面的示例所示。请注意,框架忽略此场景中指定的任何键,因为它不适用(驱逐整个缓存,而不仅仅是一个条目)。
您还可以使用 beforeInvocation
属性指示驱逐是否应在方法被调用后(默认)或之前发生。前者提供与其他注释相同的语义:方法成功完成后,对缓存执行操作(在本例中为驱逐)。如果方法未运行(因为它可能是缓存的)或抛出异常,则不会发生驱逐。后者(beforeInvocation=true
)会导致在调用方法之前始终发生驱逐。这在驱逐不需要与方法输出关联的情况下很有用。
请注意,@CacheEvict
可与 void
方法一起使用——由于方法充当触发器,因此返回值被忽略(因为它们不与缓存交互)。而 @Cacheable
则不同,它向缓存添加数据或更新缓存中的数据,因此需要一个结果。
从 6.1 开始,@CacheEvict
考虑 CompletableFuture
和响应式返回类型,只要处理完成,就会执行驱逐操作。
The @Caching
Annotation
有时,需要指定多个相同类型的注释(例如 @CacheEvict
或 @CachePut)——例如,因为条件或键表达式在不同缓存之间不同。
@Caching` 允许在同一方法上使用多个嵌套的 @Cacheable
、@CachePut
和 @CacheEvict
注释。以下示例使用两个 @CacheEvict
注释:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
The @CacheConfig
Annotation
到目前为止,我们已经看到,缓存操作提供许多自定义选项,您可以为每个操作设置这些选项。但是,如果一些自定义选项适用于类的所有操作,则配置起来可能会很繁琐。例如,可以将为类的每个缓存操作指定要使用的缓存名称替换为单个类级定义。这就是 @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
。 -
At the operation level.
通常在 |
Enabling Caching Annotations
值得注意的是,即使声明缓存注释不会自动触发其操作——就像 Spring 中的许多内容一样,该功能必须通过声明方式启用(这意味着如果您怀疑是缓存的过错,您可以通过删除代码中的所有注释而不是仅仅删除一个配置行来禁用它)。
若要启用缓存注释,请在 @Configuration
类之一中添加注释 @EnableCaching
,或与 XML 一起使用元素 cache:annotation-driven
:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableCaching
public 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
的配置故意相似。
处理缓存注释的默认建议模式为 |
有关实现 |
XML Attribute | Annotation Attribute | Default | Description |
---|---|---|---|
|
无(参见 |
|
要使用的缓存管理器的名称。通过此缓存管理器(如果未设置,则为 |
|
无(参见 |
使用已配置 |
用于解析缓存的 CacheResolver 的 Bean 名称。此属性不是必需的,只需将其作为“cache-manager”属性的替代项进行指定。 |
|
无(参见 |
|
要使用的自定义键生成器的名称。 |
|
无(参见 |
|
要使用自定义缓存错误处理器的名称。默认情况下,在缓存相关操作期间引发的任何异常都会返还给客户端。 |
|
|
|
默认模式 ( |
|
|
|
仅应用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义应用于使用 |
|
当您使用代理时,您应该仅对具有公共可见性的方法应用缓存注释。如果您使用这些注释对受保护的、私有的或包可见的方法进行注释,则不会引发错误,但带注释的方法不会表现出已配置的缓存设置。如果您需要注释非公共方法,请考虑使用 AspectJ(请参阅本节的其余部分),因为它更改了字节码本身。
Spring 建议您仅使用 |
在代理模式(默认模式)中,仅通过代理传入的外部方法调用才被拦截。这意味着,即使被调用方法用 |
Using Custom Annotations
此功能仅适用于基于代理的方法,但通过使用 AspectJ 付出一些额外的努力可以启用它。
spring-aspects
模块仅为标准注释定义了一个方面。如果您已经定义了自己的注释,则还需要为它们定义一个方面。请查看 AnnotationCacheAspect
了解示例。
缓存抽象允许您使用自己的注释来标识触发缓存填充或驱逐的方法。这非常适合用作模板机制,因为它消除了复制缓存注释声明的需要,如果指定了键或条件或在您的代码库中不允许外部导入 (org.springframework
),这尤其有用。与 stereotype 注释的其他部分类似,您可以将 @Cacheable
、@CachePut
、@CacheEvict
和 @CacheConfig
用作 meta-annotations(即,可以注释其他注释的注释)。在以下示例中,我们用自己的自定义注释替换一个常见的 @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 注释,容器也会在运行时自动提取其声明,并理解其含义。请注意,如 earlier 中所述,需要启用基于注释的行为。