声明式基于注解的缓存
对于缓存声明,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)
key
和keyGenerator
参数是互斥的,一个操作指定两者会导致异常。
默认的缓存解析
缓存抽象使用简单的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
都可以提供此特定信息。与
key
和keyGenerator
类似,cacheManager
和cacheResolver
参数是互斥的,指定这两者的操作会导致异常,因为CacheResolver
实现忽略了自定义CacheManager
,这可能不是你所期望的。
同步缓存
在多线程环境中,可能会为同一参数同时调用某些操作(通常在启动时),默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而无法实现缓存。
对于这些特定情况,你可以使用sync
属性指示底层缓存提供程序在计算值时锁定缓存条目,因此,只有一个线程忙于计算该值,而其他线程则被阻塞,直到该条目在缓存中更新为止,以下示例显示了如何使用sync
属性:
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
这是一项可选功能,你最喜欢的缓存库可能不支持它,核心框架提供的所有
CacheManager
实现都支持它,有关更多详细信息,请参阅缓存提供程序的文档。
条件缓存
有时,方法可能不适合一直缓存(例如,它可能取决于给定的参数),缓存注解通过condition
参数支持此类功能,该参数采用被评估为true
或false
的SpEL
表达式,如果为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 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 表达式、缓存放置表达式(计算键)或缓存逐出表达式(当 beforeInvocation 为false 时)中使用 对于受支持的包装器(例如 Optional ),#result 引用实际的对象,而不是包装器。 |
#result |
@CachePut
注解
当需要更新缓存而不干扰方法执行时,可以使用@CachePut
注解,也就是说,始终执行该方法,并将其结果放入缓存中(根据@CachePut
选项)。它支持与@Cacheable
相同的选项,应该用于缓存填充而不是方法流优化,以下示例使用@CachePut
注解:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
通常强烈建议不要在同一方法上使用@CachePut
和@Cacheable
注解,因为它们有不同的行为,虽然后者导致通过使用缓存跳过方法执行,但前者强制执行以执行缓存更新。这将导致意想不到的行为,除了特定的情况外(例如注解具有将它们彼此排除的条件)之外,应避免此类声明。还请注意,这些条件不应该依赖于result
对象(即#result
变量),因为这些都是预先验证以确认排除的。
@CacheEvict
注解
缓存抽象不仅允许缓存存储的填充,还允许驱逐,此过程对于从缓存中删除陈旧或未使用的数据非常有用。与@Cacheable
相反,@CacheEvict
划分了执行缓存逐出的方法(即,用作从缓存中删除数据的触发器的方法)。@CacheEvict
需要指定受操作影响的一个或多个缓存,允许指定自定义缓存和键解析或条件,并具有一个额外的参数(allEntries
),指示是否需要执行缓存范围的驱逐而不仅仅是条目驱逐(基于键),以下示例逐出books
缓存中的所有条目:
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
当需要清除整个缓存区域时,此选项会派上用场,而不是逐出每个条目(这将花费很长时间,因为它是低效的),所有条目在一个操作中被移除,如前面的示例所示。请注意,框架会忽略此方案中指定的任何键,因为它不适用(整个缓存被驱逐,而不仅仅是一个条目)。
你还可以通过使用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")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
@CacheConfig
是一个类级注解,允许共享缓存名称、自定义KeyGenerator
、自定义CacheManager
和自定义CacheResolver
,将此注解放在类上不会打开任何缓存操作。
操作级别自定义始终覆盖@CacheConfig上
的自定义集,因此,这为每个缓存操作提供了三个级别的自定义:
- 全局配置,可用于
CacheManager
、KeyGenerator
。 - 在类级别,使用
@CacheConfig
。 - 在操作级别。
启用缓存注解
请务必注意,即使声明缓存注解也不会自动触发其操作 — 与Spring中的许多功能一样,该特性必须以声明方式启用(这意味着如果你怀疑缓存是罪魁祸首,你可以通过仅删除一个配置行而不是代码中的所有注解来禁用它)。
要启用缓存注解,请将注解@EnableCaching
添加到@Configuration
类之一:
@Configuration
@EnableCaching
public class AppConfig {
}
或者,对于XML配置,你可以使用cache:annotation-driven
元素:
cache:annotation-driven
元素和@EnableCaching
注解都允许你指定各种选项,这些选项影响方式通过AOP将缓存行为添加到应用程序,该配置与@Transactional
的配置有意类似。
处理缓存注解的默认建议模式是proxy
,它允许仅通过代理拦截调用,同一类中的本地调用不能以这种方式截获,对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到aspectj
模式。
有关实现
CachingConfigurer
所需的高级自定义(使用Java配置)的更多详细信息,请参阅 javadoc。
缓存注解设置
XML属性 | 注解属性 | 默认 | 描述 |
---|---|---|---|
cache-manager | N/A(参见CachingConfigurer javadoc | cacheManager |
要使用的缓存管理器的名称; 使用此缓存管理器(如果未设置则为 cacheManager )在后台初始化默认的CacheResolver ;要获得更精细的缓存解析管理,请考虑设置“cache-resolver”属性。 |
cache-resolver | N/A(参见CachingConfigurer javadoc | 使用配置的cacheManager 的SimpleCacheResolver |
用于解析后备缓存的CacheResolver 的bean名称;此属性不是必需的,只需要指定为“cache-manager”属性的替代。 |
key-generator | N/A(参见CachingConfigurer javadoc | SimpleKeyGenerator |
要使用的自定义键生成器的名称。 |
error-handler | N/A(参见CachingConfigurer javadoc | SimpleCacheErrorHandler |
要使用的自定义缓存错误处理程序的名称; 默认情况下,在缓存相关操作期间抛出的任何异常都会在客户端返回。 |
mode | mode | proxy | 默认模式(proxy )处理要使用Spring的AOP框架代理的带注解bean(遵循代理语义,如前所述,仅适用于通过代理进入的方法调用);替代模式( aspectj )用Spring的AspectJ缓存切面编织受影响的类,修改目标类字节码以应用于任何类型的方法调用;AspectJ编织需要在类路径中使用 spring-aspects.jar 以及启用加载时编织(或编译时编织);(有关如何设置加载时编织的详细信息,请参阅Spring配置)。 |
proxy-target-class | proxyTargetClass | false |
仅适用于代理模式; 控制为使用 @Cacheable 或@CacheEvict 注解注解的类创建哪种类型的缓存代理;如果 proxy-target-class 属性设置为true ,则创建基于类的代理;如果 proxy-target-class 为false 或者省略了该属性,则会创建基于标准JDK接口的代理;(有关不同代理类型的详细解释,请参阅代理机制)。 |
order | order | Ordered.LOWEST_PRECEDENCE |
定义应用于使用@Cacheable 或@CacheEvict 注解的bean的缓存建议的顺序;(有关排序AOP建议相关规则的更多信息,请参阅建议排序); 没有指定的排序意味着AOP子系统确定建议的顺序。 |
查找
@Cacheable/@CachePut/@CacheEvict/@Caching
仅在定义它的同一应用程序上下文中的bean上进行缓存,这意味着,如果你在WebApplicationContext
中为DispatcherServlet
放置,它只会在你的控制器中检查bean,而不是你的服务,有关更多信息,请参阅MVC部分。
方法可见性和缓存注解
使用代理时,应仅将缓存注解应用于具有公共可见性的方法,如果使用这些注解来注解
protected
、private
或包可见方法,不会引发错误,但带注解的方法不会显示已配置的缓存设置。如果需要注解非公共方法,请考虑使用AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。
Spring建议只使用@Cache*
注解来注解具体类(以及具体类的方法),而不是注解接口,你当然可以将@Cache*
注解放在接口(或接口方法)上,但这只能在你使用基于接口的代理时按预期工作。Java注解不是从接口继承的事实意味着,如果你使用基于类的代理(proxy-target-class="true"
)或基于编织的切面(mode="aspectj"
),代理和编织基础设施无法识别缓存设置,并且该对象未包装在缓存代理中。
在代理模式(默认)下,只拦截通过代理进入的外部方法调用,这意味着自调用(实际上是目标对象中调用目标对象的另一个方法的方法)在运行时不会导致实际的缓存,即使调用的方法用@Cacheable
标记,在这种情况下,请考虑使用aspectj
模式。此外,必须完全初始化代理以提供预期的行为,因此你不应该在初始化代码(即@PostConstruct
)中依赖此功能。
使用自定义注解
自定义注解和AspectJ
此特性仅适用于基于代理的方法,但可以通过使用AspectJ进行一些额外的工作。
spring-aspects
模块仅定义标准注解的切面,如果你已定义自己的注解,则还需要为这些注解定义切面。
缓存抽象允许你使用自己的注解来标识触发缓存填充或驱逐的方法,这作为模板机制非常方便,因为它消除了重复缓存注解声明的需要,这在指定了键或条件或者代码库中不允许外部导入(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注解,但容器会在运行时自动获取其声明并理解其含义,请注意,如前所述,需要启用annotation-driven的行为。