36. Cache Abstraction

36.1 Introduction

从 3.1 版开始,Spring Framework 提供了对将缓存透明添加到现有 Spring 应用程序中的支持。与transaction支持类似,缓存抽象允许一致使用各种缓存解决方案,而对代码的影响最小。

从 Spring 4.1 开始,通过支持JSR-107 annotations和更多自定义选项,显着改善了缓存抽象。

36.2 了解缓存抽象

Cache vs Buffer

术语“缓冲区”和“缓存”倾向于互换使用;请注意,但是它们代表不同的事物。传统上,缓冲区用作快速实体和慢速实体之间的数据的中间临时存储。由于一方必须await另一方影响性能,因此该缓冲区通过允许立即移动整个数据块而不是小块数据来缓解这种情况。数据只能从缓冲区写入和读取一次。此外,缓冲区对于至少一个知道的一方是“可见的”。

另一方面,根据定义,缓存是隐藏的,任何一方都不知道会发生缓存,它也可以提高性能,但是可以通过快速读取多次相同的数据来达到目的。

两者之间的差异的进一步说明可以找到here

从本质上讲,抽象将缓存应用于 Java 方法,从而基于缓存中可用的信息减少了执行次数。也就是说,每次调用* targeted *方法时,抽象将应用缓存行为,以检查该方法是否已针对给定参数执行。如果有,则返回缓存的结果,而不必执行实际的方法;如果还没有,则执行该方法,然后将结果缓存并返回给用户,以便下次调用该方法时,返回缓存的结果。这样,对于一组给定的参数,昂贵的方法(无论是绑定 CPU 还是 IO)只能执行一次,结果可以重复使用,而不必再次实际执行该方法。缓存逻辑是透明地应用的,不会对调用方造成任何干扰。

Tip

显然,这种方法仅适用于保证无论给定 Importing(或参数)执行多少次都返回相同输出(结果)的方法。

抽象提供了其他与缓存相关的操作,例如更新缓存内容或删除所有条目之一的能力。如果高速缓存处理在应用程序过程中可能更改的数据,则这些功能很有用。

就像 Spring Framework 中的其他服务一样,缓存服务是一种抽象(不是缓存实现),并且需要使用实际的存储来存储缓存数据-也就是说,这种抽象使开发人员不必编写缓存逻辑。但未提供实际的 Store。 org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口实现了这种抽象。

开箱即用的抽象有一些实现:基于 JDK java.util.concurrent.ConcurrentMap的缓存,Ehcache 2.x,Gemfire 缓存,CaffeineGuava caches和符合 JSR-107 的缓存(例如 Ehcache 3.x)。有关插入其他缓存存储区/提供程序的更多信息,请参见第 36.7 节“插入不同的后端缓存”

Tip

缓存抽象没有对多线程和多进程环境的特殊处理,因为此类功能由缓存实现来处理。 。

如果您具有多进程环境(即,在多个节点上部署的应用程序),则需要相应地配置缓存提供程序。根据您的用例,在几个节点上复制相同数据可能就足够了,但是如果您在应用程序过程中更改数据,则可能需要启用其他传播机制。

高速缓存特定项直接等同于通过程序化高速缓存交互找到的典型“如果找不到,然后 continue 进行处理并最终放入”代码块:没有应用锁,并且多个线程可能尝试加载同一项同时。逐出也是如此:如果多个线程试图同时更新或逐出数据,则可以使用陈旧的数据。某些缓存提供程序在该区域提供高级功能,有关更多详细信息,请参阅所用缓存提供程序的文档。

要使用缓存抽象,开发人员需要注意两个方面:

  • 缓存声明-确定需要缓存的方法及其策略

  • 缓存配置-数据存储和读取的后备缓存

36.3 基于声明式基于 Comments 的缓存

对于缓存声明,抽象提供了一组 Java 注解:

  • @Cacheable触发缓存填充

  • @CacheEvict触发逐出缓存

  • @CachePut更新缓存,而不会干扰方法的执行

  • @Caching重新组合要在一个方法上应用的多个缓存操作

  • @CacheConfig在类级别共享一些与缓存相关的常见设置

让我们仔细看看每个 Comments:

36.3.1 @Cacheable 注解

顾名思义,@Cacheable用于划分可缓存的方法-即将结果存储到缓存中的方法,以便在后续调用(具有相同参数)时返回缓存中的值,而不必实际执行该方法。以最简单的形式,Comments 声明需要与带 Comments 的方法关联的缓存名称:

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

在上面的代码段中,方法findBook与名为books的缓存关联。每次调用该方法时,都会检查缓存以查看调用是否已执行并且不必重复。虽然在大多数情况下,仅声明一个缓存,但 Comments 允许指定多个名称,以便使用多个缓存。在这种情况下,将在执行该方法之前检查每个缓存-如果命中了至少一个缓存,则将返回关联的值:

Note

即使未实际执行缓存的方法,所有其他不包含该值的缓存也会被更新。

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

默认密钥生成

由于缓存本质上是键值存储,因此每次调用缓存的方法都需要转换为适合缓存访问的键。开箱即用的缓存抽象基于以下算法使用简单的KeyGenerator

  • 如果没有给出参数,则返回SimpleKey.EMPTY

  • 如果仅给出一个参数,则返回该实例。

  • 如果给出了一个以上的参数,则返回一个包含所有参数的SimpleKey

这种方法适用于大多数用例。只要参数具有自然键并实现有效的hashCode()equals()方法。如果不是这种情况,则需要更改策略。

为了提供不同的* default *键生成器,需要实现org.springframework.cache.interceptor.KeyGenerator接口。

Note

随着 Spring 4.0 的发布,默认的密钥生成策略发生了变化。 Spring 的早期版本使用密钥生成策略,该策略对于多个密钥参数仅考虑参数hashCode()而不考虑equals();这可能会导致意外的按键冲突(有关背景,请参见SPR-10237)。对于这种情况,新的“ SimpleKeyGenerator”使用复合键。

如果要 continue 使用以前的密钥策略,则可以配置已弃用的org.springframework.cache.interceptor.DefaultKeyGenerator类或创建基于哈希的自定义“ KeyGenerator”实现。

自定义密钥生成声明

由于缓存是通用的,因此目标方法很可能具有无法简单地 Map 到缓存结构顶部的各种签名。当目标方法具有多个参数时,只有其中一些参数适合缓存(而其余参数仅由方法逻辑使用),这往往会变得很明显。例如:

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

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

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

以下是各种 SpEL 声明的一些示例-如果您不熟悉它,请帮忙并阅读第 10 章,Spring 表达语言(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)

Note

keykeyGenerator参数是互斥的,同时指定这两个参数的操作将导致异常。

默认缓存分辨率

开箱即用的缓存抽象使用一个简单的CacheResolver,它使用已配置的CacheManager检索在操作级别定义的缓存。

为了提供不同的* default *缓存解析器,需要实现org.springframework.cache.interceptor.CacheResolver接口。

自定义缓存分辨率

默认的缓存分辨率非常适合使用单个CacheManager并且没有复杂的缓存分辨率要求的应用程序。

对于使用多个缓存 Management 器的应用程序,可以将cacheManager设置为每个操作使用:

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

也可以用与key generation类似的方式完全替换CacheResolver。对于每个高速缓存操作都要求解决方案,从而使实现有机会根据运行时参数实际解析要使用的高速缓存:

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

Note

从 Spring 4.1 开始,缓存 Comments 的value属性不再是必需的,因为CacheResolver可以提供此特定信息,而不管 Comments 的内容如何。

keykeyGenerator相似,cacheManagercacheResolver参数是互斥的,并且同时指定两者的操作将导致异常,因为CacheResolver实现将忽略自定义CacheManager。这可能不是您所期望的。

Synchronized caching

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

对于那些特殊情况,sync属性可用于指示基础缓存提供程序在计算值时“锁定”缓存条目。结果,只有一个线程将忙于计算该值,而其他线程将被阻塞,直到在缓存中更新该条目为止。

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

Note

这是一项可选功能,您最喜欢的缓存库可能不支持它。核心框架提供的所有CacheManager实现都支持它。检查您的缓存提供程序的文档以获取更多详细信息。

Conditional caching

有时,一种方法可能并不总是适合缓存(例如,它可能取决于给定的参数)。高速缓存注解通过condition参数支持这种功能,该参数采用SpEL表达式,该表达式被评估为truefalse。如果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。除了内置参数外,该框架还提供了与缓存相关的专用元数据,例如参数名称。下表列出了可用于上下文的项,因此可以将其用于键和条件计算:

表 36.1. 缓存 SpEL 可用元数据

NameLocationDescriptionExample
methodNameroot object被调用方法的名称#root.methodName
methodroot object被调用的方法#root.method.name
targetroot object被调用的目标对象#root.target
targetClassroot object被调用目标的类#root.targetClass
argsroot object用于调用目标的参数(作为数组)#root.args[0]
cachesroot object执行当前方法所依据的缓存集合#root.caches[0].name
argument nameevaluation context任何方法参数的名称。如果由于某种原因名称不可用(例如,没有调试信息),则参数名称也可以在#a<#arg>下使用,其中*#arg *代表参数索引(从 0 开始)。#iban#a0(也可以使用#p0#p<#arg>表示法作为别名)。
resultevaluation context方法调用的结果(要缓存的值)。仅在unless个表达式,cache put个表达式(用于计算key)或cache evict个表达式(当beforeInvocationfalse时)中可用。对于受支持的包装器(例如Optional),#result是指实际对象,而不是包装器。#result

36.3.2 @CachePut 注解

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

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

Tip

请注意,通常不建议在同一方法上使用@CachePut@CacheableComments,因为它们具有不同的行为。后者导致通过使用缓存跳过方法执行,而前者则强制执行以便执行缓存更新。这会导致意外的行为,并且除了特定的极端情况(例如具有相互排斥条件的 Comments)外,应避免此类声明。还请注意,此类条件不应依赖于结果对象(即#result变量),因为这些条件已预先验证以确认排除。

36.3.3 @CacheEvict 注解

缓存抽象不仅允许缓存存储的填充,还允许逐出。此过程对于从缓存中删除陈旧或未使用的数据很有用。与@Cacheable相反,Comments@CacheEvict定义了执行缓存逐出的方法,即充当从缓存中删除数据的触发器的方法。与其同级一样,@CacheEvict需要指定一个(或多个)受操作影响的缓存,允许指定自定义缓存和键解析或条件,但另外还具有一个额外的参数allEntries,该参数指示是否在整个缓存范围内驱逐需要执行,而不仅仅是一项(基于密钥):

@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)

当需要清除整个缓存区域时,此选项非常有用-而不是逐出每个条目(这会花费很长时间,因为它效率低下),而所有条目都将在一次操作中被删除,如上所示。请注意,该框架将忽略在此方案中指定的任何键,因为它不适用(整个缓存不仅被逐出一个条目,而且被逐出)。

还可以通过beforeInvocation属性指示驱逐应该发生在(默认之后)还是在方法执行之前。前者提供与其余注解相同的语义-方法成功完成后,将对缓存执行操作(在这种情况下为逐出)。如果该方法未执行(可能已被缓存)或引发了异常,则不会发生逐出。后者(beforeInvocation=true)导致驱逐总是在调用方法之前发生-在不需要将驱逐与方法结果联系在一起的情况下,这很有用。

重要的是要注意 void 方法可以与@CacheEvict一起使用-由于该方法充当触发器,返回值将被忽略(因为它们不与缓存交互)-@Cacheable并非如此,后者添加/更新数据放入缓存,因此需要结果。

36.3.4 @缓存 Comments

在某些情况下,需要指定多个相同类型的 Comments,例如@CacheEvict@CachePut,例如,因为条件或键表达式在不同的缓存之间是不同的。 @Caching允许在同一方法上使用多个嵌套的@Cacheable@CachePut@CacheEvict

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

36.3.5 @CacheConfig 注解

到目前为止,我们已经看到缓存操作提供了许多自定义选项,并且可以基于操作进行设置。但是,如果某些自定义选项适用于该类的所有操作,则配置它们可能很繁琐。例如,指定要用于该类的每个高速缓存操作的高速缓存的名称可以由单个类级定义代替。这是@CacheConfig发挥作用的地方。

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

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

@CacheConfig是类级别的注解,它允许共享缓存名称,自定义KeyGenerator,自定义CacheManager以及最后的自定义CacheResolver。将此 Comments 放在类上不会打开任何缓存操作。

操作级别的自定义将始终覆盖在@CacheConfig上设置的自定义。因此,这为每个缓存操作提供了三个级别的自定义:

  • 全局配置,可用于CacheManagerKeyGenerator

  • 在 Class 上,使用@CacheConfig

  • 在运营层面

36.3.6 启用缓存 Comments

重要的是要注意,即使声明缓存 Comments 并不会自动触发它们的动作-就像 Spring 中的许多事情一样,必须声明性地启用该功能(这意味着如果您怀疑应该归因于缓存,则可以通过删除来禁用它仅一个配置行,而不是代码中的所有 Comments)。

要启用缓存 Comments,请将 Comments@EnableCaching添加到您的@Configuration类中:

@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 http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

        <cache:annotation-driven/>
</beans>

cache:annotation-driven元素和@EnableCachingComments 都允许指定各种选项,这些选项影响通过 AOP 将缓存行为添加到应用程序的方式。该配置故意与@Transactional相似:

Note

处理缓存 Comments 的默认建议模式是“代理”,它仅允许通过代理来拦截呼叫。同一类中的本地调用无法以这种方式被拦截。对于更高级的侦听模式,请考虑结合编译时或加载时编织切换到“ aspectj”模式。

Note

使用 Java 配置的高级自定义要求实现CachingConfigurer:请参考javadoc 以获得更多详细信息

表 36.2 缓存 Comments 设置

XML AttributeAnnotation AttributeDefaultDescription
cache-managerN/A(请参阅CachingConfigurer javadocs)cacheManager要使用的缓存 Management 器的名称。默认的CacheResolver将在后台使用此缓存 Management 器(或未设置的``)初始化。为了更精细地 Management 缓存分辨率,请考虑设置“ cache-resolver”属性。
cache-resolverN/A(请参阅CachingConfigurer javadocs)使用已配置的cacheManagerSimpleCacheResolver用来解析后备缓存的 CacheResolver 的 bean 名称。此属性不是必需的,仅需要指定为“ cache-manager”属性的替代方法。
key-generatorN/A(请参阅CachingConfigurer javadocs)SimpleKeyGenerator要使用的定制密钥生成器的名称。
error-handlerN/A(请参阅CachingConfigurer javadocs)SimpleCacheErrorHandler要使用的自定义缓存错误处理程序的名称。默认情况下,与缓存相关的操作期间抛出的所有异常都将返回给 Client 端。
modemodeproxy默认模式“代理”使用 Spring 的 AOP 框架处理要 Comments 的 Bean(遵循如上所述的代理语义,仅适用于通过代理传入的方法调用)。替代模式“ aspectj”改为使用 Spring 的 AspectJ 缓存方面编织受影响的类,修改目标类字节码以应用于任何类型的方法调用。 AspectJ 编织需要在 Classpath 中使用 spring-aspects.jar 并启用加载时编织(或编译时编织)。 (有关如何设置加载时编织的详细信息,请参见称为“ Spring 配置”的部分。)
proxy-target-classproxyTargetClassfalse仅适用于代理模式。控制为使用@Cacheable@CacheEvictCommentsComments 的类创建哪种类型的缓存代理。如果proxy-target-class属性设置为true,则将创建基于类的代理。如果proxy-target-classfalse或省略了属性,则将创建基于标准 JDK 接口的代理。 (有关不同代理类型的详细检查,请参见第 11.6 节“代理机制”。)
orderorderOrdered.LOWEST_PRECEDENCE定义应用于带@Cacheable@CacheEvictComments 的 bean 的缓存建议的 Sequences。 (有关与 AOP 建议的排序有关的规则的更多信息,请参见“建议 Order”部分。)没有指定的排序意味着 AOP 子系统确定建议的 Sequences。

Note

<cache:annotation-driven/>仅在定义了该应用程序的上下文中在 Bean 上查找@Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,如果将<cache:annotation-driven/>放在WebApplicationContext中表示DispatcherServlet,则它仅检查控制器中的 Bean,而不检查服务中的 Bean。有关更多信息,请参见第 22.2 节“ DispatcherServlet”

Method visibility and cache annotations

使用代理时,应仅将缓存 Comments 应用于具有* public *可见性的方法。如果使用这些 Comments 对受保护的,私有的或程序包可见的方法进行 Comments,则不会引发任何错误,但是带 Comments 的方法不会显示已配置的缓存设置。如果您需要 Comments 非公共方法,请考虑使用 AspectJ(请参见下文),因为它会更改字节码本身。

Tip

Spring 建议您仅使用@Cache*Comments 对具体类(以及具体类的方法)进行 Comments,而不是对接口进行 Comments。您当然可以在接口(或接口方法)上放置@Cache*注解,但这仅在您使用基于接口的代理时才可以使用。 JavaComments 不是从接口继承的事实意味着,如果您使用的是基于类的代理(proxy-target-class="true")或基于编织的方面(mode="aspectj"),那么代理和编织基础结构将无法识别缓存设置,并且该对象将不会包装在缓存代理中,后者肯定是* bad *。

Note

在代理模式(默认设置)下,仅拦截通过代理传入的外部方法调用。这意味着自调用实际上是目标对象中的一种方法,该方法调用了目标对象的另一种方法,即使被调用的方法标记为@Cacheable,也不会导致运行时实际缓存-考虑到在这种情况下使用 aspectj 模式案件。另外,必须完全初始化代理以提供预期的行为,因此您不应在初始化代码(即@PostConstruct)中依赖此功能。

36.3.7 使用自定义 Comments

Custom annotation and AspectJ

此功能仅可与基于代理的方法一起使用,但可以使用 AspectJ 进行一些额外的工作才能启用。

spring-aspects模块仅为标准 Comments 定义一个方面。如果定义了自己的 Comments,则还需要为其定义一个方面。查看AnnotationCacheAspect为例。

缓存抽象使您可以使用自己的 Comments 来标识触发缓存填充或逐出的方法。作为模板机制,这非常方便,因为它避免了重复缓存 Comments 声明的重复(如果指定了键或条件,则特别有用),或者在代码库中不允许外部导入(org.springframework)。类似于其余的stereotypeComments,@Cacheable@CachePut@CacheEvict@CacheConfig可用作meta-annotations,即可以 Comments 其他 Comments 的 Comments。让我们用我们自己的自定义 Comments 替换通用的@Cacheable声明:

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

上面,我们定义了自己的SlowServiceComments,该 Comments 本身用@CacheableComments-现在我们可以替换以下代码:

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

with:

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

即使@SlowService不是 SpringComments,容器也会在运行时自动获取其声明并理解其含义。请注意,如above所述,需要启用 Comments 驱动的行为。

36.4 JCache(JSR-107)注解

从 Spring Framework 4.1 开始,缓存抽象完全支持 JCache 标准 Comments:它们是@CacheResult@CachePut@CacheRemove@CacheRemoveAll以及@CacheDefaults@CacheKey@CacheValue随播。这些 Comments 可以直接使用,而无需将缓存存储迁移到 JSR-107:内部实现使用 Spring 的缓存抽象,并提供符合规范的默认CacheResolverKeyGenerator实现。换句话说,如果您已经在使用 Spring 的缓存抽象,则可以切换到这些标准 Comments,而无需更改缓存存储(或配置)。

36.4.1 功能摘要

对于那些熟悉 Spring 缓存 Comments 的人,下表描述了 SpringComments 与 JSR-107 副本之间的主要区别:

表 36.3 Spring 与 JSR-107 缓存 Comments

SpringJSR-107Remark
@Cacheable@CacheResult相当相似。 @CacheResult可以缓存特定的异常并强制执行该方法,而不管缓存的内容如何。
@CachePut@CachePut当 Spring 使用方法调用的结果更新缓存时,JCache 要求将其作为带有@CacheValueComments 的参数传递。由于存在这种差异,JCache 允许在实际方法调用之前或之后更新缓存。
@CacheEvict@CacheRemove相当相似。 @CacheRemove支持有条件的逐出,以防方法调用导致异常。
@CacheEvict(allEntries=true)@CacheRemoveAll参见@CacheRemove
@CacheConfig@CacheDefaults允许以类似的方式配置相同的概念。

JCache 的概念javax.cache.annotation.CacheResolver与 Spring 的CacheResolver接口相同,只是 JCache 仅支持单个缓存。默认情况下,一个简单的实现根据 Comments 中声明的名称检索要使用的缓存。应该注意的是,如果 Comments 中未指定缓存名称,则会自动生成默认名称,请查看@CacheResult#cacheName()的 javadoc 以获取更多信息。

CacheResolver实例由CacheResolverFactory检索。可以为每个缓存操作自定义工厂:

@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class)
public Book findBook(ISBN isbn)

Note

对于所有引用的* classes *,Spring 尝试查找具有给定类型的 bean。如果存在多个匹配项,那么将创建一个新实例,并可以使用常规 bean 生命周期回调(如依赖项注入)。

密钥是由javax.cache.annotation.CacheKeyGenerator生成的,其目的与 Spring 的KeyGenerator相同。默认情况下,将考虑所有方法参数,除非至少一个参数用@CacheKeyComments。这类似于 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 可以 Management 带 Comments 的方法引发的异常:这可以防止缓存的更新,但是它也可以缓存异常以指示失败,而无需再次调用该方法。假设如果 ISBN 的结构无效,则抛出InvalidIsbnNotFoundException。这是一个永久性的失败,无法使用该参数检索任何书籍。以下内容缓存了该异常,以便使用相同的无效 ISBN 进行的进一步调用直接引发该缓存的异常,而不是再次调用该方法。

@CacheResult(cacheName="books", exceptionCacheName="failures"
            cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)

36.4.2 启用 JSR-107 支持

除了启用 Spring 的声明性 Comments 支持外,无需执行任何特定操作即可启用 JSR-107 支持。如果 Classpath 中同时存在 JSR-107 API 和spring-context-support模块,则@EnableCachingcache:annotation-driven元素都将自动启用 JCache 支持。

Note

根据您的用例,选择基本上是您的选择。您甚至可以使用 JSR-107 API 混合和匹配服务,以及使用 Spring 自己的 Comments 的其他服务。但是请注意,如果这些服务影响相同的缓存,则应使用一致且相同的密钥生成实现。

36.5 基于声明式 XML 的缓存

如果 Comments 不是一种选择(无法访问源代码或没有外部代码),则可以使用 XML 进行声明式缓存。因此,无需在外部 Comments 缓存方法,而是在外部指定目标方法和缓存指令(类似于声明式事务 Managementadvice)。前面的示例可以转换为:

<!-- 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 切入点表达式将缓存建议应用于程序中的适当点(更多信息可在第 11 章,使用 Spring 进行面向方面的编程中获得)。在上面的示例中,考虑了BookService中的所有方法,并将缓存建议应用于它们。

声明式 XML 缓存支持所有基于 Comments 的模型,因此在两者之间移动应该相当容易-而且两者都可以在同一应用程序中使用。基于 XML 的方法不会触及目标代码,但是它本质上比较冗长。当使用针对缓存的重载方法处理类时,确定合适的方法确实需要付出额外的努力,因为method参数不是很好的区分符-在这种情况下,AspectJ 切入点可用于挑选目标方法并应用适当的缓存功能。但是,通过 XML,更容易应用包/组/接口范围的缓存(同样由于 AspectJ 切入点)和创建类似模板的定义(如上例中通过cache:definitions cache定义目标缓存所做的那样)属性)。

36.6 配置缓存存储

开箱即用的缓存抽象提供了几种存储集成。要使用它们,只需声明一个适当的CacheManager-一个控制和 ManagementCache的实体,即可用于检索这些实体以进行存储。

36.6.1 基于 JDK ConcurrentMap 的缓存

基于 JDK 的Cache实现位于org.springframework.cache.concurrent包下。它允许使用ConcurrentHashMap作为后备Cache存储。

<!-- 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。请注意,名称是直接为每个缓存配置的。

由于缓存是由应用程序创建的,因此绑定到其生命周期,使其适合于基本用例,测试或简单应用程序。高速缓存可以很好地扩展,并且速度非常快,但是它不提供任何 Management 或持久性功能,也没有驱逐 Contract。

36.6.2 基于 Ehcache 的缓存

Note

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读取的。

36.6.3Caffeine 缓存

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>

CaffeineCacheManager还支持海关CaffeineCacheLoader。有关这些的更多信息,请参见Caffeine documentation

36.6.4 Guava 缓存

Guava 实现位于org.springframework.cache.guava软件包下,可访问 Guava 的多个功能。

配置一个按需创建缓存的CacheManager很简单:

<bean id="cacheManager"
      class="org.springframework.cache.guava.GuavaCacheManager"/>

也可以提供要显式使用的缓存。在这种情况下,Manager 只会提供那些:

<bean id="cacheManager" class="org.springframework.cache.guava.GuavaCacheManager">
    <property name="caches">
        <set>
            <value>default</value>
            <value>books</value>
        </set>
    </property>
</bean>

GuavaCacheManager还支持海关CacheBuilderCacheLoader。有关这些的更多信息,请参见Guava documentation

36.6.5 基于 GemFire 的缓存

GemFire 是面向内存/磁盘支持,弹性可伸缩,连续可用,活动(具有内置的基于模式的订阅通知),全局复制的数据库,并提供功能齐全的边缘缓存。有关如何将 GemFire 用作 CacheManager(以及更多)的更多信息,请参阅Spring Data GemFire 参考文档

36.6.6 JSR-107 缓存

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" .../>

36.6.7 在没有后备存储的情况下处理缓存

有时,在切换环境或进行测试时,可能会在未配置实际后备缓存的情况下声明缓存。由于这是无效的配置,因此在运行时将引发异常,因为缓存基础结构无法找到合适的存储。在这种情况下,与其删除缓存声明(可能会很乏味),不如将其连接到一个简单的虚拟缓存中,该缓存不执行缓存-即,强制每次执行缓存的方法:

<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 s,此外,还通过fallbackToNoOpCache标志添加了一个* no op *缓存,该缓存针对未配置的缓存 Management 器处理的所有定义。也就是说,在jdkCachegemfireCache(上述配置)中找不到的每个缓存定义将由 no op 缓存处理,该缓存不会存储任何导致每次执行目标方法的信息。

36.7 插入不同的后端缓存

显然,那里有很多可以用作后备存储的缓存产品。要插入它们,需要提供CacheManagerCache的实现,因为很遗憾,没有可用的标准可供我们使用。这听起来可能比实际上更难,因为实际上ehcache类可以显示,这些类通常是简单的adapter,它们将缓存抽象框架 Map 到存储 API 的顶部。大多数CacheManager类可以使用org.springframework.cache.support包中的类,例如AbstractCacheManager负责样板代码,仅保留实际的* mapping *即可完成。我们希望及时提供与 Spring 集成的库可以弥补这一小的配置空白。

36.8 如何设置 TTL/TTI /驱逐策略/ XXX 功能?

直接通过您的缓存提供程序。缓存抽象是……嗯,不是缓存实现。您正在使用的解决方案可能支持各种数据策略和不同的拓扑,而其他解决方案则不支持(例如 JDK ConcurrentHashMap)-暴露出缓存抽象是无用的,因为没有支持。此类功能应在配置时通过后备缓存或通过其本机 API 直接控制。