36. 缓存抽象

36.1 简介

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

从Spring 4.1开始,在 JSR-107 annotations 和更多自定义选项的支持下,缓存抽象得到了显着改进。

36.2 了解缓存抽象


Cache vs Buffer

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

另一方面,通过定义缓存是隐藏的,并且任何一方都不知道缓存发生。它也改善了性能,但是通过允许以快速方式多次读取相同数据来实现这一点。

可以找到两者之间差异的进一步解释 here


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

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

其他与缓存相关的操作由抽象提供,例如更新缓存内容或删除所有条目之一的能力。如果缓存处理可在应用程序过程中发生变化的数据,则这些非常有用。

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

这个抽象有 a few implementations 开箱即用:基于JDK java.util.concurrent.ConcurrentMap 的缓存, Ehcache 2.x ,Gemfire缓存, CaffeineGuava caches 和JSR-107兼容缓存(例如Ehcache 3.x)。有关插入其他缓存存储/提供程序的更多信息,请参阅 Section 36.7, “Plugging-in different back-end caches”

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

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

缓存特定项目直接等同于通过编程缓存交互找到的典型的get-if-not-found-then-proceed-put-finally代码块:没有应用锁定,并且多个线程可能尝试加载相同的项目同时。这同样适用于驱逐:如果多个线程试图同时更新或驱逐数据,则可以使用陈旧数据。某些缓存提供程序提供该区域的高级功能,请参阅您正在使用的缓存提供程序的文档以获取更多详细信息。

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

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

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

36.3 基于声明注释的缓存

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

  • @Cacheable 触发缓存填充

  • @CacheEvict 触发缓存驱逐

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

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

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

让我们仔细看看每个注释:

36.3.1 @Cacheable注释

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

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

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

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

@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 来选择感兴趣的参数(或其嵌套属性),执行操作甚至调用任意方法,而无需编写任何代码或实现任何接口。这是 default generator 的推荐方法,因为随着代码库的增长,签名方法往往会有很大不同;虽然默认策略可能适用于某些方法,但它很少适用于所有方法。

下面是各种SpEL声明的一些示例 - 如果您不熟悉它,请自己帮忙并阅读 Chapter 10, Spring Expression Language (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) {...}

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

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

从Spring 4.1开始,缓存注释的value属性不再是必需的,因为无论注释的内容如何,CacheResolver都可以提供此特定信息。与key和keyGenerator类似,cacheManager和cacheResolver参数是互斥的,并且指定这两者的操作将导致异常,因为CacheResolver实现将忽略自定义CacheManager。这可能不是你的意思期望。

同步缓存

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

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

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

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

条件缓存

有时,方法可能不适合一直缓存(例如,它可能取决于给定的参数)。缓存注释通过 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 。除了构建参数之外,框架还提供专用的缓存相关元数据,例如参数名称。下一个表列出了可用于上下文的项目,因此可以将它们用于键和条件计算:

Table 36.1. Cache SpEL available metadata

名称位置说明示例
methodNameroot对象正在调用的方法的名称#root.methodName
方法根对象正在调用的方法#root.method.name
targetroot对象正在调用的目标对象#root.target
targetClassroot object被调用的目标的类#root.targetClass
argsroot object用于调用目标的参数(作为数组)#root.args[0]
cachesroot对象执行当前方法的高速缓存集合#root.caches[0].name
参数名称评估上下文任何方法参数的名称。如果由于某种原因名称不可用(例如没有调试信息),则参数名称也可在 #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)

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

36.3.3 @CacheEvict注释

缓存抽象不仅允许缓存存储的填充,还允许驱逐。此过程对于从缓存中删除陈旧或未使用的数据非常有用。与 @Cacheable 相反,注释 @CacheEvict 划分了执行缓存逐出的方法,即用作从缓存中删除数据的触发器的方法。就像它的兄弟一样, @CacheEvict 需要指定受操作影响的一个(或多个)缓存,允许指定自定义缓存和密钥解析或条件,但此外,还有一个额外的参数 allEntries ,它指示是否在缓存范围内需要执行驱逐而不仅仅是一个输入(基于密钥):

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

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

还可以指示驱逐是在(默认)之后还是在方法通过 beforeInvocation 属性执行之前发生的。前者提供与其他注释相同的语义 - 一旦方法成功完成,就会执行缓存上的操作(在本例中为逐出)。如果方法未执行(因为它可能被缓存)或抛出异常,则不会发生驱逐。在调用方法之前,后者( beforeInvocation=true )会导致驱逐始终发生 - 这在驱逐不需要与方法结果相关联的情况下非常有用。

重要的是要注意void方法可以与 @CacheEvict 一起使用 - 因为方法充当触发器,返回值被忽略(因为它们不与缓存交互) - 这不是 @Cacheable 的情况,它添加/更新数据进入缓存,因此需要一个结果。

36.3.4 @Caching注释

有些情况下需要指定相同类型的多个注释,例如 @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 。将此批注放在类上不会打开任何缓存操作。

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

  • 全局配置,可用于 CacheManagerKeyGenerator

  • 在 class ,使用 @CacheConfig

  • 在操作级别

36.3.6 启用缓存注释

重要的是要注意,即使声明缓存注释不会自动触发它们的操作 - 就像Spring中的许多内容一样,该功能必须以声明方式启用(这意味着如果您怀疑缓存是责任,您可以通过删除来禁用它只有一个配置行而不是代码中的所有注释。

要启用缓存注释,请将注释 @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 元素和 @EnableCaching 注释都允许指定各种选项,这些选项会影响通过AOP将缓存行为添加到应用程序的方式。该配置与 @Transactional 的配置有意类似:

处理缓存注释的默认建议模式是“proxy”,它允许仅通过代理拦截调用;同一类中的本地调用不能以这种方式截获。对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到“aspectj”模式。

使用Java配置的高级自定义需要实现CachingConfigurer:有关更多详细信息,请参阅javadoc。

Table 36.2. Cache annotation settings

XML属性注释属性默认说明
cache-manager不适用(请参阅 CachingConfigurer javadocs)cacheManager要使用的缓存管理器的名称。默认情况下 CacheResolver 将在此后面使用此缓存管理器进行初始化(或未设置“cacheManager”)。要获得更精细的缓存分辨率管理,请考虑设置“缓存解析器”属性。
cache-resolverN / A(参见 CachingConfigurer javadocs)A SimpleCacheResolver 使用配置的 cacheManager用于解析后备缓存的CacheResolver的bean名称。此属性不是必需的,只需要指定为“cache-manager”属性的替代。
key-generator不适用(请参阅 CachingConfigurer javadocs)SimpleKeyGenerator要使用的自定义密钥生成器的名称。
error-handlerN / A(请参阅 CachingConfigurer javadocs)SimpleCacheErrorHandler要使用的自定义缓存错误处理程序的名称。默认情况下,在缓存相关操作期间抛出的任何异常都会在客户端返回。
modemodeproxy默认模式 "proxy" 使用Spring的AOP框架处理要注释的带注释的bean(遵循代理语义,如上所述,仅适用于通过代理进入的方法调用)。替代模式 "aspectj" 用Spring的AspectJ缓存方面编织受影响的类,修改目标类字节代码以应用于任何类型的方法调用。 AspectJ编织需要在类路径中使用spring-aspects.jar以及启用加载时编织(或编译时编织)。 (有关如何设置加载时编织的详细信息,请参阅 the section called “Spring configuration” 。)
proxy-target-classproxyTargetClassfalse仅适用于代理模式。控制为使用 @Cacheable@CacheEvict 注释注释的类创建的缓存代理类型。如果 proxy-target-class 属性设置为 true ,则会创建基于类的代理。如果 proxy-target-classfalse 或者省略了该属性,则会创建基于标准JDK接口的代理。 (有关不同代理类型的详细检查,请参阅 Section 11.6, “Proxying mechanisms” 。)
orderorderOrdered.LOWEST_PRECEDENCE定义应用于使用 @Cacheable@CacheEvict 注释的bean的缓存建议的顺序。 (有关与AOP建议排序相关的规则的更多信息,请参阅 the section called “Advice ordering” 。)没有指定的排序意味着AOP子系统确定建议的顺序。

<cache:annotation-driven />仅查找@ Cacheable / @ CachePut / @ CacheEvict / @在其定义的同一应用程序上下文中的bean上的缓存。这意味着,如果你放入<cache:annotation-driven />在DispatcherServlet的WebApplicationContext中,它只检查控制器中的bean,而不检查您的服务。有关更多信息,请参见第22.2节“DispatcherServlet”。


Method visibility and cache annotations

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


Spring建议您只使用@Cache *注释来注释具体类(以及具体类的方法),而不是注释接口。您当然可以将@Cache *注释放在接口(或接口方法)上,但这只能在您使用基于接口的代理时按预期工作。 Java注释不是从接口继承的事实意味着如果您使用基于类的代理(proxy-target-class =“true”)或基于编织的方面(mode =“aspectj”),那么缓存设置是代理和编织基础设施无法识别,并且该对象不会被包装在缓存代理中,这将是非常糟糕的。

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

36.3.7 使用自定义注释


Custom annotation and AspectJ

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

spring-aspects 模块仅定义标准注释的方面。如果您已定义自己的注释,则还需要为那些人定义一个方面。检查 AnnotationCacheAspect 以获取示例。


缓存抽象允许您使用自己的注释来标识触发缓存填充或驱逐的方法。这作为模板机制非常方便,因为它消除了复制缓存注释声明的需要(如果指定了键或条件,则特别有用),或者在代码库中不允许外部导入( org.springframework )。与 stereotype 注释的其余部分类似, @Cacheable@CachePut@CacheEvict@CacheConfig 可以用作 meta-annotations ,这是可以注释其他注释的注释。也就是说,让我们用我们自己的自定义注释替换一个常见的 @Cacheable 声明:

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

上面,我们定义了自己的 SlowService 注释,它本身用_672756注释 - 现在我们可以替换下面的代码:

@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注释,但容器会在运行时自动获取其声明并理解其含义。请注意,如上所述 above ,需要启用注释驱动的行为。

36.4 JCache(JSR-107)注释

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

36.4.1 功能摘要

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

Table 36.3. Spring vs. JSR-107 caching annotations

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 相同。默认情况下,除非至少有一个参数使用 @CacheKey 注释,否则将考虑所有方法参数。这类似于Spring的 custom key generation declaration 。例如,这些是相同的操作,一个使用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)

36.4.2 启用JSR-107支持

除了Spring的声明性注释支持之外,没有什么特别需要实现JSR-107支持。 @EnableCachingcache:annotation-driven 元素都将自动启用如果JSR-107 API和 spring-context-support 模块都存在于类路径中,则支持JCache。

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

36.5 声明性的基于XML的缓存

如果注释不是一个选项(无法访问源代码或没有外部代码),则可以使用XML进行声明性缓存。因此,不是注释缓存方法,而是在外部指定目标方法和缓存指令(类似于声明性事务管理 advice )。前面的例子可以翻译成:

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

声明性XML缓存支持所有基于注释的模型,因此在两者之间移动应该相当容易 - 更多可以在同一个应用程序中使用。基于XML的方法不会触及目标代码,但它本身就更加冗长;当处理具有缓存目标的重载方法的类时,识别正确的方法确实需要额外的努力,因为 method 参数不是一个好的鉴别器 - 在这些情况下,AspectJ切入点可用于挑选目标方法并应用适当的缓存功能。但是通过XML,更容易应用包/组/接口范围的缓存(同样由于AspectJ切入点)并创建类似模板的定义(正如我们在上面的示例中通过 cache:definitions cache 定义目标缓存所做的那样)属性)。

36.6 配置缓存存储

开箱即用,缓存抽象提供了几种存储集成。要使用它们,需要简单地声明一个适当的 CacheManager - 一个控制和管理 Cache 的实体,并可用于检索这些存储。

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

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

36.6.2 基于Ehcache的缓存

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.xml 读取整个特定于ehcache的配置。

36.6.3 Caffeine 缓存

Caffeine是Java 8重写的Guava缓存,它的实现位于 org.springframework.cache.caffeine 包下,可以访问Caffeine的几个功能。

配置按需创建缓存的 CacheManager 非常简单:

<bean id="cacheManager"
        class="org.springframework.cache.caffeine.CaffeineCacheManager"/>

还可以提供明确使用的高速缓存。在这种情况下,只有经理可以提供:

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

Caffeine CacheManager 也支持海关 CaffeineCacheLoader 。有关这些内容的详细信息,请参阅 Caffeine documentation

36.6.4 Guava 缓存

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

配置按需创建缓存的 CacheManager 非常简单:

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

还可以提供明确使用的高速缓存。在这种情况下,只有经理可以提供:

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

Guava CacheManager 也支持海关 CacheBuilderCacheLoader 。有关这些内容的详细信息,请参阅 Guava documentation

36.6.5 基于GemFire的缓存

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

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 ,另外,通过 fallbackToNoOpCache 标志,为所有未由配置的缓存管理器处理的定义添加无操作缓存。也就是说,在 jdkCachegemfireCache (上面配置)中找不到的每个缓存定义都将由no op缓存处理,该缓存不会存储导致每次执行目标方法的任何信息。

36.7 插入不同的后端缓存

显然,有很多缓存产品可以用作后备存储。要插入它们,需要提供 CacheManagerCache 实现,因为遗憾的是我们没有可用的标准。这可能听起来比实际上更难,因为在实践中,类往往是简单的 adapter s,它将缓存抽象框架映射到存储API之上,就像 ehcache 类可以显示的那样。大多数 CacheManager 类可以使用 org.springframework.cache.support 包中的类,例如 AbstractCacheManager ,它处理样板代码,只留下实际的映射完成。我们希望及时提供与Spring集成的库可以填补这个小的配置差距。

36.8 如何设置TTL / TTI /逐出政策/ XXX功能?

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

Updated at: 5 months ago
35.6. 更多资源Table of contentVIII. 附录
Comment
You are not logged in.

There are no comments.