转载

Spring Framework 参考文档(声明式基于注解的缓存)

声明式基于注解的缓存

对于缓存声明,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() ,这可能会导致意想不到的键冲突(有关背景,请参见 SPR-10237 ),新的 SimpleKeyGenerator 为这些场景使用复合键。

如果你想继续使用前面的键策略,可以配置已废弃的 org.springframework.cache.interceptor.DefaultKeyGenerator 类,或者创建一个基于散列的自定义 KeyGenerator 实现。

自定义键生成声明

由于缓存是通用的,因此目标方法很可能具有各种签名,这些签名无法轻松映射到缓存结构之上,当目标方法有多个参数时,这往往会变得明显,其中只有一些参数适用于缓存(其余参数仅由方法逻辑使用),请考虑以下示例:

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

乍一看,虽然两个 boolean 参数影响了书的发现方式,但它们对缓存没有用处,如果两个中只有一个重要而另一个不重要怎么办?

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

以下示例是各种SpEL声明:

@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") 
public Book findBook(ISBN isbn) {...}

你也可以完全替换 CacheResolver ,方式类似于替换键生成,为每个缓存操作请求解析,让实现实际上根据运行时参数解析要使用的缓存,以下示例显示如何指定 CacheResolver

@Cacheable(cacheResolver="runtimeCacheResolver") 
public Book findBook(ISBN isbn) {...}
从Spring 4.1开始,缓存注解的 value 属性不再是必需的,因为无论注解的内容如何,​​ CacheResolver 都可以提供此特定信息。

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

同步缓存

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

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

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

这是一项可选功能,你最喜欢的缓存库可能不支持它,核心框架提供的所有 CacheManager 实现都支持它,有关更多详细信息,请参阅缓存提供程序的文档。

条件缓存

有时,方法可能不适合一直缓存(例如,它可能取决于给定的参数),缓存注解通过 condition 参数支持此类功能,该参数采用被评估为 truefalseSpEL 表达式,如果为 true ,则缓存该方法,如果没有,它的行为就好像该方法没有被缓存(也就是说,无论缓存中的值是什么,或者使用了什么参数,每次都执行该方法)。例如,仅当参数 name 的长度小于32时,才会缓存以下方法:

@Cacheable(cacheNames="book", condition="#name.length() < 32") 
public Book findBook(String name)

condition 参数外,还可以使用 unless 参数否决向缓存添加值,与 condition 不同, unless 表达式在调用该方法后进行评估,要扩展上一个示例,也许我们只想缓存平装书,如下例所示:

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

缓存抽象支持 java.util.Optional ,仅在其存在时将其内容用作缓存值, #result 总是引用业务实体而从不支持包装器,因此前面的示例可以重写如下:

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

请注意, result 仍然引用 Book 而不是 Optional ,由于它可能为 null ,我们应该使用安全导航操作符。

可用的缓存SpEL评估上下文

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

名称 位置 描述 示例
methodName 根对象 要调用的方法的名称 #root.methodName
method 根对象 正在调用的方法 #root.method.name
target 根对象 正在调用的目标对象 #root.target
targetClass 根对象 正在调用的目标的类 #root.targetClass
args 根对象 用于调用目标的参数(作为数组) #root.args[0]
caches 根对象 执行当前方法的高速缓存的集合 #root.caches[0].name
参数名称 评估上下文 任何方法参数的名称;
如果名称不可用(可能由于没有调试信息),
参数名称也可以在 #a<#arg> 下获得。
其中 #arg 代表参数索引(从 0 开始)。
#iban#a0
(你也可以使用 #p0
#p<#arg> 表示法作为别名)
result 评估上下文 方法调用的结果(要缓存的值)
只能在 unless 表达式、缓存放置表达式(计算键)
或缓存逐出表达式(当 beforeInvocationfalse
时)中使用
对于受支持的包装器(例如 Optional ),
#result 引用实际的对象,而不是包装器。
#result

@CachePut 注解

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

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

通常强烈建议不要在同一方法上使用 @CachePut@Cacheable 注解,因为它们有不同的行为,虽然后者导致通过使用缓存跳过方法执行,但前者强制执行以执行缓存更新。这将导致意想不到的行为,除了特定的情况外(例如注解具有将它们彼此排除的条件)之外,应避免此类声明。还请注意,这些条件不应该依赖于 result 对象(即 #result 变量),因为这些都是预先验证以确认排除的。

原文  https://segmentfault.com/a/1190000020070999
正文到此结束
Loading...