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 的早期版本使用了一种密钥生成策略,对于多个键参数,只考虑参数的 hashCode(),而不考虑 equals()。这可能会导致意外的密钥冲突(有关背景信息,请参阅https://github.com/spring-projects/spring-framework/issues/14870[spring-framework#14870])。新 SimpleKeyGenerator`在这样的场景中使用复合密钥。 如果你想继续使用以前的键策略,则可以配置已弃用的 `org.springframework.cache.interceptor.DefaultKeyGenerator 类或创建一个自定义基于哈希的 KeyGenerator 实现。

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)

keykeyGenerator 参数互斥,并且指定两者都会导致操作出现异常。

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 开始,缓存注释的 value 属性不再是必需的了,因为这个特殊信息无论注释的内容是什么,都可以由 CacheResolver 提供。 与 keykeyGenerator 类似,cacheManagercacheResolver 参数是相互排斥的,一个操作指定了两个都会导致一个异常,因为一个自定义 CacheManagerCacheResolver 实现忽略了。这可能不是你所期望的。

Synchronized Caching

在多线程环境中,某些操作可能为相同的参数并发调用(通常在启动时)。默认情况下,缓存抽象不锁定任何东西,相同的值可以计算多次,从而违背缓存的目的。

对于这些特殊情况,你可以使用 sync 属性来指示底层缓存提供者在计算值时锁定缓存条目。结果,只有一个线程在忙于计算值,而其他线程则被阻塞,直到缓存中的条目被更新。以下示例展示了如何使用 sync 属性:

@Cacheable(cacheNames="foos", sync=true) 1
public Foo executeExpensiveOperation(String id) {...}
1 Using the sync attribute.

这是一个可选功能,并且您最喜爱的缓存库可能不支持它。核心框架提供的 CacheManager 实现都支持它。更多详细信息,请参见您缓存提供程序的文档。

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.

为了让这样的一个安排在运行时起作用,配置的缓存需要能以 CompletableFuture 为基础进行检索。Spring 提供的 ConcurrentMapCacheManager 自动适应这种检索风格,当其异步缓存模式已启用时,CaffeineCacheManager 本机支持它:在您的 CaffeineCacheManager 实例上设置 setAsyncCacheMode(true)

@Bean
CacheManager cacheManager() {
	CaffeineCacheManager cacheManager = new CaffeineCacheManager();
	cacheManager.setCacheSpecification(...);
	cacheManager.setAsyncCacheMode(true);
	return cacheManager;
}

最后但并非最不重要的是,请注意,基于注释的缓存不适合复杂的反应式交互,涉及组合和反压。如果您选择在特定的反应式方法上声明 @Cacheable,请考虑相当粗粒度的缓存交互的影响,它只是存储发出的对象以获取 Mono,甚至是针对 Flux 预先收集的对象列表。

Conditional Caching

有时,一个方法可能不适合一直被缓存(比如它可能依赖于给定参数)。缓存注释通过 condition 参数支持这样的用例,它使用一个 SpEL 表达式来评估 truefalse。如果为 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 进行评估。除了内置参数外,框架还提供专门的与缓存相关的元数据,例如参数名称。下表描述了可用的项目,以便你可以将它们用于键和条件计算:

Table 1. Cache SpEL available metadata
Name Location Description Example

methodName

Root object

正在调用的方法的名称

#root.methodName

method

Root object

The method being invoked

#root.method.name

target

Root object

正在调用的目标对象

#root.target

targetClass

Root object

正在调用的目标的类

#root.targetClass

args

Root object

用于调用目标的参数(作为数组)

#root.args[0]

caches

Root object

当前方法针对其运行的缓存集合

#root.caches[0].name

Argument name

Evaluation context

任何方法参数的名称。如果名称不可用(可能是因为没有调试信息),则参数名称也可以在 #a&lt;#arg&gt; 下获得,其中 #arg 代表参数索引(从 0 开始)。

#iban#a0(你也可以将 #p0#p&lt;#arg&gt; 符号作为别名使用)。

result

Evaluation context

方法调用的结果(要缓存的值)。仅可在 unless 表达式、cache put 表达式(用于计算 key)或 cache evict 表达式(当 beforeInvocationfalse 时)中使用。对于支持的包装器(例如 Optional),#result 指的是实际对象,而不是包装器。

#result

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.

通常在 CacheManager Bean 上提供特定于提供者的设置,例如在 CaffeineCacheManager 上。这些设置实际上也是全局的。

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 的配置故意相似。

处理缓存注释的默认建议模式为 proxy,它只允许通过代理拦截调用。同一类中的本地调用不能使用这种方式进行拦截。对于一个更高级的拦截模式,请考虑将 aspectj 模式与编译时或加载时编织结合使用。

有关实现 CachingConfigurer 所需的高级自定义项(使用 Java 配置)的更多详细信息,请参阅 javadoc

Table 2. Cache annotation settings
XML Attribute Annotation Attribute Default Description

cache-manager

无(参见 CachingConfigurer javadoc)

cacheManager

要使用的缓存管理器的名称。通过此缓存管理器(如果未设置,则为 cacheManager),将初始化默认 CacheResolver。如需更细粒度的缓存解析管理,请考虑设置“缓存解析器”属性。

cache-resolver

无(参见 CachingConfigurer javadoc)

使用已配置 cacheManagerSimpleCacheResolver

用于解析缓存的 CacheResolver 的 Bean 名称。此属性不是必需的,只需将其作为“cache-manager”属性的替代项进行指定。

key-generator

无(参见 CachingConfigurer javadoc)

SimpleKeyGenerator

要使用的自定义键生成器的名称。

error-handler

无(参见 CachingConfigurer javadoc)

SimpleCacheErrorHandler

要使用自定义缓存错误处理器的名称。默认情况下,在缓存相关操作期间引发的任何异常都会返还给客户端。

mode

mode

proxy

默认模式 (proxy) 使用 Spring 的 AOP 框架处理要通过代理进行处理的带注释的 Bean(按照前面讨论的代理语义,仅应用于通过代理进入的方法调用)。另一种模式 (aspectj) 则使用 Spring 的 AspectJ 缓存切面交织受影响的类,修改目标类字节码以应用于任何类型的方法调用。AspectJ 交织需要类路径中有 spring-aspects.jar,并且启用了编译时交织(或编译时交织)。(请参阅 Spring configuration 了解有关如何设置编译时交织的详细信息。)

proxy-target-class

proxyTargetClass

false

仅应用于代理模式。控制为使用 @Cacheable@CacheEvict 注释进行注释的类创建哪种类型的缓存代理。如果 proxy-target-class 属性设置为 true,将创建基于类的代理。如果 proxy-target-classfalse 或如果省略了该属性,将创建基于标准 JDK 接口的代理。(请参阅 Proxying Mechanisms 以详细了解不同代理类型。)

order

order

Ordered.LOWEST_PRECEDENCE

定义应用于使用 @Cacheable@CacheEvict 进行注释的 Bean 的缓存建议的顺序。(有关与排序 AOP 建议相关的规则的更多信息,请参阅 Advice Ordering。) 未指定的顺序意味着 AOP 子系统确定建议的顺序。

<cache:annotation-driven/> 仅在定义它的同一应用程序上下文中查找 @Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,如果您将 <cache:annotation-driven/> 放入 WebApplicationContext 中以用于 DispatcherServlet,它只会检查控制器中的 Bean,而不会检查服务中的 Bean。有关更多信息,请参阅 the MVC section

Method visibility and cache annotations

当您使用代理时,您应该仅对具有公共可见性的方法应用缓存注释。如果您使用这些注释对受保护的、私有的或包可见的方法进行注释,则不会引发错误,但带注释的方法不会表现出已配置的缓存设置。如果您需要注释非公共方法,请考虑使用 AspectJ(请参阅本节的其余部分),因为它更改了字节码本身。

Spring 建议您仅使用 @Cache* 注释具体类(和具体类的类方法),而不是注释接口。您当然可以将 @Cache* 注释放在接口(或接口方法)上,但这仅在您使用代理模式 (mode="proxy") 时才有效。如果您使用基于编织的方面 (mode="aspectj"),则编织基础设施无法在接口级别声明上识别缓存设置。

在代理模式(默认模式)中,仅通过代理传入的外部方法调用才被拦截。这意味着,即使被调用方法用 @Cacheable 标记,自调用(实际上,是目标对象中的一个方法调用 target 对象的另一个方法)在运行时也不会导致实际缓存。请考虑在此情况下使用 aspectj 模式。另请注意,代理必须被完全初始化才能提供预期行为,因此您不应该在自己的初始化代码(即,@PostConstruct)中依赖此功能。

Using Custom Annotations

Custom annotation and AspectJ

此功能仅适用于基于代理的方法,但通过使用 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 中所述,需要启用基于注释的行为。