基于声明式注解的缓存
对于缓存声明,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 使用的密钥生成策略对于多个密钥参数,仅考虑参数的 hashCode()
而不考虑 equals()
。这可能导致意外的密钥冲突(有关背景信息,请参见 spring-framework#14870)。新的 SimpleKeyGenerator
在这种情况下使用复合密钥。
如果您希望继续使用以前的密钥策略,可以配置已弃用的 org.springframework.cache.interceptor.DefaultKeyGenerator
类或创建自定义的基于哈希的 KeyGenerator
实现。
自定义键生成声明
由于缓存是通用的,目标方法很可能具有各种签名,这些签名无法直接映射到缓存结构之上。当目标方法有多个参数,而其中只有一些适合用于缓存(而其余的仅用于方法逻辑)时,这一点往往变得明显。请考虑以下示例:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然这两个 boolean
参数会影响书籍的查找方式,但它们对缓存没有用处。此外,如果只有其中一个参数重要而另一个不重要怎么办?
以下示例使用了各种 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) {...}
指定
anotherCacheManager
。
您还可以完全替换 CacheResolver
,方式类似于替换 键生成。每次缓存操作都会请求解析,让实现根据运行时参数实际解析要使用的缓存。以下示例展示了如何指定一个 CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver") 1
public Book findBook(ISBN isbn) {...}
指定
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) {...}
使用
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
检索(由缓存的 List
值的 CompletableFuture
支持):
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
这样的 CompletableFuture
和响应式适配也适用于同步缓存,在并发缓存未命中的情况下仅计算一次值:
@Cacheable(cacheNames="foos", sync=true) 1
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
使用
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)
在
@Cacheable
上设置一个条件。
除了 condition
参数之外,你还可以使用 unless
参数来否决将值添加到缓存中。与 condition
不同,unless
表达式是在方法调用后进行评估的。为了扩展前面的示例,也许我们只想缓存平装书,正如以下示例所示:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") 1
public Book findBook(String name)
使用
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
表达式都会针对一个专用的上下文进行评估。除了内置参数外,框架还提供了专用的缓存相关元数据,例如参数名称。下表描述了可用于上下文的项目,以便您可以将它们用于键和条件计算:
表 1. 在 SpEL 表达式中可用的缓存元数据
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | 根对象 | 被调用方法的名称 | #root.methodName |
method | 根对象 | 被调用的方法 | #root.method.name |
target | 根对象 | 被调用的目标对象 | #root.target |
targetClass | 根对象 | 被调用目标的类 | #root.targetClass |
args | 根对象 | 用于调用目标的参数(作为对象数组) | #root.args[0] |
caches | 根对象 | 当前方法运行时使用的缓存集合 | #root.caches[0].name |
参数名称 | 评估上下文 | 特定方法参数的名称。如果名称不可用(例如,因为代码在没有 -parameters 标志的情况下编译),也可以使用 #a<#arg> 语法访问单个参数,其中 <#arg> 代表参数索引(从 0 开始)。 | #iban 或 #a0 (你也可以使用 #p0 或 #p<#arg> 作为别名)。 |
result | 评估上下文 | 方法调用的结果(要缓存的值)。仅在 unless 表达式、cache put 表达式(用于计算 key )或 cache evict 表达式(当 beforeInvocation 为 false 时)中可用。对于支持的包装器(如 Optional ),#result 指的是实际对象,而不是包装器。 | #result |
@CachePut
注解
当需要在不干扰方法执行的情况下更新缓存时,可以使用 @CachePut
注解。也就是说,该方法总是被调用,并且其结果被放入缓存中(根据 @CachePut
的选项)。它支持与 @Cacheable
相同的选项,应该用于缓存填充而不是方法流程优化。以下示例使用了 @CachePut
注解:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一个方法上同时使用 @CachePut
和 @Cacheable
注解通常是强烈不推荐的,因为它们具有不同的行为。后者通过使用缓存跳过方法调用,而前者则强制调用以进行缓存更新。这会导致意外的行为,除了特定的极端情况(例如注解具有将它们彼此排除的条件)外,应避免这样的声明。还要注意,这样的条件不应依赖于结果对象(即 #result
变量),因为这些条件会在前期验证以确认排除。
从 6.1 版本开始,@CachePut
会考虑 CompletableFuture
和响应式返回类型,在生成的对象可用时执行放入操作。
@CacheEvict
注解
缓存抽象不仅允许填充缓存存储,还允许驱逐缓存。这一过程对于移除缓存中的陈旧或未使用的数据非常有用。与 @Cacheable
相反,@CacheEvict
标记执行缓存驱逐的方法(即,作为从缓存中移除数据的触发器的方法)。与其类似,@CacheEvict
需要指定一个或多个受影响的缓存,允许指定自定义缓存和键解析或条件,并具有一个额外的参数(allEntries
),用于指示是否需要执行整个缓存的驱逐,而不仅仅是基于键的条目驱逐。以下示例从 books
缓存中驱逐所有条目:
@CacheEvict(cacheNames="books", allEntries=true) 1
public void loadBooks(InputStream batch)
使用
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) {...}
}
使用
@CacheConfig
来设置缓存的名称。
@CacheConfig
是一个类级别的注解,允许共享缓存名称、自定义 KeyGenerator
、自定义 CacheManager
和自定义 CacheResolver
。将此注解放在类上并不会开启任何缓存操作。
操作级别的自定义总是会覆盖在 @CacheConfig
上设置的自定义。因此,这为每个缓存操作提供了三个级别的自定义:
-
全局配置,例如,通过
CachingConfigurer
:请参阅下一节。 -
在类级别,使用
@CacheConfig
。 -
在操作级别。
提供者特定的设置通常在 CacheManager
bean 上可用,例如,在 CaffeineCacheManager
上。这些实际上也是全局的。
启用缓存注解
需要注意的是,即使声明了缓存注解,也不会自动触发它们的动作——就像 Spring 中的许多功能一样,该功能必须通过声明方式启用(这意味着如果你怀疑缓存是问题所在,你可以通过删除一行配置来禁用它,而不是删除代码中的所有注解)。
要启用缓存注解,可以在你的一个 @Configuration
类中添加注解 @EnableCaching
,或者在 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。
表 2. 缓存注释设置
XML 属性 | 注解属性 | 默认值 | 描述 |
---|---|---|---|
cache-manager | N/A (参见 CachingConfigurer javadoc) | cacheManager | 要使用的缓存管理器的名称。一个默认的 CacheResolver 会在后台使用此缓存管理器(如果未设置则使用 cacheManager )进行初始化。为了更细粒度地管理缓存解析,可以考虑设置 'cache-resolver' 属性。 |
cache-resolver | N/A (参见 CachingConfigurer javadoc) | 使用配置的 cacheManager 的 SimpleCacheResolver 。 | 用于解析后备缓存的 CacheResolver 的 bean 名称。此属性不是必需的,仅作为 'cache-manager' 属性的替代方案时需要指定。 |
key-generator | N/A (参见 CachingConfigurer javadoc) | SimpleKeyGenerator | 要使用的自定义键生成器的名称。 |
error-handler | N/A (参见 CachingConfigurer javadoc) | SimpleCacheErrorHandler | 要使用的自定义缓存错误处理器的名称。默认情况下,任何在缓存相关操作期间抛出的异常都会被抛回给客户端。 |
mode | mode | proxy | 默认模式 (proxy ) 处理被注解的 bean,以使用 Spring 的 AOP 框架进行代理(遵循代理语义,如前所述,仅适用于通过代理进入的方法调用)。替代模式 (aspectj ) 则使用 Spring 的 AspectJ 缓存方面对受影响的类进行织入,修改目标类字节码以适用于任何类型的方法调用。AspectJ 织入需要在类路径中包含 spring-aspects.jar ,并启用加载时织入(或编译时织入)。(有关如何设置加载时织入的详细信息,请参见 Spring 配置)。 |
proxy-target-class | proxyTargetClass | false | 仅适用于代理模式。控制为带有 @Cacheable 或 @CacheEvict 注解的类创建的缓存代理的类型。如果 proxy-target-class 属性设置为 true ,则创建基于类的代理。如果 proxy-target-class 为 false 或省略此属性,则创建基于标准 JDK 接口的代理。(有关不同代理类型的详细检查,请参见 代理机制)。 |
order | order | Ordered.LOWEST_PRECEDENCE | 定义应用于带有 @Cacheable 或 @CacheEvict 注解的 bean 的缓存通知的顺序。(有关与排序 AOP 通知相关的规则的更多信息,请参见 通知排序)。未指定顺序意味着 AOP 子系统决定通知的顺序。 |
<cache:annotation-driven/>
仅在与其定义在同一应用程序上下文中的 bean 上查找 @Cacheable/@CachePut/@CacheEvict/@Caching
。这意味着,如果你将 <cache:annotation-driven/>
放在 DispatcherServlet
的 WebApplicationContext
中,它只会检查控制器中的 bean,而不会检查服务中的 bean。有关更多信息,请参见 MVC 部分。
Spring 建议您仅使用 @Cache*
注解标注具体类(以及具体类的方法),而不是标注接口。您当然可以在接口(或接口方法)上放置 @Cache*
注解,但这仅在您使用代理模式(mode="proxy"
)时有效。如果您使用基于织入的切面(mode="aspectj"
),织入基础设施无法识别接口级别声明的缓存设置。
在代理模式(默认模式)下,只有通过代理传入的外部方法调用才会被拦截。这意味着自调用(实际上是目标对象内的方法调用目标对象的另一个方法)在运行时不会导致实际缓存,即使被调用的方法标记了 @Cacheable
。在这种情况下,考虑使用 aspectj
模式。此外,代理必须完全初始化才能提供预期的行为,因此不应在初始化代码中(即 @PostConstruct
)依赖此功能。
使用自定义注解
@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 注解,容器也会在运行时自动获取其声明并理解其含义。请注意,如前面所述,需要启用注解驱动的行为。