|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
声明式基于注解的缓存
对于缓存声明,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 使用的键生成策略在处理多个键参数时,仅考虑参数的 如果您希望继续使用之前的密钥策略,可以配置已弃用的
|
自定义密钥生成声明
由于缓存是通用的,目标方法很可能会有各种不同的签名,这些签名无法直接映射到缓存结构上。当目标方法具有多个参数,其中只有一部分适合缓存(而其余的仅用于方法逻辑)时,这一点变得很明显。考虑以下示例:
@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 进行计算。
除了内置参数外,框架还提供了专用的缓存相关
元数据,例如参数名称。下表描述了可供上下文使用的项目,以便您可以使用它们进行键和条件计算:
| 名称 | 位置 | 描述 | 示例 |
|---|---|---|---|
|
根对象 |
被调用的方法的名称 |
|
|
根对象 |
正在调用的方法 |
|
|
根对象 |
被调用的目标对象 |
|
|
根对象 |
被调用的目标的类 |
|
|
根对象 |
调用目标时使用的参数(作为数组) |
|
|
根对象 |
与当前方法运行的缓存集合 |
|
参数名称 |
评估上下文 |
任何方法参数的名称。如果名称不可用(可能由于没有调试信息),参数名称也可通过 |
|
|
评估上下文 |
方法调用的结果(要缓存的值)。仅在 |
|
注解 @CachePut
当需要在不干扰方法执行的情况下更新缓存时,
可以使用 @CachePut 注解。也就是说,该方法总是会被调用,并且其结果会放入缓存中(根据 @CachePut 选项)。它支持与 @Cacheable 相同的选项,应用于缓存填充而不是方法流程优化。下面的例子使用了 @CachePut 注解:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一个方法上使用 @CachePut 和 @Cacheable 注解通常强烈不建议,因为它们的行为不同。虽然后者通过使用缓存来跳过方法调用,而前者则强制调用以执行缓存更新。这会导致意外的行为,并且除了特定的特殊情况(例如注解具有相互排除的条件)之外,应避免此类声明。还要注意,这些条件不应依赖于结果对象(即 #result 变量),因为这些条件会在事先进行验证以确认排除。 |
注解 @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 不同,后者会向缓存中添加数据或更新缓存中的数据,因此需要一个结果。
注解 @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 模式。 |
有关实现 CachingConfigurer 所需的高级自定义(使用 Java 配置)的更多详细信息,请参阅
javadoc。 |
| XML 属性 | 注解属性 | 默认 | 描述 |
|---|---|---|---|
|
未提供(参见 |
|
要使用的缓存管理器的名称。默认情况下会在此缓存管理器后初始化 |
|
未提供(参见 |
一个使用配置的 |
用于解析后备缓存的CacheResolver的Bean名称。 此属性不是必需的,仅在作为'cache-manager'属性的替代时才需要指定。 |
|
未提供(参见 |
|
自定义密钥生成器的名称 |
|
未提供(参见 |
|
自定义缓存错误处理程序的名称。默认情况下,任何在缓存相关操作期间抛出的异常都会返回给客户端。 |
|
|
|
默认模式( |
|
|
|
仅适用于代理模式。控制为使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定义了应用于标注有 |
<cache:annotation-driven/> 仅在它定义的同一应用程序上下文中查找 @Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,
如果你在 <cache:annotation-driven/> 中为 WebApplicationContext 添加了 DispatcherServlet,它只会检查你的控制器中的 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 注解,但容器会在运行时自动获取其声明并理解其含义。请注意,如前所述之前,需要启用注解驱动的行为。