对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:

  • @Cacheable:触发缓存填充。

  • @CacheEvict:触发缓存逐出。

  • @CachePut:在不干扰方法执行的情况下更新缓存。

  • @Caching:重新组合要应用于方法的多个缓存操作。

  • @CacheConfig:在类级别共享一些常见的缓存相关设置。

注释@Cacheable

顾名思义,您可以使用来划分可缓存的方法,即将结果存储在缓存中的方法,以便在随后的 调用(使用相同的参数),则返回缓存中的值,而没有 必须实际调用该方法。在最简单的形式中,注释声明 需要与带批注的方法关联的缓存的名称,如下所示 示例显示:@Cacheable

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

在前面的代码片段中,该方法与名为 的缓存相关联。 每次调用该方法时,都会检查缓存以查看调用是否具有 已经运行,不必重复。而在大多数情况下,只有一个 cache 被声明,注解允许指定多个名称,以便多个名称 正在使用缓存。在这种情况下,在调用 method — 如果至少命中一个缓存,则返回关联的值。findBookbooks

不包含该值的所有其他缓存也会更新,即使 缓存的方法实际上没有被调用。

以下示例在具有多个缓存的方法上使用:@CacheablefindBook

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

默认密钥生成

由于缓存本质上是键值存储,因此每次调用缓存方法 需要转换为适合缓存访问的密钥。缓存抽象 使用基于以下算法的简单算法:KeyGenerator

  • 如果未给出任何参数,则返回 。SimpleKey.EMPTY

  • 如果只给出一个参数,则返回该实例。

  • 如果给定了多个参数,则返回包含所有参数的 a。SimpleKey

这种方法适用于大多数用例,只要参数具有自然键即可 并实现有效和方法。如果不是这种情况, 你需要改变策略。hashCode()equals()

要提供不同的默认密钥生成器,您需要实现该接口。org.springframework.cache.interceptor.KeyGenerator

默认密钥生成策略随着 Spring 4.0 的发布而更改。早些时候 Spring 的版本使用密钥生成策略,对于多个密钥参数, 只考虑了 of 参数,而不是 .这可能会导致 意外的键冲突(背景参见 spring-framework#14870)。对于此类方案,新版本使用复合键。hashCode()equals()SimpleKeyGenerator

如果要继续使用以前的密钥策略,可以配置已弃用的类或创建自定义 基于哈希的实现。org.springframework.cache.interceptor.DefaultKeyGeneratorKeyGenerator

自定义密钥生成声明

由于缓存是通用的,因此目标方法很可能具有各种签名 这不能很容易地映射到缓存结构的顶部。这往往变得很明显 当目标方法具有多个参数时,其中只有一些参数适合 缓存(而其余的仅由方法逻辑使用)。请看以下示例:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

乍一看,虽然这两个论点影响了这本书的发现方式, 它们对缓存没有用处。此外,如果两者中只有一个很重要怎么办 而另一个不是?boolean

对于此类情况,注释允许您指定密钥的生成方式 通过其属性。您可以使用 SpEL 来选择 感兴趣的参数(或其嵌套属性),执行操作,甚至 调用任意方法,而无需编写任何代码或实现任何接口。 这是默认生成器的推荐方法, 因为随着代码库的增长,签名的方法往往大不相同。虽然 默认策略可能适用于某些方法,但很少适用于所有方法。@Cacheablekey

以下示例使用各种 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)

前面的代码片段显示了选择某个参数是多么容易,其中一个参数 属性,甚至是任意(静态)方法。

如果负责生成密钥的算法过于具体,或者需要 要共享,您可以在操作上定义自定义。为此, 指定要使用的 Bean 实现的名称,如下所示 示例显示:keyGeneratorKeyGenerator

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
和 参数是互斥的,并且是运算 这指定了异常中的两种结果。keykeyGenerator

默认缓存分辨率

缓存抽象使用检索缓存的简单方法 使用配置的 .CacheResolverCacheManager

若要提供不同的默认缓存解析器,需要实现接口。org.springframework.cache.interceptor.CacheResolver

自定义缓存分辨率

默认缓存分辨率非常适合使用 单一,没有复杂的缓存分辨率要求。CacheManager

对于使用多个缓存管理器的应用程序,可以将 设置为用于每个操作,如以下示例所示:cacheManager

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
1 指定。anotherCacheManager

您也可以以类似于 替换密钥生成。 每个缓存操作都请求解析,让实现 实际解析要使用的缓存,基于运行时参数。以下示例 演示如何指定:CacheResolverCacheResolver

@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
1 指定 .CacheResolver

从 Spring 4.1 开始,缓存注解的属性不再 强制性的,因为无论注释的内容如何,都可以提供此特定信息。valueCacheResolver

与 和 类似,和 参数是互斥的,并且指定两者的操作 导致异常,因为实现会忽略自定义。这可能不是你所期望的。keykeyGeneratorcacheManagercacheResolverCacheManagerCacheResolver

同步缓存

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

对于这些特殊情况,您可以使用该属性来指示基础 缓存提供程序,用于在计算值时锁定缓存条目。因此, 只有一个线程忙于计算值,而其他线程则被阻止,直到进入 在缓存中更新。下面的示例演示如何使用该属性:syncsync

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

使用 CompletableFuture 和 Reactive 返回类型进行缓存

从 6.1 开始,缓存注释采用和响应式返回类型 考虑在内,自动相应地调整缓存交互。CompletableFuture

对于返回 的方法,该 future 生成的对象 每当它完成时都会被缓存,并且缓存命中的缓存查找将 通过以下方式检索:CompletableFutureCompletableFuture

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

对于返回 Reactor 的方法,该 Reactive Streams 发出的对象 只要发布者可用,就会缓存它,并且缓存查找缓存 命中将被检索为(由 A 支持):MonoMonoCompletableFuture

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

对于返回 Reactor 的方法,该反应流发出的对象 Publisher 将被收集到 A 中,并在该列表完成时缓存, 缓存命中的缓存查找将检索为 (缓存值由 A 支持):FluxListFluxCompletableFutureList

@Cacheable("books")
public Flux<Book> findBooks(String author) {...}

这种反应式适配也适用于同步缓存, 在并发缓存未命中的情况下仅计算一次值:CompletableFuture

@Cacheable(cacheNames="foos", sync=true) (1)
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
1 使用属性。sync
为了使这种安排在运行时起作用,配置的缓存 需要能够进行基于 -的检索。Spring 提供的会自动适应该检索样式,并在其异步缓存模式为 Enabled:在实例上设置。CompletableFutureConcurrentMapCacheManagerCaffeineCacheManagersetAsyncCacheMode(true)CaffeineCacheManager
@Bean
CacheManager cacheManager() {
	CaffeineCacheManager cacheManager = new CaffeineCacheManager();
	cacheManager.setCacheSpecification(...);
	cacheManager.setAsyncCacheMode(true);
	return cacheManager;
}

最后但并非最不重要的一点是,请注意注释驱动的缓存是不合适的 用于涉及成分和背压的复杂反应相互作用。 如果选择在特定的反应式方法上声明,请考虑 相当粗粒度的缓存交互的影响,它只是存储 发出的对象,甚至是 .@CacheableMonoFlux

条件缓存

有时,某个方法可能不适合一直缓存(例如,它可能 取决于给定的参数)。缓存注释通过参数支持此类用例,该参数采用计算结果为 或 的表达式。如果 ,则缓存该方法。如果不是,它的行为就好像方法不是 cached(即,无论缓存中有什么值,每次都会调用该方法 或使用什么参数)。例如,仅当 参数的长度短于 32:conditionSpELtruefalsetruename

@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
1 在 上设置条件。@Cacheable

除了参数之外,您还可以使用该参数否决 将值添加到缓存中。与 不同,表达式是计算的 调用该方法后。为了扩展前面的例子,也许我们只 想要缓存平装书,如以下示例所示:conditionunlessconditionunless

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
1 使用该属性阻止精装书。unless

缓存抽象支持返回类型。如果值 存在,它将存储在关联的缓存中。如果值不是 现在,将存储在关联的缓存中。 始终指 业务实体,而不是支持的包装器,因此可以重写前面的示例 如下:java.util.OptionalOptionalOptionalnull#result

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

请注意,仍然是指而不是 .既然可能是,我们使用 SpEL 的安全导航算子#resultBookOptional<Book>null

可用的缓存 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

参数名称

评估背景

任何方法参数的名称。如果名称不可用 (可能是由于没有调试信息),参数名称也可以在参数索引的 where 代表下找到(从 开始)。#a<#arg>#arg0

#iban或者(您也可以使用 or 表示法作为别名)。#a0#p0#p<#arg>

result

评估背景

方法调用的结果(要缓存的值)。仅在表达式、表达式(用于计算 )或表达式(当 是 时)中可用。对于支持的包装器(如 ),指的是实际对象,而不是包装器。unlesscache putkeycache evictbeforeInvocationfalseOptional#result

#result

注释@CachePut

当需要在不干扰方法执行的情况下更新缓存时, 您可以使用批注。也就是说,该方法始终被调用,其 结果被放入缓存中(根据选项)。它支持 相同的选项,并且应该用于缓存填充,而不是 方法流程优化。以下示例使用批注:@CachePut@CachePut@Cacheable@CachePut

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
使用和注释相同的方法一般是 强烈劝阻,因为他们有不同的行为。而后者导致 使用缓存跳过方法调用,前者强制调用 命令运行缓存更新。这会导致意外行为,并且,例外 特定的极端情况(例如,具有将它们排除在每个极端情况之外的条件的注释 其他),应避免此类声明。另请注意,此类条件不应依赖 在结果对象(即变量)上,因为这些对象是预先验证的 确认排除项。@CachePut@Cacheable#result

从 6.1 开始,考虑了反应式返回类型, 每当生成的对象可用时,就执行 put 操作。@CachePutCompletableFuture

注释@CacheEvict

缓存抽象不仅允许填充缓存存储,还允许逐出。 此过程对于从缓存中删除过时或未使用的数据非常有用。与 相反,划分执行缓存的方法 逐出(即充当从缓存中删除数据的触发器的方法)。 与其同级类似,需要指定一个或多个缓存 受操作影响的,允许自定义缓存和密钥解析或 条件,并具有额外的参数 () 指示是否需要执行缓存范围的逐出 而不仅仅是入口逐出(基于密钥)。以下示例逐出 缓存中的所有条目:@Cacheable@CacheEvict@CacheEvictallEntriesbooks

@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
1 使用该属性从缓存中逐出所有条目。allEntries

当需要清除整个缓存区域时,此选项会派上用场。 而不是逐出每个条目(这需要很长时间,因为它效率低下), 所有条目都在一次操作中删除,如前面的示例所示。 请注意,框架将忽略此方案中指定的任何键,因为它不适用 (将逐出整个缓存,而不仅仅是一个条目)。

您还可以指示逐出是在(默认)之后还是之前发生 使用属性调用该方法。前者提供 与其他注释的语义相同:方法成功完成后, 对缓存运行操作(在本例中为逐出)。如果方法没有 运行(因为它可能被缓存)或引发异常,则不会发生逐出。 后者 () 导致驱逐始终发生在 方法被调用。这在不需要捆绑驱逐的情况下很有用 到方法结果。beforeInvocationbeforeInvocation=true

请注意,方法可以与 - 一起使用 - 因为这些方法充当 触发器,则返回值将被忽略(因为它们不与缓存交互)。这是 不是将数据添加到缓存或更新缓存中的数据的情况 因此,需要一个结果。void@CacheEvict@Cacheable

从 6.1 开始,考虑了反应式返回类型, 每当处理完成时,执行调用后逐出操作。@CacheEvictCompletableFuture

注释@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是允许共享缓存名称的类级注释, 自定义 , 自定义 和 自定义 。 将此批注放在类上不会打开任何缓存操作。KeyGeneratorCacheManagerCacheResolver

操作级自定义项始终覆盖 上设置的自定义项。 因此,这将为每个缓存操作提供三个级别的自定义:@CacheConfig

  • 全局配置,例如通过:请参阅下一节。CachingConfigurer

  • 在类级别,使用 .@CacheConfig

  • 在操作层面。

特定于提供程序的设置通常在 Bean 上可用, 例如,在 .这些实际上也是全球性的。CacheManagerCaffeineCacheManager

启用缓存注释

需要注意的是,即使声明缓存注释不会 自动触发他们的操作 - 就像 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>

元素和注释都允许您 指定影响缓存行为添加到 通过AOP申请。该配置有意与@Transactional的配置相似。cache:annotation-driven@EnableCaching

处理缓存注释的默认建议模式是 ,它允许 仅用于通过代理拦截呼叫。同一类中的本地呼叫 不能以这种方式被拦截。对于更高级的拦截模式,请考虑 切换到与编译时或加载时编织相结合的模式。proxyaspectj
有关高级定制(使用 Java 配置)的更多详细信息,这些定制是 需要实现,请参阅 JavadocCachingConfigurer
表 2.缓存批注设置
XML 属性 Annotation 属性 违约 描述

cache-manager

不适用(请参阅 CachingConfigurer javadoc)

cacheManager

要使用的缓存管理器的名称。默认值在后面初始化 具有此缓存管理器的场景(或者如果未设置)。查看更多 缓存分辨率的细粒度管理,请考虑设置“缓存解析器” 属性。CacheResolvercacheManager

cache-resolver

不适用(请参阅 CachingConfigurer javadoc)

A 使用配置的 .SimpleCacheResolvercacheManager

用于解析后备缓存的 CacheResolver 的 Bean 名称。 此属性不是必需的,只需指定为替代属性 “cache-manager”属性。

key-generator

不适用(请参阅 CachingConfigurer javadoc)

SimpleKeyGenerator

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

error-handler

不适用(请参阅 CachingConfigurer javadoc)

SimpleCacheErrorHandler

要使用的自定义缓存错误处理程序的名称。默认情况下,在 与缓存相关的操作将返回客户端。

mode

mode

proxy

默认模式 () 使用 Spring 的 AOP 处理要代理的带注释的 Bean 框架(如前所述,遵循代理语义,适用于方法调用 仅通过代理进入)。替代模式 () 将 受 Spring 的 AspectJ 缓存方面影响的类,修改目标类字节 应用于任何类型的方法调用的代码。AspectJ 编织需要在类路径中启用加载时编织(或编译时编织)。(有关如何设置的详细信息,请参阅 Spring 配置 加载时间编织。proxyaspectjspring-aspects.jar

proxy-target-class

proxyTargetClass

false

仅适用于代理模式。控制为哪些类型的缓存代理创建 使用 OR 注释注释的类。如果该属性设置为 ,则创建基于类的代理。 如果 是 或如果省略该属性,则标准 JDK 创建基于接口的代理。(有关不同代理类型的详细检查,请参阅代理机制@Cacheable@CacheEvictproxy-target-classtrueproxy-target-classfalse

order

order

Ordered.LOWEST_PRECEDENCE

定义应用于用 或 注释的 Bean 的缓存建议的顺序。(有关与以下内容相关的规则的更多信息 订购 AOP 建议,请参阅建议订购。 没有指定的顺序意味着 AOP 子系统确定建议的顺序。@Cacheable@CacheEvict

<cache:annotation-driven/>仅在定义 Bean 的同一应用程序上下文中查找 Bean。这意味着, 如果您输入 A for a ,它只检查控制器中的 Bean,而不是服务中的 Bean。 有关详细信息,请参阅 MVC 部分@Cacheable/@CachePut/@CacheEvict/@Caching<cache:annotation-driven/>WebApplicationContextDispatcherServlet
方法可见性和缓存注释

使用代理时,应仅将缓存批注应用于具有 公众可见性。如果对受保护的、私有的或包可见的方法进行批注 使用这些注释时,不会产生错误,但注释方法不会显示 配置的缓存设置。考虑使用 AspectJ(请参阅本节的其余部分) 如果您需要注释非公共方法,因为它会更改字节码本身。

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

使用自定义批注

自定义注释和 AspectJ

此功能仅适用于基于代理的方法,但可以启用 使用 AspectJ 需要付出一些额外的努力。

该模块仅为标准注释定义一个方面。 如果您已经定义了自己的注释,则还需要定义一个方面 那些。查看示例。spring-aspectsAnnotationCacheAspect

缓存抽象允许您使用自己的注释来识别哪种方法 触发缓存、填充或逐出。作为模板机制,这是非常方便的, 因为它消除了复制缓存注释声明的需要,即 如果指定了密钥或条件,或者如果外国进口,则特别有用 () 在您的代码库中是不允许的。与其他人类似 在构造型注释中,您可以 使用 、 、 和 作为元注释(即 可以注释其他注释)。在以下示例中,我们将通用声明替换为我们自己的自定义注释: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)

即使不是 Spring 注解,容器也会自动拾取 在运行时声明并理解其含义。请注意,如前所述,需要启用注释驱动的行为。@SlowService