基于声明性注释的缓存
对于缓存声明,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)
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
,类似于替换 键生成 的方式。为每个缓存操作请求解析,让实现实际上根据运行时参数解析要使用的缓存。以下示例展示了如何指定 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 和反应式返回类型进行缓存
从 6.1 开始,缓存注释会考虑 CompletableFuture
和反应式返回类型,并自动相应地调整缓存交互。
对于返回 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
和反应式调整也适用于同步缓存,在发生并发缓存未命中时仅计算一次值
@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;
}
最后但并非最不重要的一点是,请注意,基于注释的缓存不适用于涉及组合和反压的复杂反应式交互。如果你选择在特定反应式方法上声明 @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
值存在,它将存储在关联的缓存中。如果 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) (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") (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
类中
@Configuration
@EnableCaching
public class AppConfig {
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification(...);
return cacheManager;
}
}
或者,对于 XML 配置,可以使用 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 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 属性 | 注释属性 | 默认值 | 说明 |
---|---|---|---|
|
N/A(请参阅 |
|
要使用的缓存管理器的名称。使用此缓存管理器(或未设置时使用 |
|
N/A(请参阅 |
使用已配置 |
用于解析后备缓存的 CacheResolver 的 bean 名称。此属性不是必需的,并且仅需要作为“cache-manager”属性的替代项来指定。 |
|
N/A(请参阅 |
|
要使用的自定义键生成器的名称。 |
|
N/A(请参阅 |
|
要使用的自定义缓存错误处理器的名称。默认情况下,在与缓存相关的操作期间抛出的任何异常都会回传给客户端。 |
|
|
|
默认模式( |
|
|
|
仅适用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义应用于使用 |
<cache:annotation-driven/> 仅在定义它的同一应用程序上下文中查找 @Cacheable/@CachePut/@CacheEvict/@Caching 。这意味着,如果你将 <cache:annotation-driven/> 放入 DispatcherServlet 的 WebApplicationContext 中,它仅在控制器中检查 bean,而不是服务。有关更多信息,请参阅 MVC 部分。
|
Spring 建议你仅使用 @Cache* 注释对具体类(以及具体类的方法)进行注释,而不是对接口进行注释。你当然可以在接口(或接口方法)上放置 @Cache* 注释,但这仅在你使用代理模式(mode="proxy" )时才有效。如果你使用基于织入的方面(mode="aspectj" ),则织入基础架构不会识别接口级别声明中的缓存设置。
|
在代理模式(默认模式)下,只有通过代理传入的外部方法调用会被拦截。这意味着,即使被调用的方法已用 @Cacheable 标记,自调用(实际上,目标对象中的一个方法调用目标对象的另一个方法)也不会导致在运行时进行实际缓存。在这种情况下,请考虑使用 aspectj 模式。此外,代理必须完全初始化才能提供预期的行为,因此你不应在初始化代码(即 @PostConstruct )中依赖此功能。
|
使用自定义注释
缓存抽象允许你使用自己的注释来标识触发缓存填充或驱逐的方法。这作为一种模板机制非常方便,因为它消除了重复缓存注释声明的需要,如果指定了键或条件,或者你的代码库中不允许外部导入(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 注释,容器也会在运行时自动获取其声明并理解其含义。请注意,如 前面所述,需要启用注释驱动的行为。