20220507 8. Integration - Cache Abstraction
前言
相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
从版本 3.1 开始,Spring 框架就支持向现有的 Spring 应用程序透明地添加缓存。与事务支持类似,缓存抽象允许一致地使用各种缓存解决方案,对代码的影响最小。
从 Spring 4.1开始,在 JSR-107 注解 和更多定制选项的支持下,缓存抽象得到了显著的扩展。
理解 Cache 抽象
Cache vs Buffer
术语
buffer
和cache
通常可以互换使用。然而,请注意,它们代表不同的事物。传统上,buffer
用作快速实体和慢速实体之间的数据的中间临时存储。由于一方必须等待另一方 (这会影响性能) ,buffer
通过允许整个数据块一次移动而不是小块移动来缓解这一问题。数据只从缓冲区写入和读取一次。此外,缓冲区至少对知道它的一方是可见的。另一方面,
cache
按照定义是隐藏的,任何一方都不知道发生了缓存。它还可以提高性能,但这是通过以快速的方式多次读取相同的数据实现的。
缓存抽象将缓存应用于 Java 方法,从而减少了基于缓存中可用信息的执行次数。也就是说,每次调用目标方法时,抽象都会应用一个缓存行为,以检查该方法是否已经被给定的参数调用。如果已经调用了它,则返回缓存的结果,而不必调用实际的方法。如果尚未调用该方法,则调用该方法,并将结果缓存并返回给用户,以便在下次调用该方法时,返回缓存的结果。这样,对于给定的一组参数和结果,只需要调用一次昂贵的方法 (无论是 CPU 绑定的方法还是 IO 绑定的方法) ,而无需实际再次调用该方法。在不对调用程序造成任何干扰的情况下透明地应用缓存逻辑。
这种方法只适用于那些无论调用多少次都保证为给定的输入返回相同的输出的方法。
缓存抽象提供了其他与缓存相关的操作,例如更新缓存的内容或删除一个或所有条目的能力。如果缓存处理在应用程序运行过程中可能发生更改的数据,那么这些方法非常有用。
与 Spring 框架中的其他服务一样,缓存服务是一个抽象 (而不是一个缓存实现) ,需要使用实际存储来存储缓存数据ーー也就是说,这个抽象使您不必编写缓存逻辑,不用提供实际的数据存储。这个抽象是通过 org.springframework.cache.Cache
和 org.springframework.cache.CacheManager
接口实现的。
Spring 提供了该抽象的 一些实现 : JDK 基于 java.util.concurrent.ConcurrentMap
的缓存、 Ehcache 2.x 、 Gemfire 缓存、 Caffeine 和 JSR-107 兼容缓存 (比如 Ehcache 3.x) 。有关插入其他缓存存储器和供应商的更多信息,请参见 插入不同的后端缓存 。
缓存抽象对于多线程和多进程环境没有特殊的处理,因为这些特性是由缓存实现处理的
如果您有一个多进程环境 (即部署在多个节点上的应用程序) ,则需要相应地配置缓存供应商。根据用例的不同,在几个节点上复制相同的数据就足够了。但是,如果在应用程序运行过程中更改了数据,则可能需要启用其他传播机制。
缓存特定项是通过编程缓存交互找到的典型的 get-if-not-found-then-proceed-and-put-eventually
代码块的直接等价物。不应用锁,并且多个线程可能试图并发加载同一项。这同样适用于删除(eviction)。如果多个线程试图同时更新或删除数据,则可以使用陈旧数据。某些缓存供应商在该领域提供高级功能。有关详细信息,请参阅缓存提供程序的文档。
要使用缓存抽象,需要注意两个方面:
- 缓存声明 : 确定需要缓存的方法及其策略
- 缓存配置 : 存储数据并从中读取数据的底层缓存
基于声明式注解的缓存
对于缓存声明,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()
。这可能导致意外的键冲突。新的SimpleKeyGenerator
使用了一个复合键来处理这样的场景。如果希望继续使用前面的键策略,可以配置已弃用的
org.springframework.cache.interceptor.DefaultKeyGenerator
类或创建基于散列的自定义KeyGenerator
实现。
自定义键生成声明
由于缓存是通用的,目标方法很可能具有各种签名,这些签名不能很容易地映射到缓存结构的顶部。当目标方法有多个参数,其中只有一些参数适合缓存时 (而其余的参数仅由方法逻辑使用) ,这种情况就会变得很明显。考虑下面的例子:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然这两个 boolean
参数影响了查找图书的方式,但它们对缓存毫无用处。此外,如果两者中只有一个是重要的,而另一个不重要,那该怎么办?
对于这种情况,@Cacheable
注解允许您指定如何通过键属性生成键。您可以使用 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
参数支持这种用例, condition
参数接受一个 SpEL 表达式,该表达式的计算结果为 true
或 false
。如果为 true
,则缓存该方法。如果 false
,它的行为就好像方法没有使用缓存 (也就是说,无论缓存中有什么值或者使用什么参数,每次都会调用该方法) 。例如,只有当参数 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
计算结果。除了内置参数之外,框架还提供了专用的与缓存相关的元数据,例如参数名称。下表描述了上下文中可用的项,以便您可以使用它们进行键和条件计算:
缓存 SpEL 可用元数据
Name | Location | 描述 | 示例 |
---|---|---|---|
methodName |
根对象 | 被调用的方法的名称 | #root.methodName |
method |
根对象 | 被调用的方法 | #root.method.name |
target |
根对象 | 被调用的目标对象 | #root.target |
targetClass |
根对象 | 被调用的目标对象的类 | #root.targetClass |
args |
根对象 | 用于调用目标的参数 (作为数组) | #root.args[0] |
caches |
根对象 | 当前方法的缓存的集合 | #root.caches[0].name |
Argument name | 计算上下文 | 任何方法参数的名称。如果名称不可用 (可能是因为没有调试信息) ,参数名称也可以在 #a<#arg> 下使用,#arg 代表参数索引 (从 0 开始) |
#iban 或 #a0 也可以使用 #p0 或 #p<#arg> 作为别名) |
result |
计算上下文 | 方法调用的结果 (要缓存的值) 。只可用于 unless 表达式,cache put 表达式 (计算 key ) ,或 cache evict 表达式 (当 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
变量) ,因为这些条件是事先验证过的,可以确认排除。
@CacheEvict
注解
缓存抽象不仅允许缓存存储的填充,还允许删除。此过程对于从缓存中删除陈旧或未使用的数据非常有用。与 @Cacheable
相反,@CacheEvict
区分了执行缓存回收的方法 (即作为从缓存中删除数据的触发器的方法) 。与其兄弟类似,@CacheEvict
需要指定一个或多个受操作影响的缓存,允许指定自定义缓存和键解析或条件,并具有一个额外的参数 ( allEntries
) ,用于指示是否需要执行缓存作用域的删除,而不仅仅是(基于键的)条目删除。下面的示例将清除 books
缓存中的所有条目:
@CacheEvict(cacheNames="books", allEntries=true) // 使用 allEntries 属性从缓存中删除所有条目
public void loadBooks(InputStream batch)
当需要清除整个缓存区域时,此选项非常方便。如前面的示例所示,所有条目都在一个操作中被删除,而不是逐个删除条目 (这将花费很长时间,因为它效率低下) 。注意,框架忽略此场景中指定的任何键,因为它不适用 (整个缓存被删除,而不仅仅是一个条目) 。
还可以使用 beforeInvocation
属性指示是在之后 (默认) 还是在调用方法之前执行删除操作。前者提供了与其他注解相同的语义: 一旦方法成功完成,就会运行缓存上的一个操作(在本例中为 eviction )。如果该方法不运行 (因为它可能被缓存) 或者抛出异常,则不会发生删除。后者 ( 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
- 操作层面
启用缓存注解
需要注意的是,即使声明缓存注解并不会自动触发它们的操作,该特性必须以声明方式启用 (这意味着如果您怀疑缓存是罪魁祸首,您可以通过删除一行配置行而不是代码中的所有注解来禁用它) 。
为了启用缓存注解,在 @Configuration
类中添加注解 @EnableCaching
:
@Configuration
@EnableCaching
public class AppConfig {
}
或者,对于 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/>
</beans>
无论是 cache:annotation-driven
标签还是 @EnableCaching
注解,都允许您指定各种选项,这些选项可以影响通过 AOP 将缓存行为添加到应用程序的方式。该配置与 @Transactional
的配置有意地相似。
处理缓存注解的默认通知模式是
proxy
,它只允许通过代理拦截调用。同一个类中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑结合编译时编织或加载时编织切换到aspectj
模式。
有关实现
CachingConfigurer
所需的高级自定义 (使用 Java 配置) 的详细信息,请参阅 javadoc
缓存注解设置
XML 属性 | 注解属性 | 默认值 | 描述 |
---|---|---|---|
cache-manager |
N/A 参见 CachingConfigurer |
cacheManager |
要使用的缓存管理器的名称。默认的 CacheResolver 通过这个缓存管理器在幕后初始化 (如果没有设置,则初始化 CacheResolver ) 。对于更细粒度的缓存解析管理,可以考虑设置 cache-resolver 属性 |
cache-resolver |
N/A 参见 CachingConfigurer |
使用配置的 cacheManager 创建一个 SimpleCacheResolver |
用于解析底层缓存的 CacheResolver 的 bean 名称。此属性不是必需的,只需指定为 cache-manager 属性的替代品。 |
key-generator |
N/A 参见 CachingConfigurer |
SimpleKeyGenerator |
要使用的自定义键生成器的名称 |
error-handler |
N/A 参见 CachingConfigurer |
SimpleCacheErrorHandler |
要使用的自定义缓存错误处理器的名称。默认情况下,在缓存相关操作期间引发的任何异常都会被抛回到客户端 |
mode |
mode |
proxy |
默认模式 ( proxy ) 通过使用 Spring 的 AOP 框架注解要代理的 bean (遵循前面讨论过的代理语义,仅适用于通过代理进入的方法调用)。而另一种模式 ( aspectj ) 则将受影响的类与 Spring 的 AspectJ 缓存切面组织在一起,修改目标类的字节码以适用于任何类型的方法调用。AspectJ 编织需要在类路径中使用 spring-aspects.jar 以及启用加载时编织 (或编译时编织) 。(有关如何设置加载时编织的详细信息,请参阅 Spring 配置 ) |
proxy-target-class |
proxyTargetClass |
false |
仅应用于代理 ( proxy ) 模式。控制为带有 @Cacheable 或 @CacheEvict 注解的类创建哪种类型的缓存代理。如果 proxy-target-class 属性设置为 true ,则创建基于类的代理。如果 proxy-target-class 为 false ,或者属性被省略,则创建标准的基于 JDK 接口的代理。(参见 代理机制 对不同代理类型的详细检查) |
order |
order |
Ordered.LOWEST_PRECEDENCE |
定义应用于带有 @Cacheable 或 @CacheEvict 注解的 bean 的缓存通知的顺序 (有关排序 AOP 通知的相关规则的详细信息,请参阅 通知排序 ) 。没有指定顺序意味着 AOP 子系统决定通知的顺序 |
<cache:annotation-driven/>
查找@Cacheable/@CachePut/@CacheEvict/@Caching
只在定义它的相同应用上下文中的 bean 上进行缓存。这意味着,如果将<cache:annotation-driven/>
放在针对DispatcherServlet
的WebApplicationContext
中,它只检查控制器中的 bean ,而不检查服务层。有关更多信息,请参见 MVC 部分
方法可见性和缓存注解
当使用代理时,应该只对具有
public
可见性的方法应用缓存注解。如果使用这些注解对受保护的、私有的或包可见的方法进行注解,不会引发错误,但是注解的方法不会表现配置的缓存设置。如果您需要注解非公共方法,因为它改变了字节码本身,那么可以考虑使用 AspectJ
Spring 推荐只使用
@Cache*
注解具体类 (以及具体类的方法) ,而不是注解接口。当然可以将@Cache*
注解放在接口 (或接口方法) 上,但是如果使用基于接口的代理,这只能按照预期的方式工作。Java 注解不能从接口继承这一事实意味着,如果使用基于类的代理 (proxy-target-class="true"
) 或基于编织的切面 (mode="aspectj"
) ,代理和编织基础设施不能识别缓存设置,并且对象不会包装在代理中
在代理模式 (默认模式) 中,只有通过代理进入的外部方法调用才会被拦截。这意味着自调用 (实际上,目标对象的一个方法调用目标对象中的另一个方法) 不会在运行时导致实际的缓存,即使调用的方法标记为
@Cacheable
。考虑在这种情况下使用aspectj
模式。此外,代理必须完全初始化以提供预期的行为,因此在初始化代码中不应该依赖该特性 (即@PostConstruct
) 。
使用自定义注解
自定义注解和 AspectJ
这个特性只适用于基于代理的方法,但是可以通过使用 AspectJ 进行一些额外的工作来启用。
spring-aspects
模块仅为标准注解定义了一个切面。如果您已经定义了自己的注解,那么还需要为它们定义一个面。示例:AnnotationCacheAspect
缓存抽象允许您使用自己的注解来标识哪些方法触发缓存填充或删除。作为模板机制,这非常方便,因为它消除了重复缓存注解声明的需要,如果指定了键或条件,或者在代码库中不允许外部导入 ( 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 注解,但是容器会在运行时自动选择其声明并理解其含义。注意,如前所述,需要启用注解驱动的行为。
JCache (JSR-107) 注解
从版本 4.1 开始,Spring 的缓存抽象完全支持 JCache 标准注解 : @CacheResult
, @CachePut
, @CacheRemove
, @CacheRemoveAll
以及 @CacheDefaults
, @CacheKey
, @CacheValue
。即使不将缓存存储迁移到 JSR-107,也可以使用这些注解。内部实现使用 Spring 的缓存抽象,并提供符合规范的默认 CacheResolver
和 KeyGenerator
实现。换句话说,如果您已经在使用 Spring 的缓存抽象,那么您可以切换到这些标准注解,而无需更改您的缓存存储 (或配置) 。
特性摘要
对于那些熟悉 Spring 的缓存注解的人,下表描述了 Spring 注解和 JSR-107 注解之间的主要区别:
Spring | JSR-107 | 描述 |
---|---|---|
@Cacheable |
@CacheResult |
非常相似。@CacheResult 可以缓存特定的异常,并强制执行方法,而不管缓存的内容是什么 |
@CachePut |
@CachePut |
当 Spring 用方法调用的结果更新缓存时,JCache 要求将其作为一个参数传递给它,并用 @CacheValue 注解。由于这种差异,JCache 允许在实际方法调用之前或之后更新缓存 |
@CacheEvict |
@CacheRemove |
非常相似。当方法调用导致异常时,@CacheRemove 支持条件删除 |
@CacheEvict(allEntries=true) |
@CacheRemoveAll |
参见 @CacheRemove |
@CacheConfig |
@CacheDefaults |
让您以类似的方式配置相同的概念 |
JCache 具有 javax.cache.annotation.CacheResolver
的概念,它与 Spring 的 CacheResolver
接口相同,只是 JCache 只支持一个缓存。默认情况下,简单实现根据注解上声明的名称检索要使用的缓存。应该注意的是,如果没有在注解上指定缓存名称,则会自动生成默认值。有关更多信息,请参见 @CacheResult#cacheName()
的 javadoc
CacheResolver
实例由 CacheResolverFactory
检索。可以为每个缓存操作定制工厂,如下面的示例所示:
@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class)
public Book findBook(ISBN isbn)
对于所有引用的类,Spring 尝试定位具有给定类型的 bean 。如果存在多个匹配,则创建一个新实例,并可以使用常规的 bean 生命周期回调,例如依赖注入。
键由 javax.cache.annotation.CacheKeyGenerator
生成。与 Spring 的 KeyGenerator
用途相同的 CacheKeyGenerator
。默认情况下,将考虑所有方法参数,除非至少有一个参数带有 @CacheKey
注解。这类似于 Spring 的自定义键生成声明。例如,下面是相同的操作,一个使用 Spring 的抽象,另一个使用 JCache :
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@CacheResult(cacheName="books")
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed)
还可以在操作上指定 CacheKeyResolver
,类似于指定 CacheResolverFactory
。
JCache 可以管理由带注解的方法引发的异常。这可以防止缓存的更新,但是它也可以缓存异常作为失败的指示器,而不是再次调用该方法。假设如果 ISBN 的结构无效,则抛出 InvalidIsbnNotFoundException
。这是一个永久性的失败 (没有一本书可以通过这样的参数检索到) 。下面缓存异常,以便将来使用相同的,无效的,ISBN 调用时直接抛出缓存的异常,而不是再次调用方法:
@CacheResult(cacheName="books", exceptionCacheName="failures"
cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)
启用 JSR-107 支持
您不需要特定地启用 JSR-107 支持和 Spring 的声明式注解支持。如果类路径中同时存在 JSR-107 API 和 Spring 上下文支持模块,那么@EnableCaching
和 cache:annotation-driven
标签都会自动启用 JCache 支持。
根据您的情况,由您自己选择。您甚至可以通过在某些服务上使用 JSR-107 API,而在其他服务上使用 Spring 自己的注解来混合和匹配服务。但是,如果这些服务影响相同的缓存,您应该使用一致的、相同的键生成实现。
基于 XML 的声明式缓存
如果不能使用注解 (可能是因为无法访问源代码或没有外部代码) ,则可以使用 XML 进行声明式缓存。因此,您可以在外部指定目标方法和缓存指令 (类似于声明式事务管理通知) ,而不是对缓存方法进行注解。上一节的例子可以翻译成下面的例子:
<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
<cache:caching cache="books">
<cache:cacheable method="findBook" key="#isbn"/>
<cache:cache-evict method="loadBooks" all-entries="true"/>
</cache:caching>
</cache:advice>
<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
<!-- cache manager definition omitted -->
在前面的配置中,可缓存 bookService
。要应用的缓存语义被封装在 cache:advice
定义中,这导致使用 findBooks
方法将数据放入缓存,使用 loadBooks
方法将数据清除。这两个定义都对 books
缓存起作用。
aop:config
定义通过使用 AspectJ 切点表达式将缓存通知应用于程序中的适当连接点。在前面的示例中,将考虑 BookService
中的所有方法,并将缓存通知应用于这些方法。
声明式 XML 缓存支持所有基于注解的模型,因此在两者之间切换应该相当容易。此外,两者都可以在同一个应用程序中使用。基于 XML 的方法不涉及目标代码。然而,它本质上更加冗长。在处理具有针对缓存的重载方法的类时,确定合适的方法需要额外的工作,因为方法参数不是一个好的区分器。在这些情况下,您可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。然而,通过 XML,应用包或组或接口范围的缓存 (同样由于 AspectJ 切入点) 和创建类似模板的定义 (正如我们在前面的示例中通过 cache:definitions
cache
属性定义目标缓存所做的那样) 更加容易。
配置 Cache Storage
缓存抽象提供了几个存储集成选项。要使用它们,您需要声明一个适当的 CacheManager
(一个控制和管理 Cache
实例的实体,可以用它来检索这些实例以便存储) 。
基于 JDK ConcurrentMap
的缓存
基于 JDK 的 Cache
实现在 org.springframework.cache.concurrent
包下。它允许您使用 ConcurrentHashMap
作为底层缓存。下面的示例演示如何配置两个缓存:
<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
</set>
</property>
</bean>
前面的代码片段使用 SimpleCacheManager
为两个名为 default
和 books
的嵌套 ConcurrentMapCache
实例创建 CacheManager
。请注意,名称是直接为每个缓存配置的。
由于缓存是由应用程序创建的,因此它被绑定到其生命周期,使其适合于基本用例、测试或简单的应用程序。缓存可以很好地扩展并且非常快,但是它不提供任何管理、持久性能力或者删除契约。
基于 Ehcache 的 Cache
Ehcache 3.x 完全兼容 JSR-107,不需要专门的支持。
Ehcache 2.x 实现位于 org.springframework.cache.ehcache
包中。同样,要使用它,您需要声明适当的 CacheManager
。下面的例子说明了如何这样做:
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
<!-- EhCache library setup -->
<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
这个设置引导 Spring IoC 中的 ehcache 库 ( 通过 ehcache
bean ) ,然后将其连接到专用的 CacheManager
实现。请注意,整个 Ehcache 特定的配置是从 ehcache.xml
中读取的。
Caffeine Cache
Caffeine 是 Java 8 对 Guava 缓存的重写,它的实现位于 org.springframework.cache.caffeine
包中,并提供了对 Caffeine 几个特性的访问。
下面的例子配置了一个 CacheManager
,它可以根据需要创建缓存:
<bean id="cacheManager"
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
您还可以提供显式使用的缓存。在这种情况下,只有 manager 才能提供这些信息。下面的例子说明了如何这样做:
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="caches">
<set>
<value>default</value>
<value>books</value>
</set>
</property>
</bean>
Caffeine CacheManager
也支持定制的 Caffeine
和 CacheLoader
。参见文档
基于 GemFire 的缓存
GemFire 是一个面向内存的、磁盘支持的、弹性可伸缩的、连续可用的、活动的 (具有内置的基于模式的订阅通知) 、全球复制的数据库,并提供全功能的边缘缓存。有关如何将 GemFire 用作 CacheManager (以及更多) 的进一步信息,请参阅 Spring Data GemFire 参考文档
JSR-107 Cache
Spring 的缓存抽象还可以使用符合 JSR-107 的缓存。JCache 实现位于 org.springframework.cache.jcache
包
同样,要使用它,您需要声明适当的 CacheManager
:
<bean id="cacheManager"
class="org.springframework.cache.jcache.JCacheCacheManager"
p:cache-manager-ref="jCacheManager"/>
<!-- JSR-107 cache manager setup -->
<bean id="jCacheManager" .../>
处理没有底层存储的缓存
有时,在切换环境或进行测试时,您可能在没有配置实际的底层缓存的情况下就拥有了缓存声明。因为这是一个无效的配置,所以在运行时会抛出异常,因为缓存基础设施无法找到合适的存储。在这种情况下,您可以将缓存声明连接到一个简单的虚拟缓存中,不执行缓存,也就是说,它强制每次调用缓存的方法。下面的例子说明了如何这样做:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="jdkCache"/>
<ref bean="gemfireCache"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
前面的链中的 CompositeCacheManager
有多个 CacheManager
实例,并通过 fallbackToNoOpCache
标志为配置的缓存管理器未处理的所有定义添加一个无操作缓存。也就是说,在 jdkCache
或 gemfireCache
(本例前面配置的) 中没有找到的每个缓存定义都由 no-op 缓存处理,该缓存不存储任何信息,导致每次调用目标方法。
插入不同的底层缓存
显然,有很多缓存产品可以用作底层存储。要插入它们,您需要提供一个 CacheManager
和一个 Cache
实现,因为不幸的是,没有可用的标准可以替代。这听起来可能比实际上要难,因为在实践中,类往往是简单的 适配器 ,它们将缓存抽象框架映射到存储 API 之上,就像 ehcache
类一样。大多数 CacheManager
类都可以使用 org.springframework.cache.support
包中的类 (比如 AbstractCacheManager
,它负责处理模板代码,只完成实际的映射) 。我们希望,随着时间的推移,提供与 Spring 集成的库能够填补这一小块配置空白。
如何设定 TTL/TTI/Eviction 政策/XXX 功能?
直接通过您的缓存供应商。缓存抽象是一个抽象,而不是一个缓存实现。您使用的解决方案可能支持其他解决方案不支持的各种数据策略和不同的拓扑 (例如,JDK ConcurrentHashMap ,在缓存抽象中暴露这一点是没有用的,因为没有底层支持) 。这样的功能应该通过底层缓存 (在配置它时) 或者通过它的本地 API 来直接控制。
参考
简单使用缓存
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private ISBN isbn;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ISBN {
private Long id;
}
public class BookStore {
private static List<Book> books = new ArrayList<>();
static {
books.add(new Book(new ISBN(1L)));
books.add(new Book(new ISBN(2L)));
books.add(new Book(new ISBN(3L)));
}
@Cacheable("books")
public Book findBook(ISBN isbn) {
System.out.println("findBook :: " + isbn);
for (Book book : books) {
if (Objects.equals(book.getIsbn().getId(), isbn.getId())) {
return book;
}
}
return null;
}
}
@Configuration
@EnableCaching
@Import(BookStore.class)
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
simpleCacheManager.setCaches(Collections.singleton(concurrentMapCache()));
return simpleCacheManager;
}
@Bean
public ConcurrentMapCache concurrentMapCache() {
ConcurrentMapCacheFactoryBean concurrentMapCacheFactoryBean = new ConcurrentMapCacheFactoryBean();
concurrentMapCacheFactoryBean.setName("books");
concurrentMapCacheFactoryBean.afterPropertiesSet();
return concurrentMapCacheFactoryBean.getObject();
}
}
public class Main {
/**
* 测试缓存功能
*/
@Test
public void test1() {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(CacheConfig.class);
BookStore bookStore = applicationContext.getBean(BookStore.class);
Book book = bookStore.findBook(new ISBN(1L));
System.out.println("1 :: " + book);
book = bookStore.findBook(new ISBN(1L));
System.out.println("2 :: " + book);
book = bookStore.findBook(new ISBN(2L));
System.out.println("3 :: " + book);
/*
输出:
findBook :: ISBN(id=1)
1 :: Book(isbn=ISBN(id=1))
2 :: Book(isbn=ISBN(id=1))
findBook :: ISBN(id=2)
3 :: Book(isbn=ISBN(id=2))
*/
}
/**
* 测试缓存内部信息
*/
@Test
public void test2() {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(CacheConfig.class);
ConcurrentMapCache cache = applicationContext.getBean(ConcurrentMapCache.class);
BookStore bookStore = applicationContext.getBean(BookStore.class);
Book book = bookStore.findBook(new ISBN(1L));
System.out.println(JSONUtil.toJsonPrettyStr(cache));
System.out.println(cache.getNativeCache());
/*
输出:
findBook :: ISBN(id=1)
{
"allowNullValues": true,
"name": "books"
}
{ISBN(id=1)=Book(isbn=ISBN(id=1))}
*/
}
}