JSR-107
什么是JSR-107
要回答这个问题,首先要知道JSR是什么,JSR是Java Specification Requests 的缩写 ,Java规范请求,故名思议提交Java规范,大家一同遵守这个规范的话,会让大家‘沟通’起来更加轻松。 规范是很重要的 ,举个例子大家都知道红灯停,路灯行吧,如果每个城市的信号灯代表不一样,那就麻烦了,B城市红灯行,绿灯停,C城市甚至出现紫灯行,闪灯行,想想都知道,如果我们保证不出问题,必须知道每个城市的信号等代表的意义。我们一直使用的JDBC就一个访问数据库的一个规范的例子。 JSR-107呢就是关于如何使用缓存的规范。
Java Caching定义了5个核心接口
分别是CachingProvider
, CacheManager
, Cache
, Entry
和 Expiry
。
接口 | 说明 |
---|---|
CachingProvider | 定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。 |
CacheManager | 定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache 存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有 |
Cache | 是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个 CacheManager所拥有 |
Entry | 是一个存储在Cache中的key-value对。 |
Expiry | 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。 |
看下面这张图你就能更清楚的明白他们之间的对应关系:
在pom.xml中导入我们的标准的规范依赖:
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术; 并支持使用JCache(JSR-107)注解简化我们开发;
- Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
- Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache ,ConcurrentMapCache等;
- 每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
- 使用Spring缓存抽象时我们需要关注以下两点:
1.确定方法需要被缓存以及他们的缓存策略
2.从缓存中读取之前缓存存储的数据
几个重要的概念&缓存注解
说明 | |
---|---|
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、 ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@Cacheable/@CachePut/@CacheEvict 主要的参数
参数名 | 说明 | 举例 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定 至少一个 | 例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 例如:@Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断 | 例如:@Cacheable(value=”testcache”,condition=”#userNam e.length()>2”) |
allEntries (@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | 例如:@CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation(@CacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存, 缺省情况下,如果方法执行抛出异常,则不会清空缓存 | 例如: @CachEvict(value=”testcache”, beforeInvocation=true) |
unless(@CachePut) (@Cacheable) | 用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存 | 例如:@Cacheable(value=”testcache”,unless=”#result == null”) |
Cache SpEL available metadata 下图为在使用缓存注解的能用的表达式
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”,“cache2”})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的 形式,0代表参数的索引 | #iban 、 #a0 、 #p0 ; |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false) | #result |
springboot 缓存注解式使用
搭建环境
记得,想要使用缓存,要在主配置类中加上@EnableCaching//缓存注解
pom.xml
<!-- 引入spring-boot-starter-cache模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
这是用于演示的实体类
public class Book implements Serializable {
private Integer bookId;
private String bookName;
private Float price;
//此处省略 get/set ......
}
@Cacheable
@Cacheable 将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法;
CacheManager管理多个Cache组件的,对缓存对真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字
@Cacheable 几个属性
属性名 | 说明 |
---|---|
cacheNames/value | 指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的形式可以指定多个 |
key | 缓存数据使用的key,可以有它指定,默认是方法参数的值。将方法的运行结果进行缓存,再要相同的数据直接从缓存中获取, |
keyGenerator | key的生成器;可以自己指定key的生成器的组件id 注: keyGenerator/key:二选一使用 |
cacheManager | 指定缓存管理器 或者cacheResolver指定获取解析器 |
condition | 指定符合条件的情况下缓存 |
unless | 否定缓存,当unless指定的条件为true,方法的返回值就不会缓存;可以获取到结果进行判断 |
sync | 是否使用异步 使用这个属性的话就不支持使用unless |
这里Mybatis 的代码就直接省略了,直接看这个更具id查单个的方法吧:
/**
* 几个属性:
* cacheName/value:指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的形式
* 可以指定多个
* key:缓存数据使用的key,可以有它指定,默认是方法参数的值
* //将方法的运行结果进行缓存,再要相同的数据直接从缓存中获取,
*bookId;参数bookId的值 当然你也可以这么搞: #a0 #p0 #root.args[0]
* keyGenerator:key的生成器,可以自己指定
* key/keyRenerator:二选一
* cacheManger:指定缓存管理器 或者cacheResolver指定获取解析器
* condition:指定符合条件的情况下缓存
* ,condition = "#id>0"
* unless:否定缓存,当unless指定的条件为true,方法的返回值就不会缓存;可以获取到结果进行判断
* ,unless = "#result==null"
* unless ="#a0==2" 如果第一个参数的结果为2,不缓存
* sync:是否使用异步 使用这个属性的话就不支持使用unless
*/
@Override
@Cacheable(value = "book",keyGenerator="myKeyGenerator",unless ="#a0==2")
public Book selectByPrimaryKey(Integer bookId) {
return bookMapper.selectByPrimaryKey(bookId);
}
自定义KeyGenerator:
/**
* @Description: 自定义KeyGenerator,默认是使用方法参数+返回值
* @Author: cpc
* @Date: 2019-11-12 23:23
* @Version: V1.0
*/
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object o, Method method, Object... objects) {
return method.getName()+"["+ Arrays.asList(objects).toString() +"]";
}
};
}
}
说明一下:上面这种方式需要制定使用这个KeyGenerator,如果想默认使用的就是这个策略的话可以照下面这种方式来玩(就不需要制定了):
@Configuration
public class MyCacheConfig extends CachingConfigurerSupport {
@Bean
@Override
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object o, Method method, Object... objects) {
return method.getName()+"["+ Arrays.asList(objects).toString() +"]";
}
};
}
}
测试一波:
@Test
public void selectByPrimaryKey() {
Book book = bookService.selectByPrimaryKey(19);
System.out.println(book);
System.out.println("---------------------------------------------");
Book book2 = bookService.selectByPrimaryKey(19);
System.out.println(book2);
}
@CachePut
既调用方法,又更新缓存数据:同步更新
修改数据库中的某个数据,同时更新缓存
value/cacheNames要和查询缓存中的一致才能达到效果
运行时机:
- 1.先调用目标方法
- 2.将目标方法的结果缓存起来
- key="#employee.id"为修改的id
- key="#result.id"为修改后的id
@CachePut的key是不能用 #result.id
只要这个key和之前上面那个查询的缓存key一致的话就会更新
@CachePut(value = "book",key = "#book.bookId")
public Book updateBook(Book book){
System.out.println("update:"+book);
bookMapper.update(book);
return book;
}
@CacheEvict
清除缓存
参数名 | 说明 |
---|---|
key | 指定清除的数据 |
allEntries | 是否删除缓存中所有数据 默认为false |
beforInvocation | 缓存的清除是否在方法之前执行 默认为false |
@CacheEvict(value = "book",key = "#bookId")
public void deleteBook(Integer bookId){
System.out.println("删除"+bookId);
bookMapper.deleteBook(id);
}
@Caching
定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(value = "book",key="#bookName")
},
put = {
@CachePut(value = "book",key="#result.bookId")
}
)
public Book getBookByName(String bookName){
return bookMapper.getBookByName(bookName);
}
@CacheConfig
抽取公共配置
@CacheConfig(cacheNames="book")
@Service
public class BookService {
整合redis作为缓存
缓存注解都一样,没什么变化
整合部分代码
pom.xml
<!-- springboot 整合redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
添加配置:
spring:
redis:
database: 0
host: 192.168.43.170
port: 6379
password: root
jedis:
pool:
max-active: 100
max-idle: 3
max-wait: -1
min-idle: 0
timeout: 1000
写RedisConfig配置类,来配置我们的redis:
这个配置类的注释已经详细说明各项配置是干嘛的啦,我就不多废话了哈
package com.cpc.springboot01.conf;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* redis配置类
**/
@Configuration
@EnableCaching//开启注解式缓存
//继承CachingConfigurerSupport,为了自定义生成KEY的策略。可以不继承。
public class RedisConfig extends CachingConfigurerSupport {
/**
* 生成key的策略 根据类名+方法名+所有参数的值生成唯一的一个key
*
* @return
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* 管理缓存
*
* @param redisConnectionFactory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//通过Spring提供的RedisCacheConfiguration类,构造一个自己的redis配置类,从该配置类中可以设置一些初始化的缓存命名空间
// 及对应的默认过期时间等属性,再利用RedisCacheManager中的builder.build()的方式生成cacheManager:
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
config = config.entryTtl(Duration.ofMinutes(1)) // 设置缓存的默认过期时间,也是使用Duration设置
.disableCachingNullValues(); // 不缓存空值
// 设置一个初始化的缓存空间set集合
Set<String> cacheNames = new HashSet<>();
cacheNames.add("my-redis-cache1");
cacheNames.add("my-redis-cache2");
// 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("my-redis-cache1", config);
configMap.put("my-redis-cache2", config.entryTtl(Duration.ofSeconds(120)));
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) // 使用自定义的缓存配置初始化一个cacheManager
.initialCacheNames(cacheNames) // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
.withInitialCacheConfigurations(configMap)
.build();
return cacheManager;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
}
测试
/**
* 使用 my-redis-cache1 这个槽,key为 book + bookId,当参数bookId 大于 10的时候才进行缓存
* @param bookId
* @return
*/
@Override
@Cacheable(value = "my-redis-cache1",key = "'book'+#bookId",condition = "#bookId>10")
public Book selectByPrimaryKey(Integer bookId) {
return bookMapper.selectByPrimaryKey(bookId);
}
@Test
public void selectByPrimaryKey() {
Book book = bookService.selectByPrimaryKey(19);
System.out.println(book);
System.out.println("--------------我是分割线--------------------------");
Book book2 = bookService.selectByPrimaryKey(19);
System.out.println(book2);
}
reids中缓存的数据: