Spring 缓存

Spring 缓存


2020-04-03

平时在项目中总是手动的管理缓存,多出来不少的冗余代码不说,在管理某个资源的缓存保存、更新和清除时容易出现差错,借助 Spring Cache 的注解,在某种程度上可以做到优雅地管理缓存。

Spring 缓存关键注解

  • @Cacheable: 缓存填充。
  • @CacheEvict: 缓存清除。
  • @CachePut: 不影响方法执行的情况下更新缓存。
  • @Caching: 支持一组缓存操作。
  • @CacheConfig: Class 级别的缓存设置。

@Cacheable

1
2
3
4
5
6
7
8
@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 来确定某一个缓存,在 @Cacheable 中支持 SpEL 表达式。可以自定义 keyGenerator 来自定义 key 的生成,但需要注意的是 keykeyGenerator 是互斥的,同样,cacheManagercacheResolver 也是互斥的。

缓存条件主要使用 unlesscondition 参数设置,前者过滤结果,后者过滤条件。

不缓存查询结果为空的数据:

1
2
@Cacheable(cacheNames="book", unless="#result == null")
public Optional<Book> findBook(String name)

支持 java.util.Optional的情况 :

1
2
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

注意 result 可能为空,需要另加处理。

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 the names are not available (perhaps due to having 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 (you 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

在不影响方法执行的基础上更新缓存,用法与 @Cachable 一致。

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

需要注意的是不要与 @Cachable 一起使用,原因如下:

Using @CachePut and @Cacheable annotations on the same method is generally strongly discouraged because they have different behaviors. While the latter causes the method execution to be skipped by using the cache, the former forces the execution in order to execute a cache update. This leads to unexpected behavior and, with the exception of specific corner-cases (such as annotations having conditions that exclude them from each other), such declarations should be avoided. Note also that such conditions should not rely on the result object (that is, the #result variable), as these are validated up-front to confirm the exclusion.

@CacheEvict

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

allEntriestrue 时,可以清除该 cacheName 下的所有缓存。默认是在方法执行之后清除 ,也可以使用 beforeInvocation 指定之前。

@Caching

批量缓存操作。

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

@CacheConfig

对某个类中的缓存执行设置。

1
2
3
4
5
6
@CacheConfig("books") 
public class BookRepositoryImpl implements BookRepository {

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

存在的问题

  1. Spring Cache 有时候会出现奇怪的行为,比如在指定 key#username 后,会一直提示这个参数为空,这个时候使用 #p0 可以获取到值,还有一种方法是在 @EnableCaching 中设置 modeASPECTJ ,而不使用 PROXY 。但这个时候可能会报错,说没有 ASPECTJ 的包,这时候需要手动把 spring-aspects 加入依赖(需注意版本一致)。不幸的是,在 JDK 11当中执行了以上操作,依然不起效果,目前我并没有找到解决方法,只能先用 PROXY 。(可能的原因是: Spring recommends that you only annotate concrete classes (and methods of concrete classes) with the @Cache* annotation, as opposed to annotating interfaces. You certainly can place the @Cache* annotation on an interface (or an interface method), but this works only as you would expect it to if you use interface-based proxies. The fact that Java annotations are not inherited from interfaces means that, if you use class-based proxies (proxy-target-class="true") or the weaving-based aspect (mode="aspectj"), the caching settings are not recognized by the proxying and weaving infrastructure, and the object is not wrapped in a caching proxy.)
  2. 第二个问题,这些注解在嵌套使用时只会有一个注解生效(我记不清楚是外层还是内层),所以最好避免这样使用方式。