1. SpringCache
1.1 SpringCache 依赖
Spring 3.1 引入了激动人心的基于凝视(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(比如EHCache 或者 OSCache,Redis等),而是一个对缓存使用的抽象,通过在既有代码中加入少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性。不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存暂时存储方案,也支持和主流的专业缓存比如 EHCache,OSCache,Redis等集成。
Spring 的缓存技术还具备相当的灵活性。不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存暂时存储方案,也支持和主流的专业缓存比如 EHCache 集成。
其特点总结例如以下:
- 通过少量的配置 annotation 凝视就可以使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件就可以使用缓存
- 支持 Spring Express Language,能使用对象的不论什么属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过事实上现不论什么方法的缓存支持
- 支持自己定义 key 和自己定义缓存管理者,具有相当的灵活性和扩展性
使用Spring Cache首先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1.1.1 SpringCache的好处
SpringCache包含两个顶级接口,Cache(缓存)和CacheManager(缓存管理器)。顾名思义,用CacheManager去管理一堆Cache。
最最关键的地方:抱紧了Spring的大腿,可以使用注解就能完成数据进入缓存!!
2. SpringCache使用
2.1 @EnableCaching
该注解是最基础的注解,用于启用Spring的缓存管理功能,类似于Spring XML配置中的cache:annotation-driven 标签。
在启动类上开启 @EnableCaching。
@SpringBootApplication
@EnableCaching
public class SpringCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCacheApplication.class, args);
}
}
2.2 @Cacheable
@Cacheable 注解在方法上,表示该方法的返回结果是可以缓存的。也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法。
**注意,这里强调了一点:参数相同。**这一点应该是很容易理解的,因为缓存不关心方法的执行逻辑,它能确定的是:对于同一个方法,如果参数相同,那么返回结果也是相同的。但是如果参数不同,缓存只能假设结果是不同的,所以对于同一个方法,你的程序运行过程中,使用了多少种参数组合调用过该方法,理论上就会生成多少个缓存的 key(当然,这些组合的参数指的是与生成 key 相关的)。下面来了解一下 @Cacheable 的一些参数:
cacheNames & value
@Cacheable 提供两个参数来指定缓存名:value、cacheNames ,二者选其一即可。这是 @Cacheable 最简单的用法示例:
@Service("helloService")
@Log4j2
public class HelloServiceImpl {
@Cacheable(value = "sayHello")
public String sayHello(){
System.out.println("call sayHello...");
return "hello";
}
}
在这个例子中,sayHello 方法与一个名为 sayHello 的缓存关联起来了。调用该方法时,会检查 sayHello缓存,如果缓存中有结果,就不会去执行方法了。
@Resource
private HelloServiceImpl helloService;
@Test
void springCacheTest01(){
System.out.println(helloService.sayHello());
System.out.println(helloService.sayHello());
System.out.println(helloService.sayHello());
}
结果为:
call sayHello...
hello
hello
hello
其实,按照官方文档,@Cacheable 支持同一个方法关联多个缓存。这种情况下,当执行方法之前,这些关联的每一个缓存都会被检查,而且只要至少其中一个缓存命中了,那么这个缓存中的值就会被返回。如果缓存不存在则调用方法,并将结果同时缓存在每一个关联的缓存里面,示例:
@Service
public class HelloService {
@Cacheable(value = {"sayHello","sayHello2",})
public String sayHello(String name){
System.out.println("call sayHello...");
return "hello";
}
}
key & keyGenerator
一个缓存名对应一个被注解的方法,但是一个方法可能传入不同的参数,那么结果也就会不同,这应该如何区分呢?这就需要用到 key 。在 spring 中,key 的生成有两种方式:显式指定和使用 keyGenerator 自动生成。
最后生成的Key为cacheNames :: key,比如sayHello::sha256(自定义shaKeyGenerator 生成的256值)
2.2.1 KeyGenerator 自动生成
当我们在声明 @Cacheable 时不指定 key 参数,则该缓存名下的所有 key 会使用 KeyGenerator 根据参数 自动生成。spring 有一个默认的 SimpleKeyGenerator ,在 spring boot 自动化配置中,这个会被默认注入。生成规则如下:
a. 如果该缓存方法没有参数,返回 SimpleKey.EMPTY ;
b. 如果该缓存方法有一个参数,返回该参数的实例 ;
c. 如果该缓存方法有多个参数,返回一个包含所有参数的 SimpleKey ;
默认的 key 生成器要求参数具有有效的 hashCode() 和 equals() 方法实现。另外,keyGenerator 也支持自定义, 并通过 keyGenerator 来指定。
不过我们也可以继承CachingConfigurerSupport来配置自定义KeyGenerator方法如下:
首先导入依赖:
<!-- fastJson序列化依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<!-- 用于摘要运算、编码解码的包 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
/**
* 自定义缓存 key 生成策略
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String,Object> container = new HashMap<>(3);
Class<?> targetClass = target.getClass();
// 类地址
container.put("class",targetClass.toGenericString());
// 方法名称
container.put("methodName",method.getName());
// 包名称
container.put("package",targetClass.getPackage());
// 参数列表
for(int i=0; i<params.length; i++){
container.put(String.valueOf(i),params[i]);
}
// 转为 JSON 字符串
String jsonString = JSON.toJSONString(container);
// System.out.println(jsonString);
// 做 SHA256 哈希得到一个 SHA256 摘要作为 key
return DigestUtils.sha256Hex(jsonString);
};
}
}
2.2.2 显式指定 key
相较于使用 KeyGenerator 生成,spring 官方更推荐显式指定 key 的方式,即指定 @Cacheable 的 key 参数。
即便是显式指定,但是 key 的值还是需要根据参数的不同来生成,那么如何实现动态拼接呢?SpEL(Spring Expression Language,Spring 表达式语言) 能做到这一点。
显示指定的好处在于,直观明了,看到代码就能想象生成的 key 是什么样。而且 SpEL 也很强大。关于 SpEL 的详细用法,这里不详述,可以参考官方文档:
注意:官方说 key 和 keyGenerator 参数是互斥的,同时指定两个会导致异常。
2.2.3 condition
SpEL 表达式,用于条件过滤,表示当满足该条件的情况下才进行缓存;true或者false,只有为true时才进行缓存;
2.3 @CacheEvict
该注解用于缓存的清除,由于该注解的参数与Cacheable的参数大部分都是相同的,这里来简单介绍下CacheEvict注解独有的两个参数:
allEntries,是否清空所有的缓存内容,默认为false,如果指定为 true,则方法调用后将清空所有缓存;不过不允许在该值设置为true的情况下,再设置key的值;
beforeInvocation,是否在调用该方法之前清空缓存,默认为false;如果为true,在该方法被调用前就清空缓存,不用考虑该方法的执行结果(即不考虑是否抛出异常);而默认情况下,如果方法执行时发生异常,则不会清除缓存;
为了更好的查看效果,我们先引入 Redis,将SpringCache缓存在Redis,而不是使用内置的缓存。
2.4 使用Redis做缓存
2.4.1 Redis依赖
<!-- 引入Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis连接池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.4.2 Redis 配置
application.properties:
#Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
# 超时设置 5000ms
spring.redis.lettuce.pool.max-wait=5000
删除CacheConfig.java文件, 新建RedisConfig
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean(name = "redisTemplate")
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Object> template = new RedisTemplate<>();
//Value序列化方法
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
//Value值采用 FastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
//Key序列化方法
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//Key值采用 StringRedisSerializer
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// 全局开启AutoType,这里方便开发,使用全局模式
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// 建议使用这种方式,小范围指定白名单
//ParserConfig.getGlobalInstance().addAccept("com.xxx.xxx");
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
/**
* 自定义缓存 key 生成策略
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String,Object> container = new HashMap<>(3);
Class<?> targetClass = target.getClass();
// 类地址
container.put("class",targetClass.toGenericString());
// 方法名称
container.put("methodName",method.getName());
// 包名称
container.put("package",targetClass.getPackage());
// 参数列表
for(int i=0; i<params.length; i++){
container.put(String.valueOf(i),params[i]);
}
// 转为 JSON 字符串
String jsonString = JSON.toJSONString(container);
// System.out.println(jsonString);
// 做 SHA256 哈希得到一个 SHA256 摘要作为 key
System.out.println(DigestUtils.sha256Hex(jsonString));
return DigestUtils.sha256Hex(jsonString);
};
}
/**
* Value序列化器
*/
class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
private final Class<T> clazz;
FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
/**
* Serialize the given object to binary data.
*
* @param t object to serialize. Can be {@literal null}.
* @return the equivalent binary data. Can be {@literal null}.
*/
@Override
public byte[] serialize(T t) throws SerializationException {
if(t == null){
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8);
}
/**
* Deserialize an object from the given binary data.
*
* @param bytes object binary representation. Can be {@literal null}.
* @return the equivalent object instance. Can be {@literal null}.
*/
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if(bytes == null || bytes.length <=0){
return null;
}
String str = new String(bytes,StandardCharsets.UTF_8);
return JSON.parseObject(str,clazz);
}
}
/**
* Key序列化器
*/
class StringRedisSerializer implements RedisSerializer<Object>{
private final Charset charset; // 编码
StringRedisSerializer(){ this(StandardCharsets.UTF_8); }
private StringRedisSerializer(Charset charset) {
Assert.notNull(charset,"Charset must not be null!");
this.charset = charset;
}
/**
* Serialize the given object to binary data.
*
* @param object object to serialize. Can be {@literal null}.
* @return the equivalent binary data. Can be {@literal null}.
*/
@Override
public byte[] serialize(Object object) throws SerializationException {
String string = JSON.toJSONString(object);
if(StringUtil.isNullOrEmpty(string)){
return null;
}
string = string.replace("\"","");
return string.getBytes(charset);
}
/**
* Deserialize an object from the given binary data.
*
* @param bytes object binary representation. Can be {@literal null}.
* @return the equivalent object instance. Can be {@literal null}.
*/
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
return (bytes == null ? null:new String(bytes,charset));
}
}
}
因为使用Redis后,RedisCacheConfiguration类注册了RedisCacheManager, 因此SpringCache内置的被替换成了使用RedisCacheManager, 即可以使用Redis做缓存了,非常方便。但是会发现默认的RedisCacheManager并没有使用我们自定义的序列化器,因此要使用我们自定义的序列化器,还需要自定义RedisCacheManager。
2.4.3 SpringCache使用自定义序列化器
重写自己的 RedisCacheConfiguration
如下:(使用默认的序列化方法会存储字节码,不便于查看)
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* 设置 Redis数据默认过期时间, 默认6小时(不会影响正常 Redis 的过期时间)
* 设置 @cacheable 序列化方式 (spring cache 序列化方式)
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(){
// Value 序列化器
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer));
// 默认 Spring Cache 使用 Redis 的过期时间是 6小时 (不会影响正常 Redis 的过期时间)
configuration = configuration.entryTtl(Duration.ofHours(6));
return configuration;
}
...
}
测试:
@Test
void redisTemplateTest01(){
redisTemplate.opsForValue().set("hello","hello"); //普通的Redis操作
}
结果:
127.0.0.1:6379> KEYS *
1) "hello"
127.0.0.1:6379> get hello
"\"hello\""
127.0.0.1:6379> TTL hello
(integer) -1 //不会设置6小时过期
测试Spring Cache:
@Cacheable(value = "sayHello", key = "#text")
public String sayHello(String text){
System.out.println("call sayHello...");
return text;
}
调用后结果:
127.0.0.1:6379> KEYS *
1) "hello"
2) "sayHello::hello"
127.0.0.1:6379> ttl sayHello::hello //@Cacheable 默认过期时间 6 小时
(integer) 21079
127.0.0.1:6379> GET sayHello::hello //查看不会是字节乱码
"\"hello\""
127.0.0.1:6379>
3. 继续SpringCache的使用
3.1 @CacheEvict
该注解用于缓存的清除,由于该注解的参数与Cacheable的参数大部分都是相同的,这里来简单介绍下CacheEvict注解独有的两个参数:
allEntries,是否清空所有的缓存内容,默认为false,如果指定为 true,则方法调用后将清空所有缓存;不过不允许在该值设置为true的情况下,再设置key的值;
beforeInvocation,是否在调用该方法之前清空缓存,默认为false;如果为true,在该方法被调用前就清空缓存,不用考虑该方法的执行结果(即不考虑是否抛出异常);而默认情况下,如果方法执行时发生异常,则不会清除缓存;
多个缓存使用同一个value,可以发现缓存的Key 是不一样的,是根据最后生成的Key为cacheNames :: key,比如sayHello::sha256(自定义shaKeyGenerator 生成的256值)的。
如上采用自定义 KeyGenerator 发现无法删除成功,因为此时的方法名不一致,当然产生的sha256不一样,因此删除时产生Redis的key并不在Redis中存在。因此需要采用显示指定key来覆盖自定义 KeyGenerator ,只要显示指定key自定义的KeyGenerator就会失效。
重定义方法如下:
@Service("helloService")
@Log4j2
public class HelloServiceImpl {
@Cacheable(value = "sayHello", key = "#text")
public String sayHello(String text){
System.out.println("call sayHello...");
return text;
}
@Cacheable(value = "sayHello",key = "#text")
public String sayHello2(String text){
System.out.println("call sayHello2...");
return text;
}
@CacheEvict(value = "sayHello",key = "#delText",allEntries = false)
public void delHello(String delText){
System.out.println("call delHello...");
}
}
测试:
@Test
void springCacheTest01(){
System.out.println(helloService.sayHello("hello"));
System.out.println(helloService.sayHello("hello"));
System.out.println(helloService.sayHello("hello"));
System.out.println(helloService.sayHello2("hello2"));
System.out.println(helloService.sayHello2("hello2"));
System.out.println(helloService.sayHello2("hello2"));
}
结果:
127.0.0.1:6379> KEYS *
1) "sayHello::hello"
2) "sayHello::hello2"
再测试删除:
@Test
void springCacheTest02(){
helloService.delHello("hello2");
}
结果:
127.0.0.1:6379> KEYS *
1) "sayHello::hello" //"sayHello::hello2"被删除
3.1.1 allEntries属性
再重新恢复缓存,设置allEntries:true:
@CacheEvict(value = "sayHello",key = "#delText",allEntries = true)
public void delHello(String delText){
System.out.println("call delHello...");
}
执行清除缓存,结果:
127.0.0.1:6379> KEYS *
1) "sayHello::hello"
2) "sayHello::hello2"
127.0.0.1:6379> KEYS *
(empty list or set) //清空所有的缓存内容,即所有sayHello::*
127.0.0.1:6379>
3.1.2 beforeInvocation属性
清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
3.2 @CachePut
配置于函数上,能够根据参数定义条件进行缓存,与@Cacheable不同的是,每次都会真实调用函数,所以主要用于数据新增和修改操作上。
@CachePut(value = "sayHello",key = "#changeText")
public String changeHello(String changeText){
System.out.println("call changeHello...");
return changeText+changeText;
}
测试:
@Test
void springCacheTest03(){
helloService.changeHello("hello");
}
结果:
127.0.0.1:6379> GET sayHello::hello // 发生更新
"\"hellohello\""
127.0.0.1:6379> ttl sayHello::hello // ttl 重置为 6小时
(integer) 21589
3.3 @CacheConfig
所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了, 所以,有了@CacheConfig这个配置。
@CacheConfig是一个类级别的注解。
@Service("helloService")
@Log4j2
@CacheConfig(cacheNames = "sayHello")
public class HelloServiceImpl {
...
}
这样就不需要在每个类方法上申明cacheNames或者value了。但是如果在方法上继续又声明了cacheNames或者value则方法注解覆盖类注解。
3.4 @Caching
有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。
@Caching(put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
})
public User save(User user) {
}
3.5 cacheManager属性
当同时使用了多种缓存技术,需要定义多个cacheManager, 同时使用@Primary指定一个主Bean。 此时 spring cache注解可以使用此字段决定缓存到哪种缓存中。
常规的SpringBoot
已经为我们自动配置了EhCache
、Collection
、Guava
、ConcurrentMap
等缓存,默认使用ConcurrentMapCacheManager
。