一、简述
对于缓存声明,抽象提供了一组Java注解:
@Cacheable触发缓存填充(这里一般放在创建和获取的方法上)
@CacheEvict触发缓存驱逐(用于删除的方法上)
@CachePut更新缓存而不干扰方法的执行(用于修改的方法上,该注解下的方法始终会被执行)
@Caching重组多个缓存操作以应用于方法(该注解可以允许一个方法同时设置多个注解)
@CacheConfig在类级别共享一些常见的与缓存相关的设置(与其它缓存配合使用)
1.1、@Cacheable annotation
@Cacheable:可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。@Cacheable可以指定三个属性,value、key和condition。
用于划分可缓存的方法 - 也就是说,结果存储在缓存中的方法,以便后续调用(使用相同的参数),缓存中的值将被返回,而不必实际执行该方法。以最简单的形式,注释声明需要与注释方法关联的缓存的名称:
value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。
@Cacheable("books") public Book findBook(ISBN isbn) {...}
在上面的代码片段中,findBook方法与名为books的缓存相关联。每次调用该方法时,都会检查缓存以查看调用是否已被执行,并且不必重复。虽然在大多数情况下,只声明一个缓存,但注解允许指定多个名称,以便使用多个缓存。在这种情况下,每个缓存将在执行该方法之前进行检查 - 如果至少有一个缓存被命中,则会返回相关的值:
注意:即使缓存的方法没有实际执行,所有其他不包含该值的缓存也会被更新。
@Cacheable({"books", "isbns"}) public Book findBook(ISBN isbn) {...}
源码解读:
public @interface Cacheable { /** * 设定要使用的cache的名字,必须提前定义好缓存 */ @AliasFor("cacheNames") String[] value() default {}; /** * 同value(),决定要使用那个/些缓存 */ @AliasFor("value") String[] cacheNames() default {}; /** * 使用SpEL表达式来设定缓存的key,如果不设置默认方法上所有参数都会作为key的一部分 */ String key() default ""; /** * 用来生成key,与key()不可以共用 */ String keyGenerator() default ""; /** * 设定要使用的cacheManager,必须先设置好cacheManager的bean,这是使用该bean的名字 */ String cacheManager() default ""; /** * 使用cacheResolver来设定使用的缓存,用法同cacheManager,但是与cacheManager不可以同时使用 */ String cacheResolver() default ""; /** * 使用SpEL表达式设定出发缓存的条件,在方法执行前生效 */ String condition() default ""; /** * 使用SpEL设置出发缓存的条件,这里是方法执行完生效,所以条件中可以有方法执行后的value */ String unless() default ""; /** * 用于同步的,在缓存失效(过期不存在等各种原因)的时候,如果多个线程同时访问被标注的方法 * 则只允许一个线程通过去执行方法 */ boolean sync() default false; }
1.1.1、默认Key生成
由于缓存本质上是键值存储,因此每次缓存方法的调用都需要转换为适合缓存访问的key。抽象缓存使用一个简单的基于以下算法KeyGenerator:
如果没有给出参数,则返回SimpleKey.EMPTY。
如果只给出一个参数,则返回该实例。
如果给出了多于一个参数,返回一个包含所有参数的SimpleKey。
这种方法适用于大多数使用情况;只要参数具有自然键并实现有效的hashCode()和equals()方法。如果情况并非如此,那么战略需要改变。
为了提供不同的默认key生成器,需要实现org.springframework.cache.interceptor.KeyGenerator接口。
注意:默认的key生成策略随着Spring 4.0的发布而改变。 Spring的早期版本使用了一种key生成策略,对于多个关键参数,只考虑参数的hashCode()而不考虑equals();这可能会导致意外的键碰撞(请参阅SPR-10237的背景)。新的'SimpleKeyGenerator'使用复合键来实现这种场景。
如果您想继续使用以前的key策略,则可以配置弃用的org.springframework.cache.interceptor.DefaultKeyGenerator类或创建基于散列的自定义“KeyGenerator”实现。
1.1.2、自定义key生成策略
由于缓存是通用的,因此目标方法很可能具有不能简单映射到缓存结构顶部的各种签名。当目标方法有多个参数,其中只有一些适用于缓存(而其余的仅由方法逻辑使用)时,这会变得很明显。例如:
@Cacheable("books") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
对于这种情况,@Cacheable注解允许用户指定如何通过其关键属性生成key。开发人员可以使用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参数是互斥的,指定两者的操作将导致异常。
1.1.3、默认缓存解析器
缓存抽象使用一个简单的CacheResolver,它使用配置的CacheManager检索在操作级别定义的缓存。
要提供不同的默认缓存解析器,需要实现org.springframework.cache.interceptor.CacheResolver接口。
1.1.4、自定义缓存解析器
默认的缓存解析非常适合使用单个CacheManager并且不需要复杂的缓存解析要求的应用程序。对于使用多个缓存管理器的应用程序,可以将cacheManager设置为使用每个操作:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") public Book findBook(ISBN isbn) {...}
也可以完全以与key生成类似的方式替换CacheResolver。该解决方案针对每个缓存操作进行请求,给予实现基于运行时参数实际解析要使用的缓存的机会:
@Cacheable(cacheResolver="runtimeCacheResolver") public Book findBook(ISBN isbn) {...}
注意:自Spring 4.1以来,缓存注释的值属性不再是强制性的,因为无论注释的内容如何,CacheResolver都可以提供此特定信息。
与key和keyGenerator类似,cacheManager和cacheResolver参数是互斥的,指定两者的操作将导致出现异常,因为CacheResolver实现将忽略自定义CacheManager。这可能不是你所期望的。
1.1.5、同步缓存
在多线程环境中,可能会为同一个参数(通常在启动时)同时调用某些操作。默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而破坏缓存的目的。
对于这些特殊情况,可以使用sync属性指示基础缓存提供程序在计算值时锁定缓存条目。因此,只有一个线程会忙于计算值,而其他线程将被阻塞,直到该条目在缓存中更新。
@Cacheable(cacheNames="foos", sync=true) public Foo executeExpensiveOperation(String id) {...}
注意:这是一个可选功能,您最喜欢的缓存库可能不支持它。核心框架提供的所有CacheManager实现都支持它。
1.1.6、条件缓存
有时,一个方法可能并不适合于一直进行缓存(例如,它可能取决于给定的参数)。缓存注释通过条件参数支持这种功能,该条件参数采用SpEL表达式,该表达式被评估为true或false。如果为true,则该方法被缓存 - 如果不是,则其行为就像该方法未被缓存一样,每当无论缓存中的值或使用什么参数时都会执行该方法。
有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。
@Cacheable(cacheNames="book", condition="#name.length() < 32") public Book findBook(String name)
此外,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)
注意:Note that result
still refers to Book
and not Optional
. As it might be null
, we should use the safe navigation operator.
1.1.7、可用的缓存SpEL评估上下文
每个SpEL表达式再次评估一个专用上下文。除了内置参数外,框架还提供了与参数名称相关的专用缓存相关元数据。下表列出了可用于上下文的项目,以便可以将它们用于Key和条件计算:
Name | Location | Description | Example |
---|---|---|---|
methodName | root object | The name of the method being invoked |
|
method | root object | The method being invoked |
|
target | root object | The target object being invoked |
|
targetClass | root object | The class of the target being invoked |
|
args | root object | The arguments (as array) used for invoking the target |
|
caches | root object | Collection of caches against which the current method is executed |
|
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 |
|
result | evaluation context | The result of the method call (the value to be cached). Only available in |
|