|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
基于注解的声明式缓存
对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:
-
@Cacheable:触发缓存填充。 -
@CacheEvict:触发缓存清除。 -
@CachePut:在不干扰方法执行的情况下更新缓存。 -
@Caching:将多个缓存操作组合在一起,应用于一个方法。 -
@CacheConfig:在类级别共享一些通用的缓存相关设置。
这@Cacheable注解
顾名思义,您可以使用 @Cacheable 来标记可缓存的方法——也就是说,这些方法的执行结果会被存储在缓存中,以便在后续调用(使用相同的参数)时,直接从缓存中返回值,而无需实际再次执行该方法。在最简单的形式中,注解声明需要指定与被注解方法关联的缓存名称,如下例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在上述代码片段中,findBook 方法与名为 books 的缓存相关联。
每次调用该方法时,都会检查缓存,以确定该调用是否已经执行过,从而无需重复执行。虽然大多数情况下只声明一个缓存,但该注解允许指定多个名称,以便同时使用多个缓存。在这种情况下,会在调用方法之前逐一检查每个缓存——只要至少有一个缓存命中,就会返回对应的值。
| 所有其他不包含该值的缓存也会被更新,即使缓存的方法实际上并未被调用。 |
以下示例在 @Cacheable 方法上使用了 findBook 注解,并指定了多个缓存:
@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 实现均支持此功能。更多详细信息,请参阅您所使用的缓存提供者的文档。 |
条件缓存
有时,某个方法可能并不总是适合进行缓存(例如,它可能依赖于传入的参数)。缓存注解通过 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 进行求值。
除了内置参数外,该框架还提供了专用的缓存相关元数据,例如参数名称。下表描述了上下文中可用的各项内容,以便您将其用于键和条件计算:
| 姓名 | 位置 | <description> </description> | 例举 |
|---|---|---|---|
|
根对象 |
正在被调用的方法的名称 |
|
|
根对象 |
被调用的方法 |
|
|
根对象 |
被调用的目标对象 |
|
|
根对象 |
被调用目标的类 |
|
|
根对象 |
用于调用目标的参数(以数组形式) |
|
|
根对象 |
当前方法所运行的缓存集合 |
|
参数名称 |
评估上下文 |
任意方法参数的名称。如果这些名称不可用(可能是因为缺少调试信息),参数名称也可以通过 |
|
|
评估上下文 |
方法调用的结果(要被缓存的值)。仅在 |
|
这@CachePut注解
当需要在不干扰方法执行的情况下更新缓存时,可以使用 @CachePut 注解。也就是说,该方法始终会被调用,并将其结果放入缓存中(根据 @CachePut 的配置选项)。它支持与 @Cacheable 相同的选项,但应主要用于填充缓存,而非优化方法执行流程。以下示例使用了 @CachePut 注解:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一方法上同时使用 @CachePut 和 @Cacheable 注解通常是强烈不推荐的,因为它们的行为不同。#result 注解会通过使用缓存来跳过方法的执行,而 3 注解则会强制执行方法以更新缓存。这会导致意外的行为。除非在特定的边缘情况下(例如注解带有互斥条件,使得它们彼此排除),否则应避免此类声明。另外请注意,这些条件不应依赖于结果对象(即 4 变量),因为这些条件会在执行前进行验证,以确认是否应排除某个注解。 |
这@CacheEvict注解
缓存抽象不仅支持向缓存存储中填充数据,还支持清除数据。
这一过程有助于从缓存中移除过时或未使用的数据。与
@Cacheable 不同,@CacheEvict 用于标记执行缓存清除操作的方法
(即那些作为触发器、用于从缓存中移除数据的方法)。
与 @CacheEvict 类似,allEntries 也需要指定一个或多个受该操作影响的缓存,
允许自定义缓存和键的解析方式,或指定一个条件,并且提供了一个额外的参数
(books),用于指示是否需要执行整个缓存的清除操作,
而不仅仅是基于键清除某一条目。以下示例将从 5 缓存中清除所有条目:
@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
| 1 | 使用 allEntries 属性从缓存中清除所有条目。 |
当需要清除整个缓存区域时,此选项非常有用。 与逐个驱逐每个条目(这种方式效率低下且耗时较长)不同, 所有条目会在一次操作中被全部移除,如前面的示例所示。 请注意,在这种情况下,框架会忽略所指定的任何键,因为该键在此场景下不适用 (清除的是整个缓存,而不仅仅是一个条目)。
你还可以通过使用 beforeInvocation 属性来指定驱逐操作是在方法调用之后(默认行为)还是之前发生。前者与其他注解具有相同的语义:一旦方法成功执行完成,就会在缓存上执行一个操作(在本例中为驱逐)。如果方法未实际执行(例如可能已被缓存)或抛出了异常,则不会发生驱逐操作。而后者(beforeInvocation=true)则会导致驱逐操作总是在方法调用之前发生。这在驱逐操作无需与方法执行结果关联的情况下非常有用。
请注意,void 方法可以与 @CacheEvict 一起使用——因为这些方法仅作为触发器,其返回值会被忽略(因为它们不与缓存进行交互)。而 @Cacheable 则不同,它会向缓存中添加数据或更新缓存中的数据,因此需要一个返回结果。
这@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 模式,并结合编译时或加载时织入(weaving)使用。 |
有关实现 CachingConfigurer 所需的高级自定义(使用 Java 配置)的更多详细信息,请参阅
javadoc。 |
| XML 属性 | 注解属性 | 默认 | <description> </description> |
|---|---|---|---|
|
不适用(请参阅 |
|
要使用的缓存管理器的名称。后台会使用此缓存管理器(如果未设置,则使用 |
|
不适用(请参阅 |
一个使用已配置的 |
用于解析底层缓存的 CacheResolver 的 Bean 名称。 此属性不是必需的,仅在需要替代 'cache-manager' 属性时才需指定。 |
|
不适用(请参阅 |
|
要使用的自定义键生成器的名称。 |
|
不适用(请参阅 |
|
要使用的自定义缓存错误处理器的名称。默认情况下,任何在缓存相关操作期间抛出的异常都会直接抛回给客户端。 |
|
|
|
默认模式( |
|
|
|
仅适用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义应用于带有 |
<cache:annotation-driven/> 仅在其所定义的同一应用上下文中的 bean 上查找 @Cacheable/@CachePut/@CacheEvict/@Caching 注解。
这意味着,如果你将 <cache:annotation-driven/> 放在用于 WebApplicationContext 的 DispatcherServlet 中,
它只会检查控制器(controller)中的 bean,而不会检查服务(service)中的 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 的注解,但容器会在运行时自动识别其声明并理解其含义。请注意,如前文所述,需要启用基于注解的驱动行为。