概述
从 spring3 开始,spring 开始支持缓存组件,并提供了一系列非常方便的注解。
其中,Cache 接口定义了基本的增删改查方法,Spring 提供了一些默认的实现,比如 RedisCache 等。 CacheManager 接口则用于接入并获取 Redis 这样的缓存组件。
在 springboot 中,当我们启用缓存以后,需要如果要将接入 Redis 作为缓存,就需要配置 RedisCacheManager,其他缓存组件亦同。当没有自定义的缓存的时候,就会使用默认的 ConcurrentMapCacheManager,即将缓存存入本地的一个 Map 集合中。
一、配置
1.添加依赖
在创建项目的时候直接在 springboot 启动器中配置,或在项目中添加 Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2. 启用注解
在启动类或者配置类里添加 @EnableCaching 注解开启缓存
@EnableCaching//开启缓存
@SpringBootApplication
public class SpringbootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
注意:由于 Spring 的 AOP 基于 CGLib 代理,所以如果在本类调用成员方法,因为不走代理所以缓存是不会生效的。
二、注解与 SpEL 表达式
1.缓存注解
注解 | 解释 |
@EnableCaching | 开启基于注解的缓存 |
@CacheConfig | 统一配置本类的缓存注解的属性 |
@Cacheable | 第一次调用方法后都将返回值存入缓存,下次则请求直接调用缓存 |
@CachePut | 每次调用方法后都将返回值存入缓存,用于缓存更新 |
@CacheEvict | 清除缓存 |
@Caching | 组合注解,即给一个方法同时设置多个缓存方案 |
2. SpEL 表达式
spring 表达式支持我们通过指定的明确获取参数或者方法的属性:
名称 | 位置 | 描述 | 示例 |
methodName | root对象 | 当前被调用的方法名 | #root.methodname |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] #root.参数名 |
caches | root对象 | 当前方法调用使用的缓存列表 | #root.caches[0].name |
Argument Name | 执行上下文 | 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 | #artsian.id |
result | 执行上下文 | 法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) | #result |
因为默认对象即为 #root,所以写的时候可以直接省略 #root
此外,表达式同样支持运算符:
类型 | 运算符 |
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | &&,||,!,and,or,not,between,instanceof |
条件 | ?: (ternary),?: (elvis) |
三、使用
1.添加缓存
@Cacheable 注解用于标记方法。第一次执行的时候,会将方法结果存入缓存,再次调用该方法,在执行前,如果 key 相同则直接返回缓存中的数据,不会再次执行方法。
他的主要属性有以下五种:
- value/cacheName:指定缓存命名空间,只有保证命名空间相同,key 才能找到对应的缓存。
- key:指定缓存数据的键值对的key,默认为方法参数中的值;
- Condition:指定缓存执行的条件;
- unliss:指定缓存不执行的条件;
- sync:指定缓存是否使用异步模式,默认同步,若异步则不支持 unless。这个主要是用来处理多线程环境下缓存更新导致数据出错的问题的。
上述一些复杂的条件依赖于 sqEl 表达式
/*
* 方法调用给前进行检测,若存在 key 对应的数据则直接从缓存中返回结果,否则在执行后将返回值存入缓存
* */
@Override
@Cacheable(cacheNames = "dept",key = "targetClass")
public List<Dept> findAll() {
System.out.println("全查!");
return mapper.selectAll();
}
2.缓存更新
@CachePut 注解一般用于更新某个命名空间中的某条缓存数据。当方法调用结束以后,会根据注解将结果存入缓存,如果 key 已经存在,则会更新缓存。
他的主要属性同 @Cacheable 基本相同,但是由于是先执行才处理数据,所以 key 是可以获得 #root.result 的。
/*
* 调用方法后,再把返回值放入缓存
* */
@Override
@CachePut(cacheNames = "dept",key = "#root.targetClass")
public List<Dept> add(Dept dept) {
System.out.println("增加!");
mapper.insert(dept);
return mapper.selectAll();
}
3.清空缓存
@CacheEvict 用于清空某个命名空间内某条、或全部缓存数据。
他的主要属性就两个:
- allEntries:是否清空全部缓存数据。默认 true,会忽略 key,否则只清除 key 对应的缓存;
- beforeInvocation:是否要在方法执行前就清空缓存。默认为 false,即执行结束后才清空缓存。
注意:当发生异常时,是不会清空缓存的。
/*
* 清除缓存中的数据
* */
@CacheEvict(cacheNames = "dept", key = "#root.targetClass", beforeInvocation = false)
@Override
public void delete(Integer deptno) {
System.out.println("删除!");
mapper.deleteByPrimaryKey(deptno);
}
4.多条件组合
@Caching 是一个组合注解,可以在里面添加复数的以上三种注解。
/*
* 组合注解,三种注解以每种可有多个,以数组形式存储
* */
@Caching(
cacheable = {
//添加一个缓存
@Cacheable(value = "dept",key = "#deptno")
},
put = {
//更新一个缓存
@CachePut(value = "emp",key = "#root.targetClass")
},
evict = {
//清除一个缓存
@CacheEvict(value = "person",key = "#deptno")
}
)
@Override
public Dept findById(Integer deptno) {
return null;
}
5.统一命名空间
@CacheConfig 注解可以在类上统一命名空间,这样类中如果没有指定 cacheName 或者 value,那么默认会使用 @CacheConfig 指定的命名空间。当然,如果在方法上自己指定了另外的命名空间,则以方法上的注解为准。
//使用@CacheConfig统一cacheName,如果在方法上仍写了value,则该方法值仍以value为准
@CacheConfig(cacheNames = "dept")
@Service
public class DeptServiceImp implements DeptService {
@Resource
private DeptMapper mapper;
@Override
@Cacheable(key = "targetClass")
public List<Dept> findAll() {
System.out.println("全查!");
return mapper.selectAll();
}
@Override
@CachePut(key = "targetClass")
public List<Dept> add(Dept dept) {
System.out.println("增加!");
mapper.insert(dept);
return mapper.selectAll();
}
@CachePut(key = "targetClass")
@Override
public List<Dept> delete(Integer deptno) {
System.out.println("删除!");
mapper.deleteByPrimaryKey(deptno);
return mapper.selectAll();
}
}