基于声明式注解的缓存

对于缓存声明,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参数会影响查找图书的方式,但它们对缓存没有任何用处。此外,如果只有两个参数中的一个很重要,而另一个不重要怎么办?

对于这种情况,@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)
keykeyGenerator参数是互斥的,指定两者的操作会导致异常。

默认缓存解析

缓存抽象使用简单的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开始,缓存注解的value属性不再是强制性的,因为无论注解的内容如何,此特定信息都可以由CacheResolver提供。

keykeyGenerator类似,cacheManagercacheResolver参数是互斥的,并且指定两者的操作会导致异常,因为自定义CacheManager会被CacheResolver实现忽略。这可能不是您期望的结果。

同步缓存

在多线程环境中,某些操作可能会针对相同的参数并发调用(通常在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而破坏缓存的目的。

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

@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
1 使用sync属性。
这是一个可选功能,您喜爱的缓存库可能不支持它。核心框架提供的全部CacheManager实现都支持它。有关更多详细信息,请参阅您的缓存提供商的文档。

使用CompletableFuture和响应式返回类型进行缓存

从6.1版本开始,缓存注解会考虑CompletableFuture和响应式返回类型,并自动相应地调整缓存交互。

对于返回CompletableFuture的方法,只要期货对象完成,它产生的对象就会被缓存,并且缓存命中时的缓存查找将通过CompletableFuture检索。

@Cacheable("books")
public CompletableFuture<Book> findBook(ISBN isbn) {...}

对于返回Reactor Mono的方法,只要响应式流发布者发出的对象可用,它就会被缓存,并且缓存命中时的缓存查找将作为Mono(由CompletableFuture支持)检索。

@Cacheable("books")
public Mono<Book> findBook(ISBN isbn) {...}

对于返回Reactor Flux的方法,只要该列表完成,该响应式流发布者发出的对象就会被收集到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表达式,该表达式计算结果为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的安全导航运算符

可用的缓存SpEL评估上下文

每个SpEL表达式都针对一个专用的context进行评估。除了内置参数外,框架还提供专用的与缓存相关的元数据,例如参数名称。下表描述了提供给上下文以便您可以将其用于键和条件计算的项目。

表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表达式(当beforeInvocationfalse时)中可用。对于受支持的包装器(例如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和响应式返回类型,并在生成的對象可用时执行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 中许多其他功能一样,此功能必须声明式启用(这意味着如果您怀疑缓存是罪魁祸首,您可以只移除一行配置代码来禁用它,而无需更改代码中的所有注解)。

要启用缓存注解,请在您的一个@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)

使用已配置的cacheManagerSimpleCacheResolver

要用于解析后端缓存的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-classfalse或省略此属性,则创建标准的基于JDK接口的代理。(有关不同代理类型的详细说明,请参见代理机制)。

order

order

Ordered.LOWEST_PRECEDENCE

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

<cache:annotation-driven/>仅在其定义的同一应用程序上下文中的bean上查找@Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,如果您将<cache:annotation-driven/>放在DispatcherServletWebApplicationContext中,它只检查控制器中的bean,而不检查服务中的bean。有关更多信息,请参见MVC部分
方法可见性和缓存注解

使用代理时,应仅将缓存注解应用于具有公共可见性的方法。如果您确实使用这些注解来注解受保护的、私有的或包可见的方法,则不会引发错误,但带注解的方法不会显示已配置的缓存设置。如果您需要注解非公共方法,请考虑使用AspectJ(参见本节的其余部分),因为它会更改字节码本身。

Spring 建议您只使用@Cache*注解来注解具体的类(和具体类的),而不是注解接口。您当然可以在接口(或接口方法)上放置@Cache*注解,但这仅在您使用代理模式(mode="proxy")时才有效。如果您使用基于织入的方面(mode="aspectj"),则织入基础结构不会识别接口级别声明上的缓存设置。
在代理模式(默认模式)下,只有通过代理传入的外部方法调用才会被拦截。这意味着自调用(实际上,目标对象中的一个方法调用目标对象的另一个方法)即使被调用的方法用@Cacheable标记,也不会在运行时导致实际的缓存。在这种情况下,请考虑使用aspectj模式。此外,代理必须完全初始化才能提供预期的行为,因此您不应在初始化代码(即@PostConstruct)中依赖此功能。

使用自定义注解

自定义注解和AspectJ

此功能仅适用于基于代理的方法,但可以通过一些额外的努力使用AspectJ来启用。

spring-aspects模块仅为标准注解定义了一个方面。如果您已定义自己的注解,则还需要为这些注解定义一个方面。请查看AnnotationCacheAspect以了解示例。

缓存抽象允许您使用自己的注解来识别触发缓存填充或驱逐的方法。这作为模板机制非常方便,因为它消除了重复缓存注解声明的需要,这在指定键或条件或不允许在代码库中使用外部导入(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注解,容器也会在运行时自动拾取其声明并理解其含义。请注意,如前面所述,需要启用注解驱动的行为。