对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

声明式基于注解的缓存

对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:spring-doc.cadn.net.cn

注解 @Cacheable

顾名思义,你可以使用@Cacheable来标记可缓存的方法——也就是说,对于这些方法,其结果会被存储在缓存中,这样在后续调用(使用相同参数)时,会直接返回缓存中的值,而无需实际调用该方法。在最简单的情况下,注解声明需要指定与带注解的方法相关联的缓存名称,如下例所示:spring-doc.cadn.net.cn

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

在前面的代码片段中,findBook 方法与名为 books 的缓存相关联。 每次调用该方法时,都会检查缓存以查看该调用是否已经执行过,不需要重复执行。虽然在大多数情况下只声明了一个缓存,但注解允许指定多个名称,以便使用多个缓存。在这种情况下,调用方法之前会检查每个缓存——如果至少有一个缓存命中,就会返回相关的值。spring-doc.cadn.net.cn

所有不包含该值的其他缓存也会被更新,尽管实际并未调用该缓存方法。

以下示例在具有多个缓存的 findBook 方法中使用 @Cacheablespring-doc.cadn.net.cn

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

默认密钥生成

由于缓存本质上是键值存储,每次调用缓存的方法都需要转换为适合缓存访问的键。缓存抽象基于以下算法使用一个简单的 KeyGeneratorspring-doc.cadn.net.cn

这种方法在大多数使用情况下效果良好,只要参数具有自然键并实现了有效的 hashCode()equals() 方法。如果不符合这种情况,则需要更改策略。spring-doc.cadn.net.cn

要提供不同的默认密钥生成器,您需要实现 org.springframework.cache.interceptor.KeyGenerator 接口。spring-doc.cadn.net.cn

Spring 4.0 版本发布后,默认的键生成策略发生了变化。早期版本的 Spring 使用的键生成策略在处理多个键参数时,仅考虑参数的 hashCode() 而不是 equals()。这可能导致意外的键冲突(详见 SPR-10237 了解背景)。新的 SimpleKeyGenerator 在此类情况下使用复合键。spring-doc.cadn.net.cn

如果您希望继续使用之前的密钥策略,可以配置已弃用的 org.springframework.cache.interceptor.DefaultKeyGenerator 类或创建自定义的 基于哈希的 KeyGenerator 实现。spring-doc.cadn.net.cn

自定义密钥生成声明

由于缓存是通用的,目标方法很可能会有各种不同的签名,这些签名无法直接映射到缓存结构上。当目标方法具有多个参数,其中只有一部分适合缓存(而其余的仅用于方法逻辑)时,这一点变得很明显。考虑以下示例:spring-doc.cadn.net.cn

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

乍一看,虽然两个boolean参数会影响如何找到这本书, 但它们对缓存没有用。此外,如果其中只有一个重要而另一个不重要呢?spring-doc.cadn.net.cn

在这些情况下,@Cacheable注解允许您通过其key属性指定如何生成键。您可以使用SpEL来选择感兴趣的参数(或其嵌套属性),执行操作,甚至调用任意方法而无需编写任何代码或实现任何接口。 这是推荐的方法,而不是默认生成器,因为随着代码库的增长,方法的签名往往会有所不同。虽然默认策略可能适用于某些方法,但很少适用于所有方法。spring-doc.cadn.net.cn

以下示例使用了各种SpEL声明(如果你不熟悉SpEL, 请为自己着想,阅读 Spring表达式语言):spring-doc.cadn.net.cn

@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)

前面的代码片段展示了如何轻松地选择某个参数、它的某个属性,甚至是一个任意的(静态)方法。spring-doc.cadn.net.cn

如果生成密钥的算法过于特定或需要共享,可以在操作上定义一个自定义的keyGenerator。要做到这一点,请指定要使用的KeyGenerator bean 实现的名称,如下例所示:spring-doc.cadn.net.cn

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
keykeyGenerator 参数是互斥的,同时指定这两个参数会导致异常。

默认缓存解析

缓存抽象使用一个简单的 CacheResolver,它通过配置的 CacheManager 在操作级别检索定义的缓存。spring-doc.cadn.net.cn

要提供不同的默认缓存解析器,您需要实现 org.springframework.cache.interceptor.CacheResolver 接口。spring-doc.cadn.net.cn

自定义缓存解析

默认的缓存解析方式适用于使用单个 CacheManager 且没有复杂缓存解析需求的应用程序。spring-doc.cadn.net.cn

对于使用多个缓存管理器的应用程序,可以为每个操作设置 cacheManager,如下例所示:spring-doc.cadn.net.cn

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

你也可以以类似于替换密钥生成的方式,完全替换CacheResolver。每次缓存操作都会请求解析,使实现能够根据运行时参数实际解析要使用的缓存。下面的示例显示了如何指定CacheResolverspring-doc.cadn.net.cn

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

自 Spring 4.1 起,缓存注解的 value 属性不再是必需的,因为此特定信息可以通过 CacheResolver 提供,而不管注解的内容如何。spring-doc.cadn.net.cn

keykeyGenerator 类似,cacheManagercacheResolver 参数是互斥的,指定两者的操作会导致异常, 因为自定义的 CacheManager 会被 CacheResolver 实现忽略。这可能不是你所期望的。spring-doc.cadn.net.cn

同步缓存

在多线程环境中,某些操作可能会针对相同的参数被同时调用(通常是在启动时)。默认情况下,缓存抽象不会锁定任何内容,因此相同的值可能会被多次计算,这违背了缓存的初衷。spring-doc.cadn.net.cn

对于这些特定情况,您可以使用 sync 属性,以指示底层的缓存提供者在计算值期间锁定缓存条目。结果是,只有一个线程在计算值,而其他线程则被阻塞,直到条目在缓存中更新。下面的示例显示了如何使用 sync 属性:spring-doc.cadn.net.cn

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

条件缓存

有时,某个方法可能并不总是适合缓存(例如,它可能依赖于给定的参数)。通过condition参数,缓存注解支持这种用例,该参数接受一个SpEL表达式,该表达式被求值为truefalse。如果为true,则会缓存该方法。否则,其行为就像该方法未被缓存一样(即,无论缓存中有什么值或使用了什么参数,该方法都会每次被调用)。例如,以下方法仅在参数name的长度小于32时才会被缓存:spring-doc.cadn.net.cn

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

除了 condition 参数外,您还可以使用 unless 参数来否决将值添加到缓存中的操作。与 condition 不同,unless 表达式是在方法被调用之后才进行求值的。为了进一步说明前面的例子,也许我们只希望缓存平装书,如下一个例子所示:spring-doc.cadn.net.cn

@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 总是指代业务实体,而不是支持的包装器,因此前面的示例可以重写如下:spring-doc.cadn.net.cn

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

请注意,#result 仍然指的是 Book 而不是 Optional<Book>。由于它可能是 null,我们使用 SpEL 的 安全导航运算符spring-doc.cadn.net.cn

可用的缓存SpEL求值上下文

每个 SpEL 表达式都针对一个专用的 context 进行计算。 除了内置参数外,框架还提供了专用的缓存相关 元数据,例如参数名称。下表描述了可供上下文使用的项目,以便您可以使用它们进行键和条件计算:spring-doc.cadn.net.cn

表 1. 缓存 SpEL 可用的元数据
名称 位置 描述 示例

methodNamespring-doc.cadn.net.cn

根对象spring-doc.cadn.net.cn

被调用的方法的名称spring-doc.cadn.net.cn

#root.methodNamespring-doc.cadn.net.cn

methodspring-doc.cadn.net.cn

根对象spring-doc.cadn.net.cn

正在调用的方法spring-doc.cadn.net.cn

#root.method.namespring-doc.cadn.net.cn

targetspring-doc.cadn.net.cn

根对象spring-doc.cadn.net.cn

被调用的目标对象spring-doc.cadn.net.cn

#root.targetspring-doc.cadn.net.cn

targetClassspring-doc.cadn.net.cn

根对象spring-doc.cadn.net.cn

被调用的目标的类spring-doc.cadn.net.cn

#root.targetClassspring-doc.cadn.net.cn

argsspring-doc.cadn.net.cn

根对象spring-doc.cadn.net.cn

调用目标时使用的参数(作为数组)spring-doc.cadn.net.cn

#root.args[0]spring-doc.cadn.net.cn

cachesspring-doc.cadn.net.cn

根对象spring-doc.cadn.net.cn

与当前方法运行的缓存集合spring-doc.cadn.net.cn

#root.caches[0].namespring-doc.cadn.net.cn

参数名称spring-doc.cadn.net.cn

评估上下文spring-doc.cadn.net.cn

任何方法参数的名称。如果名称不可用(可能由于没有调试信息),参数名称也可通过 #a<#arg> 获取,其中 #arg 表示参数索引(从 0 开始)。spring-doc.cadn.net.cn

#iban#a0(你也可以使用 #p0#p<#arg> 表示法作为别名)。spring-doc.cadn.net.cn

resultspring-doc.cadn.net.cn

评估上下文spring-doc.cadn.net.cn

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

#resultspring-doc.cadn.net.cn

注解 @CachePut

当需要在不干扰方法执行的情况下更新缓存时, 可以使用 @CachePut 注解。也就是说,该方法总是会被调用,并且其结果会放入缓存中(根据 @CachePut 选项)。它支持与 @Cacheable 相同的选项,应用于缓存填充而不是方法流程优化。下面的例子使用了 @CachePut 注解:spring-doc.cadn.net.cn

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一个方法上使用 @CachePut@Cacheable 注解通常强烈不建议,因为它们的行为不同。虽然后者通过使用缓存来跳过方法调用,而前者则强制调用以执行缓存更新。这会导致意外的行为,并且除了特定的特殊情况(例如注解具有相互排除的条件)之外,应避免此类声明。还要注意,这些条件不应依赖于结果对象(即 #result 变量),因为这些条件会在事先进行验证以确认排除。

注解 @CacheEvict

缓存抽象不仅允许填充缓存存储,还允许驱逐。 此过程有助于从缓存中删除过时或未使用的数据。与 @Cacheable 相反,@CacheEvict 标记执行缓存 驱逐的方法(即,作为从缓存中移除数据的触发器的方法)。 与它的兄弟标签类似,@CacheEvict 需要指定一个或多个 受该操作影响的缓存,允许指定自定义缓存和键解析或条件, 并且还有一个额外的参数( allEntries)用于指示是否需要执行整个缓存的驱逐, 而不是仅基于键进行条目驱逐。以下示例将从 books 缓存中驱逐所有条目:spring-doc.cadn.net.cn

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

此选项在需要清除整个缓存区域时非常有用。 而不是逐个驱逐条目(这会花费很长时间,因为效率低下), 所有条目都会通过一次操作被删除,如前面的示例所示。 请注意,在这种情况下,框架会忽略任何指定的键,因为它不适用 (整个缓存被驱逐,而不仅仅是某一条目)。spring-doc.cadn.net.cn

您也可以通过使用 beforeInvocation 属性来指示驱逐操作是在方法调用之后(默认)还是之前发生。前者提供了与其他注解相同的语义:一旦方法成功完成,就会对缓存执行一个操作(在这种情况下是驱逐)。如果方法未运行(可能已被缓存)或抛出异常,则不会发生驱逐。后者(beforeInvocation=true)会导致在方法调用之前始终发生驱逐。这在驱逐不需要与方法结果相关联的情况下很有用。spring-doc.cadn.net.cn

请注意,void 方法可以与 @CacheEvict 一起使用 - 因为这些方法作为触发器使用,所以返回值会被忽略(因为它们不与缓存交互)。这与 @Cacheable 不同,后者会向缓存中添加数据或更新缓存中的数据,因此需要一个结果。spring-doc.cadn.net.cn

注解 @Caching

有时,需要指定多个相同类型的注解(例如 @CacheEvict@CachePut)——例如,因为不同缓存之间的条件或键表达式不同。 @Caching 允许在同一方法上使用多个嵌套的 @Cacheable@CachePut@CacheEvict 注解。 下面的例子使用了两个 @CacheEvict 注解:spring-doc.cadn.net.cn

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

注解 @CacheConfig

到目前为止,我们已经看到缓存操作提供了许多自定义选项,并且您可以为每个操作设置这些选项。但是,如果这些选项适用于类的所有操作,那么配置起来可能会很繁琐。例如,为该类的每个缓存操作指定要使用的缓存名称可以被一个类级别的定义所取代。这就是@CacheConfig发挥作用的地方。下面的示例使用@CacheConfig来设置缓存的名称:spring-doc.cadn.net.cn

@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {

	@Cacheable
	public Book findBook(ISBN isbn) {...}
}
1 使用 @CacheConfig 来设置缓存的名称。

@CacheConfig 是一个类级别注解,允许共享缓存名称, 自定义 KeyGenerator、自定义 CacheManager 和自定义 CacheResolver。 将此注解放在类上不会开启任何缓存操作。spring-doc.cadn.net.cn

操作级别的自定义总是会覆盖在 @CacheConfig 上设置的自定义。 因此,这为每个缓存操作提供了三个级别的自定义:spring-doc.cadn.net.cn

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

启用缓存注解

需要注意的是,即使声明了缓存注解也不会自动触发它们的操作——像Spring中的许多功能一样,该特性必须通过声明方式启用(这意味着如果你怀疑缓存是问题所在,可以通过删除一行配置而不是删除代码中的所有注解来禁用它)。spring-doc.cadn.net.cn

要启用缓存注解,请将注解 @EnableCaching 添加到您的一个 @Configuration 类中:spring-doc.cadn.net.cn

@Configuration
@EnableCaching
public class AppConfig {

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

或者,对于XML配置,您可以使用 cache:annotation-driven 元素:spring-doc.cadn.net.cn

<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 的配置有意地保持相似。spring-doc.cadn.net.cn

处理缓存注释的默认建议模式是 proxy,这仅允许通过代理拦截调用。同一类中的本地调用无法以这种方式被拦截。如需更高级的拦截模式,可以考虑结合编译时或加载时织入切换到 aspectj 模式。
有关实现 CachingConfigurer 所需的高级自定义(使用 Java 配置)的更多详细信息,请参阅 javadoc
表 2. 缓存注解设置
XML 属性 注解属性 默认 描述

cache-managerspring-doc.cadn.net.cn

未提供(参见 CachingConfigurer 的 javadoc)spring-doc.cadn.net.cn

cacheManagerspring-doc.cadn.net.cn

要使用的缓存管理器的名称。默认情况下会在此缓存管理器后初始化 CacheResolver(或 cacheManager,如果未设置)。如需对缓存解析进行更细粒度的管理,请考虑设置“cache-resolver”属性。spring-doc.cadn.net.cn

cache-resolverspring-doc.cadn.net.cn

未提供(参见 CachingConfigurer 的 javadoc)spring-doc.cadn.net.cn

一个使用配置的 cacheManagerSimpleCacheResolverspring-doc.cadn.net.cn

用于解析后备缓存的CacheResolver的Bean名称。 此属性不是必需的,仅在作为'cache-manager'属性的替代时才需要指定。spring-doc.cadn.net.cn

key-generatorspring-doc.cadn.net.cn

未提供(参见 CachingConfigurer 的 javadoc)spring-doc.cadn.net.cn

SimpleKeyGeneratorspring-doc.cadn.net.cn

自定义密钥生成器的名称spring-doc.cadn.net.cn

error-handlerspring-doc.cadn.net.cn

未提供(参见 CachingConfigurer 的 javadoc)spring-doc.cadn.net.cn

SimpleCacheErrorHandlerspring-doc.cadn.net.cn

自定义缓存错误处理程序的名称。默认情况下,任何在缓存相关操作期间抛出的异常都会返回给客户端。spring-doc.cadn.net.cn

modespring-doc.cadn.net.cn

modespring-doc.cadn.net.cn

proxyspring-doc.cadn.net.cn

默认模式(proxy)通过使用Spring的AOP框架对带注解的bean进行代理处理(遵循前面讨论的代理语义,仅适用于通过代理进入的方法调用)。替代模式(aspectj)则使用Spring的AspectJ缓存切面来编织受影响的类,修改目标类的字节码以适用于任何类型的方法调用。AspectJ编织还需要spring-aspects.jar在类路径中,并且需要启用加载时编织(或编译时编织)。(有关如何设置加载时编织的详细信息,请参见Spring配置。)spring-doc.cadn.net.cn

proxy-target-classspring-doc.cadn.net.cn

proxyTargetClassspring-doc.cadn.net.cn

falsespring-doc.cadn.net.cn

仅适用于代理模式。控制为使用@Cacheable@CacheEvict注解的类创建的缓存代理类型。如果proxy-target-class属性设置为true,则创建基于类的代理。如果proxy-target-classfalse或者该属性被省略,则创建标准的JDK接口代理。(有关不同代理类型的详细分析,请参见代理机制。)spring-doc.cadn.net.cn

orderspring-doc.cadn.net.cn

orderspring-doc.cadn.net.cn

Ordered.LOWEST_PRECEDENCEspring-doc.cadn.net.cn

定义了应用于标注有@Cacheable@CacheEvict的Bean的缓存建议的顺序。(有关与AOP建议排序相关的规则的更多信息,请参阅建议排序。) 未指定顺序意味着AOP子系统将确定建议的顺序。spring-doc.cadn.net.cn

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

使用代理时,应仅将缓存注解应用于具有公共可见性的方法。如果您使用这些注解标注受保护的、私有的或包可见的方法,不会引发错误,但被注解的方法不会表现出配置的缓存设置。如果需要注解非公共方法,请考虑使用 AspectJ(参见本节的其余部分),因为它会直接修改字节码。spring-doc.cadn.net.cn

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

使用自定义注解

自定义注解和AspectJ

此功能仅适用于基于代理的方法,但可以通过使用 AspectJ 进行一些额外的努力来启用。spring-doc.cadn.net.cn

spring-aspects 模块仅定义了标准注解的切面。 如果您定义了自己的注解,还需要为这些注解定义切面。 请查看 AnnotationCacheAspect 以获取示例。spring-doc.cadn.net.cn

缓存抽象机制允许您使用自己的注解来标识哪些方法会触发缓存的生成或清除。作为一种模板机制,这非常方便,因为它消除了重复声明缓存注解的需要,这在键或条件被指定的情况下,或者在您的代码库中不允许使用外部导入(org.springframework)时尤其有用。与其余的 构造型 注解类似,您可以使用 @Cacheable@CachePut@CacheEvict@CacheConfig 作为 元注解(即可以注解其他注解的注解)。在下面的例子中,我们将一个常见的 @Cacheable 声明替换为自定义注解:spring-doc.cadn.net.cn

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}

在前面的示例中,我们定义了自己的 SlowService 注解, 它本身被标注为 @Cacheable。现在我们可以替换以下代码:spring-doc.cadn.net.cn

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

以下示例展示了我们可以用来替换前面代码的自定义注解:spring-doc.cadn.net.cn

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

尽管 @SlowService 不是 Spring 注解,但容器会在运行时自动获取其声明并理解其含义。请注意,如前所述之前,需要启用注解驱动的行为。spring-doc.cadn.net.cn