Spring 使用缓存
介绍
从Spring3.1版开始,Spring Framework提供了对现有Spring应用程序透明地添加缓存的支持。
与事务支持类似,缓存抽象允许一致地使用各种缓存解决方案,而减小对现有代码的影响。
从Spring 4.1开始,通过对JSR-107注释和更多自定义选项的支持,缓存抽象得到了显着改进。
从根本上说,抽象将缓存应用于Java方法,从而减少了基于缓存中可用信息的执行次数。也就是说,每次调用目标方法时,抽象都将应用缓存行为,检查该方法是否已针对给定参数执行。如果有,则返回缓存的结果而不必执行实际的方法;如果没有,则执行方法,将结果缓存并返回给用户,以便下次调用该方法时,返回缓存的结果。这样,昂贵的方法(无论是CPU还是IO绑定)只能对给定的一组参数执行一次,并且重用结果而不必再次实际执行该方法。缓存逻辑是透明应用的,不会对调用者造成任何干扰。显然,这种方法仅适用于保证为给定输入(或参数)返回相同输出(结果)的方法,无论它执行多少次。
与缓存相关的操作由抽象提供,例如更新缓存内容或删除所有条目之一的能力。如果缓存可以在应用程序执行过程中动态变更,是非常有用的。
像Spring Framework中的其他服务一样,缓存服务是一个抽象(不是缓存实现),需要使用实际存储来存储缓存数据,也就是说,抽象使开发人员不必编写缓存逻辑,但不提供实际存储。这种抽象由org.springframework.cache.Cache和org.springframework.cache.CacheManager接口实现。
这个抽象的一些实现是开箱即用的:JDK java.util.concurrent.ConcurrentMap based caches, Ehcache 2.x, Gemfire cache, Caffeine, Guava caches and JSR-107 compliant caches (e.g. Ehcache 3.x)。
需要注意,缓存抽象没有对多线程和多进程环境的特殊处理,因为这些功能由缓存实现处理。
如果您有多进程环境(即部署在多个节点上的应用程序),则需要相应地配置缓存提供程序。根据您的使用情况,多个节点上相同数据的副本可能已足够,但如果您在应用程序过程中更改数据,则可能需要启用其他传播机制。
缓存特定项目直接等同于通过编程缓存交互找到的典型的get-if-not-found-then-proceed-put-finally代码块。没有应用锁定,并且多个线程可能会尝试同时加载相同的项目。这同样适用于逐出缓存:如果多个线程试图同时更新或逐出数据,则可以使用陈旧数据。
要使用缓存抽象,开发人员需要注意两个方面:
- 缓存声明 - 确定需要缓存的方法及其策略
- 缓存配置 - 存储和读取数据的后备缓存
可用注解
- @Cacheable 触发缓存填充
- @CacheEvict 触发缓存驱逐
- @CachePut 更新缓存而不会干扰方法执行
- @Caching 重新组合要在方法上应用的多个缓存操作
- @CacheConfig 在类级别共享一些常见的缓存相关设置
@Cacheable 注解
使用该注解的方法,将会缓存返回结果,而不必后续调用时(使用相同的入参)再次执行。
在最简单的形式中,注释声明需要与注释方法关联的缓存的名称:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在上面的代码片段中,方法findBook与名为books的缓存相关联。每次调用该方法时,都会检查缓存以查看调用是否已经执行且不必重复。
虽然在大多数情况下,只声明了一个缓存,但注释允许指定多个名称,以便使用多个缓存。在这种情况下,将在执行方法之前检查每个缓存 - 如果至少有一个缓存被命中,则将返回关联的值:
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
即使没有实际执行缓存的方法,所有其他不包含数据的缓存也将更新。
默认的key生成器
由于缓存本质上是键值存储,因此缓存方法的每次调用需要生成适合缓存访问的key。默认的,缓存抽象使用基于以下算法的简单KeyGenerator:
- 如果没有给出参数,则返回SimpleKey.EMPTY。
- 如果只给出一个参数,则返回该实例。
- 如果给出了更多的参数,则返回包含所有参数的SimpleKey。
这种方法适用于大多数用例,只要参数具有自然键并实现有效的hashCode()和equals()方法。如果不是这样,则需要改变策略。
要提供不同的默认密钥生成器,需要实现org.springframework.cache.interceptor.KeyGenerator接口。
在Spring 4.0之后,默认key生成策略发生了变化。早期版本的Spring使用了key生成策略,对于多个关键参数,只考虑参数的hashCode()而不是equals();这可能会导致意外的键碰撞(参见SPR-10237的背景信息)。新的’SimpleKeyGenerator’在这种情况下使用复合键。
如果要继续使用以前的key策略,可以配置已弃用的org.springframework.cache.interceptor.DefaultKeyGenerator类或创建基于哈希的自定义“KeyGenerator”实现。
自定义key生成
由于缓存是通用的,因此目标方法很可能具有各种签名,这些签名无法简单地映射到缓存结构之上。当目标方法具有多个参数时,这往往变得明显,其中只有一些参数适合于缓存(而其余参数仅由方法逻辑使用)。例如:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然两个布尔参数影响了书的查询方式,但它们对缓存没有用处。如果两个中只有一个重要而另一个不重要,那么还有什么呢?
对于这种情况,@ Cacheable注释允许用户通过其key属性指定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)
上面的片段显示了选择某个参数,其属性之一甚至是任意(静态)方法。
如果负责生成key的算法过于具体或需要共享,则可以在操作中定义自定义keyGenerator。为此,请指定要使用的KeyGenerator bean实现的名称:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
key和keyGenerator参数是互斥的,指定两者的操作将导致异常。
默认的 Cache Resolution
开箱即用,缓存抽象使用一个简单的CacheResolver,它使用配置的CacheManager检索在操作级别定义的缓存。
要提供不同的默认缓存解析器,需要实现org.springframework.cache.interceptor.CacheResolver接口。
自定义 cache resolution
默认缓存解析非常适合使用单个CacheManager且没有复杂缓存解析要求的应用程序。
可以如下给特定方法操作设置指定的cacheManager:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}
也可以替换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参数支持此类功能,该参数采用被评估为true或false的SpEL表达式。如果为true,则缓存该方法 - 如果不是,则其行为就像该方法未缓存一样,无论缓存中的值是什么,或者使用哪些参数,都会执行该方法。一个简单示例 - 仅当参数名称的长度小于32时,才会缓存以下方法:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
另外条件参数,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)
请注意,结果仍然是指Book而非Optional。由于它可能为null,我们应该使用安全导航操作符。
可用缓存SpEL计算上下文
Name | Location | Description | Example |
---|---|---|---|
methodName | root object | The name of the method being invoked | #root.methodName |
method | root object | The method being invoked | #root.method.name |
target | root object | The target object being invoked | #root.target |
targetClass | root object | The class of the target being invoked | #root.targetClass |
args | root object | The arguments (as array) used for invoking the target | #root.args[0] |
caches | root object | Collection of caches against which the current method is executed | #root.caches[0].name |
argument name | evaluation context | Name of any of the method arguments. If for some reason the names are not available (e.g. no debug information), the argument names are also available under the #a<#arg> where #arg stands for the argument index (starting from 0). | #iban or #a0 (one can also use #p0 or #p<#arg> notation as an alias). |
result | evaluation context | The result of the method call (the value to be cached). Only available in unless expressions, cache put expressions (to compute the key), or cache evict expressions (when beforeInvocation is false). For supported wrappers such as Optional, #result refers to the actual object, not the wrapper. | #result |
@CachePut 注解
对于需要在不干扰方法执行的情况下更新缓存的情况,可以使用@CachePut注解。也就是说,该方法将始终执行并将其结果放入缓存中(根据@CachePut选项)。它支持与@Cacheable相同的选项,应该用于缓存填充而不是方法流优化:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
请注意,通常强烈建议不要在同一方法上使用@CachePut和@Cacheable注释,因为它们具有不同的行为。虽然后者导致通过使用缓存跳过方法执行,但前者强制执行以执行缓存更新。这会导致意外的行为,并且除了特定的角落情况(例如注释具有将它们彼此排除的条件)之外,应该避免这种声明。还要注意,这种条件不应该依赖于结果对象(即#result变量),因为这些条件是在前面验证的,以确认排除。
@CacheEvict 注解
缓存抽象不仅允许缓存存储的填充,还允许驱逐。此过程对于从缓存中删除陈旧或未使用的数据非常有用。与@Cacheable相反,注释@CacheEvict划分了执行缓存逐出的方法,即用作从缓存中删除数据的触发器的方法。就像它的兄弟姐妹一样,@CacheEvict需要指定受操作影响的一个(或多个)缓存,允许指定自定义缓存和key解析或条件,但另外有一个额外的参数allEntries,它指示是否需要执行缓存范围的驱逐,而不仅仅是一个条目(基于key):
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
当需要清除整个缓存区域时,此选项会派上用场 - 而不是逐出每个条目(这会花费很长时间,因为效率低),所有条目都会在一个操作中被删除,如上所示。请注意,框架将忽略此方案中指定的任何键,因为它不适用(整个缓存不仅仅被逐出一个条目)。
还可以指示驱逐是在(默认)之后还是在方法通过beforeInvocation属性执行之前发生的。前者提供与其他注释相同的语义 - 一旦方法成功完成,就会执行缓存上的操作(在本例中为逐出)。如果该方法未执行(因为它可能被缓存)或抛出异常,则不会发生逐出。后者(beforeInvocation = true)导致驱逐始终在调用方法之前发生 - 这在驱逐不需要与方法结果相关联的情况下很有用。
值得注意的是,void方法可以与@CacheEvict一起使用 - 因为这些方法充当触发器,返回值被忽略(因为它们不与缓存交互) - @Cacheable不是这种情况,它将数据添加/更新到缓存中,因此需要一个结果。
@Caching 注解
有些情况下需要指定相同类型的多个注释,例如@CacheEvict或@CachePut,例如因为条件或key表达式在不同的缓存之间是不同的。@Caching允许在同一方法上使用多个嵌套@Cacheable,@ CachePut和@CacheEvict:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
@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元素:
<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的配置有意类似。
处理缓存注释的默认建议模式是“代理”,它允许仅通过代理拦截调用;同一类中的本地调用不能以这种方式截获。对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到“aspectj”模式。使用Java配置的高级自定义需要实现CachingConfigurer。
XML Attribute | Annotation Attribute | Default | Description |
---|---|---|---|
cache-manager | N/A (See CachingConfigurer javadocs) | cacheManager | Name of cache manager to use. A default CacheResolver will be initialized behind the scenes with this cache manager (or cacheManager if not set). For more fine-grained management of the cache resolution, consider setting the ‘cache-resolver’ attribute. |
cache-resolver | N/A (See CachingConfigurer javadocs) | A SimpleCacheResolver using the configured cacheManager. | The bean name of the CacheResolver that is to be used to resolve the backing caches. This attribute is not required, and only needs to be specified as an alternative to the ‘cache-manager’ attribute. |
key-generator | N/A (See CachingConfigurer javadocs) | SimpleKeyGenerator | Name of the custom key generator to use. |
error-handler | N/A (See CachingConfigurer javadocs) | SimpleCacheErrorHandler | Name of the custom cache error handler to use. By default, any exception throw during a cache related operations are thrown back at the client. |
mode | mode | proxy | The default mode “proxy” processes annotated beans to be proxied using Spring’s AOP framework (following proxy semantics, as discussed above, applying to method calls coming in through the proxy only). The alternative mode “aspectj” instead weaves the affected classes with Spring’s AspectJ caching aspect, modifying the target class byte code to apply to any kind of method call. AspectJ weaving requires spring-aspects.jar in the classpath as well as load-time weaving (or compile-time weaving) enabled. (See the section called “Spring configuration” for details on how to set up load-time weaving.) |
proxy-target-class | proxyTargetClass | false | Applies to proxy mode only. Controls what type of caching proxies are created for classes annotated with the @Cacheable or @CacheEvict annotations. If the proxy-target-class attribute is set to true, then class-based proxies are created. If proxy-target-class is false or if the attribute is omitted, then standard JDK interface-based proxies are created. (See Section 11.6, “Proxying mechanisms” for a detailed examination of the different proxy types.) |
order | order | Ordered.LOWEST_PRECEDENCE | Defines the order of the cache advice that is applied to beans annotated with @Cacheable or @CacheEvict. (For more information about the rules related to ordering of AOP advice, see the section called “Advice ordering”.) No specified ordering means that the AOP subsystem determines the order of the advice. |
<cache:annotation-driven />仅查找在其定义的相同应用程序上下文中的bean上的@ Cacheable / @ CachePut / @ CacheEvict / @缓存。这意味着,如果在WebApplicationContext中为DispatcherServlet放置<cache:annotation-driven />,它只检查控制器中的bean,而不检查您的服务。
方法可见性和缓存注释
使用代理时,应仅将缓存注释应用于具有公共可见性的方法。如果使用这些注释对带保护的,私有的或包可见的方法进行注释,则不会引发错误,但带注释的方法不会显示已配置的缓存设置。如果需要注释非公共方法,请考虑使用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注释,但容器会在运行时自动获取其声明并理解其含义。请注意,如上所述,需要启用注释驱动的行为。