简介
说明
本文用示例介绍Spring的缓存注解的用法
Spring的缓存注解有这几种:
- 读数据时将数据写入缓存
- 每次都正常执行方法,执行结束后将数据写入缓存
- 将缓存中的数据删除
在若一个方法只读数据库,读完后放到缓存,此时用@Cacheable很方便。
如果需要写数据库同时缓存,就需要慎重。原因:为缓存和数据库的一致性问题很重要,
在我还不明确Spring的数据库事务AOP与这个缓存注解的AOP的执行顺序时,我会手动地读写缓存:在写数据的方法上加@Transactional,写数据库后删缓存,这样如果删缓存如果失败,数据库会回滚。相反,如果我用本文的注解,万一它先提交事务,后删除缓存,如果缓存删除失败,那么数据库中是新数据,缓存中是老数据,就导致不一致了!
公共代码
简介
下边的所有测试都采用下边这种写法,只是会换一下具体的方法。
启动类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
注意
本处不需要其他配置, 我们使用默认的ConcurrentHashMap来测试。
Controller
package com.example.controller;
import com.example.order.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
public class ArticleController {
private ArticleService articleService;
("/test")
public String test() {
System.out.println(articleService.list());
return "test success";
}
}
Service
package com.example.order;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
public class ArticleService {
(cacheNames = {"cache1"})
public List<String> list() {
System.out.println("获取文章列表!");
return Arrays.asList("Spring", "MySQL", "中间件");
}
}
工具类
package com.example.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
public void setApplicationContext(ApplicationContext context)
throws BeansException {
ApplicationContextHolder.context = context;
}
public static ApplicationContext getContext() {
return context;
}
}
@Cacheable
@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。
Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。
属性简介
名称 | 解释 |
cacheNames 或value | 指定缓存的名称,即:方法的返回结果放在哪个缓存中。 属性定义为数组,可以指定多个缓存; |
key | 缓存的 key,可以为空。默认按照方法的所有参数进行组合。如果指定:按照 SpEL 表达式编写。 |
keyGenerator | key 的生成器。 如果觉得通过参数的方式来指定比较麻烦,可以指定 key 的生成器的组件 id。key/keyGenerator:二选一使用。 |
condition | 缓存的条件,使用 SpEL 编写,只有为 true 才进行缓存/清除缓存。 |
unless | 否定缓存。当条件结果为true时,就不会缓存。 |
sync | 是否使用同步,该属性默认值为 false,即:默认不会进行多线程的同步。 若指定 sync = true ,unless 属性不可用。 |
cacheManager | 指定缓存管理器。(很少手动指定) |
cacheResolver | 指定缓存管理器。(很少手动指定) |
cacheNames/value
简介
value和cacheNames属性作用一样,必须指定其中一个,表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache。
可以将Cache想象为一个HashMap,系统中可以有很多个Cache,每个Cache有一个名字,你需要将方法的返回值放在哪个缓存中,需要通过缓存的名称来指定。
Service
cacheNames = {"cache1"})(
public List<String> list() {
System.out.println("从db中获得列表!");
return Arrays.asList("Spring", "MySQL", "中间件");
}
Controller
"/test")(
public String test() {
System.out.println(articleService.list());
System.out.println(articleService.list());
return "test success";
}
测试(第二次访问时直接从缓存中获取)
访问:http://localhost:8080/test
从db中获得列表!
[Spring, MySQL, 中间件]
[Spring, MySQL, 中间件]
key
简介
key属性用来指定Spring缓存方法的返回结果时对应的key的,上面说了你可以将Cache理解为一个hashMap,缓存以key->value的形式存储在hashmap中,value就是需要缓存值(即方法的返回值)。
key属性支持SpEL表达式;当我们没有指定该属性时,Spring将使用默认策略生成key(org.springframework.cache.interceptor.SimpleKeyGenerator),默认会通过方法的参数创建key。
可以通过SpEL表达式来指定我们的key,这里的SpEL表达式可以使用方法的参数及它们对应的属性,使用方法的参数时我们可以直接使用“#参数名”或者“#p参数index”。
Spring还为我们提供了一个root对象可以用来生成key,本处以如下代码进行示例说明
package com.example.service;
import com.example.entity.Result;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
public class TestService {
(cacheNames = {"cache1"})
public Result page(int page, int pageSize) {
String msg = String.format("page为%s;pageSize为%s", page, pageSize);
System.out.println("从db中分页:" + msg);
return new Result().data("Page success");
}
}
属性 | 描述 | 示例代码的值(入参:1,10) |
#root.methodName 或#root.method.name | 当前方法名 | page |
#root.target | 当前被调用的对象 | com.example.service.TestService@3fb0313e |
#root.target.class.name | 当前被调用的对象的类名 | com.example.service.TestService |
#root.targetClass | 当前被调用的对象 | class com.example.service.TestService |
#root.args[0] | 当前方法参数组成的数组 | 1 |
#root.caches[0].name | 当前被调用的方法使用的Cache | cache1 |
示例
Service
分页,缓存其类+第几页+页的大小
cacheNames = {"cache1"},(
key = "#root.target.class.name + '-' + #page + '-' + #pageSize")
public String page(int page, int pageSize) {
String msg = String.format("page为%s;pageSize为%s", page, pageSize);
System.out.println("从db中分页:" + msg);
return msg;
}
Controller
"/test")(
public String test() {
System.out.println(articleService.page(1, 10));
System.out.println("------------------------------------------");
System.out.println(articleService.page(1, 10));
System.out.println("------------------------------------------");
System.out.println("下面打印出cache1缓存中的key列表");
ConcurrentMapCacheManager cacheManager = ApplicationContextHolder
.getContext().getBean(ConcurrentMapCacheManager.class);
ConcurrentMapCache cache1 = (ConcurrentMapCache) cacheManager.getCache("cache1");
cache1.getNativeCache().keySet().stream().forEach(System.out::println);
return "test success";
}
测试(将指定的key作为了key来保存)
访问:http://localhost:8080/test
从db中分页:page为1;pageSize为10
Result(success=true, code=1000, message=null, data=Page success)
------------------------------------------
Result(success=true, code=1000, message=null, data=Page success)
------------------------------------------
下面打印出cache1缓存中的key列表
com.example.order.ArticleService-1-10
condition
简介
有时希望查询不走缓存,同时返回的结果也不要被缓存,就可以通过condition属性来实现。
condition属性默认为空,表示将缓存所有的调用情形,其值是通过spel表达式来指定的,当为true时表示先尝试从缓存中获取;若缓存中不存在,则执行方法,并将方法返回值丢到缓存中;当为false的时候,不走缓存、直接执行方法、并且返回结果也不会丢到缓存中。
其值spel的写法和key属性类似。
Service
方法的第二个参数cache用来控制是否走缓存。
cacheNames = "cache1", key = "'getById'+#id", condition = "#cache")(
public String getById(Long id, boolean cache) {
System.out.println("根据id获取数据!");
return "Spring缓存:" + UUID.randomUUID();
}
Controller
"/test")(
public String test() {
System.out.println(articleService.getById(1L, true));
System.out.println(articleService.getById(1L, true));
System.out.println(articleService.getById(1L, false));
System.out.println(articleService.getById(1L, true));
return "test success";
}
测试(condition为false时不从缓存中取、也不清空缓存)
访问:http://localhost:8080/test
根据id获取数据!
Spring缓存:18096e79-bde1-4207-9992-46e41141d41d
Spring缓存:18096e79-bde1-4207-9992-46e41141d41d
根据id获取数据!
Spring缓存:ed9d4adc-563e-4551-8682-058e39b6044b
Spring缓存:18096e79-bde1-4207-9992-46e41141d41d
unless
简介
- 控制是否需要将结果丢到缓存中。SpEL表达式控制。
- 若unless为true,方法返回结果不会放到缓存。
- 若不设置(默认为“”)或为false,方法返回结果会放到缓存。
- 与condition不同,此表达式是在调用方法后计算的,因此可以引用结果。
- condition为空或者为true的情况下,unless才有效。
- condition为false的时候,unless无效。
Service
当返回结果为null的时候,不要将结果进行缓存。
cacheNames = "cache1",(
key = "'findById'+#id",
unless = "#result==null")
public String getById(Long id) {
System.out.println("根据id获取文章:" + id);
return id == 1L
? "article" + id
: null;
}
Controller
"/test")(
public String test() {
System.out.println(articleService.getById(1L));
System.out.println(articleService.getById(1L));
System.out.println(articleService.getById(3L));
System.out.println(articleService.getById(3L));
System.out.println("下面打印出cache1缓存中的key列表");
ConcurrentMapCacheManager cacheManager = ApplicationContextHolder
.getContext().getBean(ConcurrentMapCacheManager.class);
ConcurrentMapCache cache1 = (ConcurrentMapCache) cacheManager.getCache("cache1");
cache1.getNativeCache().keySet().stream().forEach(System.out::println);
return "test success";
}
测试(结果为null时没有缓存)
访问:http://localhost:8080/test
根据id获取文章:1
article1
article1
根据id获取文章:3
null
根据id获取文章:3
null
下面打印出cache1缓存中的key列表
findById1
@CachePut
@CachePut也可以标注在类或者方法上。若标注在方法上,被标注的方法每次都会被调用,然后方法执行完毕之后,会将方法结果丢到缓存中;当标注在类上,相当于在类的所有方法上标注了@CachePut。
属性简介
跟上边@Cacheable一模一样。
案例
简介
新增文章,然后将文章丢到缓存中。
注意下面@CachePut中的cacheNames、key 2个参数和案例4中findById方法上@Cacheable中的一样,说明他们共用一个缓存,key也是一样的,那么当add方法执行完毕之后,再去调用findById方法,则可以从缓存中直接获取到数据。
Service
cacheNames = "cache1", key = "'getById'+#id")(
public String add(Long id) {
System.out.println("新增文章:" + id);
String content = "article" + id;
map.put(id, content);
return content;
}
(cacheNames = "cache1",
key = "'getById'+#id",
unless = "#result==null")
public String getById(Long id) {
System.out.println("根据id获取文章:" + id);
return map.get(id);
}
Controller
"/test")(
public String test() {
System.out.println(articleService.add(1L));
System.out.println(articleService.add(2L));
System.out.println(articleService.getById(1L));
System.out.println(articleService.getById(2L));
System.out.println("下面打印出cache1缓存中的key列表");
ConcurrentMapCacheManager cacheManager = ApplicationContextHolder
.getContext().getBean(ConcurrentMapCacheManager.class);
ConcurrentMapCache cache1 = (ConcurrentMapCache) cacheManager.getCache("cache1");
cache1.getNativeCache().keySet().stream().forEach(System.out::println);
return "test success";
}
测试(结果为null时没有缓存)
访问:http://localhost:8080/test
新增文章:1
article1
新增文章:2
article2
article1
article2
下面打印出cache1缓存中的key列表
getById2
getById1
输出中并没有“根据id获得文章”,说明数据是从缓存中取的。
@CacheEvict
用来清除缓存的,@CacheEvict也可以标注在类或者方法上,被标注在方法上,则目标方法被调用的时候,会清除指定的缓存;当标注在类上,相当于在类的所有方法上标注了@CacheEvict。
属性简介
名称 | 解释 |
cacheNames 或value | 指定缓存的名称,即:方法的返回结果放在哪个缓存中。 属性定义为数组,可以指定多个缓存; |
key | 缓存的 key,可以为空。默认按照方法的所有参数进行组合。如果指定:按照 SpEL 表达式编写。 |
condition | 缓存的条件,使用 SpEL 编写,只有为 true 才进行缓存/清除缓存。 |
allEntries | 是否清理 cacheNames 指定的缓存中的所有缓存信息,默认是false。 当 allEntries 为true的时候,会清理所有缓存,相当于HashMap.clear() |
beforeInvocation | 何时执行清除操作(方法执行前 or 方法执行成功之后) |
案例
简介
方法执行完毕之后,会清理cache1中key=findById+参数id的缓存信息。
注意cacheNames和key两个参数的值和findById中这2个参数的值一样。
Service
public String add(Long id) {
System.out.println("新增文章:" + id);
String content = "article" + id;
map.put(id, content);
return content;
}
(cacheNames = "cache1",
key = "'getById'+#id",
unless = "#result==null")
public String findById(Long id) {
System.out.println("根据id获取文章:" + id);
return map.get(id);
}
(cacheNames = "cache1", key = "'getById'+#id")
public void delete(Long id) {
System.out.println("删除文章:" + id);
this.map.remove(id);
}
Controller
"/test")(
public String test() {
// 新增文章。不缓存
System.out.println(articleService.add(1L));
//第1次调用findById,缓存中没有,则调用方法,将结果丢到缓存中
System.out.println(articleService.findById(1L));
//第2次调用findById,缓存中存在,直接从缓存中获取
System.out.println(articleService.findById(1L));
//执行删除操作,delete方法上面有@CacheEvict方法,会清除缓存
articleService.delete(1L);
//再次调用findById方法,发现缓存中没有了,则会调用目标方法
System.out.println(articleService.findById(1L));
return "test success";
}
测试
访问:http://localhost:8080/test
新增文章:1
article1
根据id获取文章:1
article1
article1
删除文章:1
根据id获取文章:1
null
@Caching
简介
当我们在类上或者同一个方法上同时使用@Cacheable、@CachePut和@CacheEvic这几个注解中的多个的时候,此时可以使用@Caching这个注解来实现。
用法示例
cacheable = ("users"),(
evict = {
("cache2"),
(value = "cache3", allEntries = true)
}
)
public String find(Integer id) {
return null;
}
@CacheConfig
简介
这个注解标注在类上,可以将其他几个缓存注解(@Cacheable、@CachePut和@CacheEvic)的公共参数给提取出来放在@CacheConfig中。
比如当一个类中有很多方法都需要使用(@Cacheable、@CachePut和@CacheEvic)这些缓存注解的时候,若这些属性值都是一样的,可以将其提取出来,放在@CacheConfig中。3个注解有很多公共的属性,比如:cacheNames、keyGenerator、cacheManager、cacheResolver。
这些注解(@Cacheable、@CachePut和@CacheEvic)中也可以指定属性的值对@CacheConfig中的属性值进行覆盖。
示例
cacheNames = "cache1")(
public class ArticleService {
(key = "'findById'+#id")
public String findById(Long id) {
this.articleMap.put(1L, "spring系列");
System.out.println("----获取文章:" + id);
return articleMap.get(id);
}
}
其他网址
Spring系列第40篇:缓存使用【公众号:路人甲Java】