声明式基于注解的缓存
对于缓存声明,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
支持)进行检索:
@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
表达式都针对一个专用的上下文
进行评估。除了内置参数外,框架还提供了专用的与缓存相关的元数据,例如参数名称。以下表格描述了上下文中提供的项目,以便您可以将它们用于键和条件计算:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
|
根对象 |
正在调用的方法的名称 |
|
|
根对象 |
正在调用的方法 |
|
|
根对象 |
正在调用的目标对象 |
|
|
根对象 |
正在调用的目标的类 |
|
|
根对象 |
用于调用目标的参数(作为数组) |
|
|
根对象 |
当前方法运行的缓存集合 |
|
参数名称 |
评估上下文 |
任何方法参数的名称。如果名称不可用(可能是由于没有调试信息),参数名称也可以在 |
|
|
评估上下文 |
方法调用的结果(要缓存的值)。仅在 |
|
@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)
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 模式,结合编译时或加载时织入。 |
有关使用Java配置实现CachingConfigurer 所需的高级自定义详细信息,请参阅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
)的情况下尤其有用。与其余的构造型注解类似,您可以使用 @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注解,但容器会在运行时自动获取其声明并理解其含义。请注意,如前面所述,需要启用基于注解的行为。