Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)

前言
来啦老铁! 笔者学习Spring Boot有一段时间了,附上Spring Boot系列学习文章,欢迎取阅、赐教:
  1. 5分钟入手Spring Boot;
  2. Spring Boot数据库交互之Spring Data JPA;
  3. Spring Boot数据库交互之Mybatis;
  4. Spring Boot视图技术;
  5. Spring Boot之整合Swagger;
  6. Spring Boot之junit单元测试踩坑;
  7. 如何在Spring Boot中使用TestNG;
  8. Spring Boot之整合logback日志;
  9. Spring Boot之整合Spring Batch:批处理与任务调度;
  10. Spring Boot之整合Spring Security: 访问认证;
  11. Spring Boot之整合Spring Security: 授权管理;
  12. Spring Boot之多数据库源:极简方案;
  13. Spring Boot之使用MongoDB数据库源;
  14. Spring Boot之多线程、异步:@Async;
  15. Spring Boot之前后端分离(一):Vue前端;
  16. Spring Boot之前后端分离(二):后端、前后端集成;
  17. Spring Boot之前后端分离(三):登录、登出、页面认证;
  18. Spring Boot之面向切面编程:Spring AOP;
  19. Spring Boot之集成Redis(一):Redis初入门;
  20. Spring Boot之集成Redis(二):集成Redis;
在上一篇文章Spring Boot之集成Redis(二):集成Redis中,我们一起学习了如何使用StringRedisTemplate来与Redis进行交互,但也在文末提到这种方式整体代码还是比较多的,略显臃肿,并剧透了另外一种操作Redis的方式:
基于Spring Cache方式操作Redis! 今天的内容厉害啦,不仅能学会在Spring Boot中更好的使用Redis,还能学习Spring Cache!
我们一起拨开云雾睹青天吧!
代码基于上期使用的Git Hub仓库演进,欢迎取阅:
  • https://github.com/dylanz666/spring-boot-redis-demo
整体步骤
  1. 安装commons-pool2依赖;
  2. 修改application.properties配置;
  3. 学习Spring Cache的缓存注解;
  4. 使用Spring Cache的缓存注解操作Redis;
  5. Spring Cache + Redis 缓存演示;
  6. 配置类方式配置和管理Redis缓存;
  7. 动态缓存有效期的实现;
1. 安装commons-pool2依赖;
由于我们会在application.properties配置文件中配置lettuce类型的redis连接池,因此需要引入新的依赖:
org.apache.commons commons-pool2

记得安装一下依赖:
mvn install -Dmaven.test.skip=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true

2. 修改application.properties配置;
application.properties文件整体样子:
server.port=8080 # 设置Spring Cache的缓存类型为redis spring.cache.type=redis # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务端的host和port spring.redis.host=127.0.0.1 spring.redis.port=6379 # Redis服务端的密码 spring.redis.password=Redis!123 # Redis最大连接数 spring.redis.pool.max-active=8 # 连接池最大连接数(使用负值表示没有限制) spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0 # Redis连接超时时间,单位 ms(毫秒) spring.redis.timeout=5000

特别是spring.cache.type=redis这个配置,指定了redis作为Spring Cache的缓存(还有多种可选的缓存方式,如Simple、none、Generic、JCache、EhCache、Hazelcast等,请读者有需要自行脑补哈)。
3. 学习Spring Cache的缓存注解;
Spring Cache提供了5个缓存注解,通常直接使用在Service类上,各有其作用:
1. @Cacheable; @Cacheable 作用在方法上,触发缓存读取操作。
被作用的方法如果缓存中没有,比如第一次调用,则会执行方法体,执行后就会进行缓存,而如果已有缓存,那么则不再执行方法体;
2. @CachePut; @CachePut 作用在方法上,触发缓存更新操作;
被作用的方法每次调用都会执行方法体;
3. @CacheEvict; @CachePut 作用在方法上,触发缓存失效操作;
也即清除缓存,被作用的方法每次调用都会执行方法体,并且使用方法体返回更新缓存;
4. @Caching; @Caching作用在方法上,注解中混合使用@Cacheable、@CachePut、@CacheEvict操作,完成复杂、多种缓存操作;
比如同一个方法,关联多个缓存,并且其缓存名字、缓存key都不同时,或同一个方法有增删改查的缓存操作等,被作用的方法每次调用都会执行方法体。
5. @CacheConfig; 在类上设置当前缓存的一些公共设置,也即类级别的全局缓存配置。
4. 使用Spring Cache的缓存注解操作Redis;
整体步骤: 1). 修改项目入口类;
2). 编写演示用实体类;
3). 编写演示用Service类;
4). 编写演示用Controller类;
1). 修改项目入口类; 在Spring Boot项目入口类App.java上添加注解:@EnableCaching,App.java类整体如下:
package com.github.dylanz666; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; /** * @author : dylanz * @since : 10/28/2020 */ @SpringBootApplication @EnableCaching public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }

2). 编写演示用实体类; 在domain包内建立演示用实体类User.java,简单撸点代码演示一下:
package com.github.dylanz666.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; import java.io.Serializable; /** * @author : dylanz * @since : 10/31/2020 */ @NoArgsConstructor @AllArgsConstructor @Data @Component public class User implements Serializable { private static final long serialVersionUID = 1L; private String userName; private String roleName; @Override public String toString() { return "{" + "\"userName\":\"" + userName + "\"," + "\"roleName\":" + roleName + "" + "}"; } }

3). 编写演示用Service类; 重点来了,在service包内新建UserService类,编写带有Spring Cache注解的方法们,代码如下:
package com.github.dylanz666.service; import com.github.dylanz666.domain.User; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; /** * @author : dylanz * @since : 10/31/2020 */ @Service public class UserService {@Cacheable(value = "https://www.it610.com/article/content", key = "'id_'+#userName") public User getUserByName(String userName) { System.out.println("io operation : getUserByName()"); //模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName("dylanz"); userInDb.setRoleName("adminUser"); return userInDb; }@CachePut(value = "https://www.it610.com/article/content", key = "'id_'+#user.userName") public User updateUser(User user) { System.out.println("io operation : updateUser()"); //模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName(user.getUserName()); userInDb.setRoleName(user.getRoleName()); return userInDb; }@CacheEvict(value = "https://www.it610.com/article/content", key = "'id_'+#user.userName") public void deleteUser(User user) { System.out.println("io operation : deleteUser()"); }@Caching(cacheable = { @Cacheable(cacheNames = "test", key = "'id_'+#userName") }, put = { @CachePut(value = "https://www.it610.com/article/testGroup", key = "'id_'+#userName"), @CachePut(value = "https://www.it610.com/article/user", key = "#userName") }) public User getUserByNameAndDoMultiCaching(String userName) { System.out.println("io operation : getUserByNameAndDoMultiCaching()"); //模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName("dylanz"); userInDb.setRoleName("adminUser"); return userInDb; } }

简单介绍一下:
  • 在UserService的方法内模拟了一下数据库中的数据,用于模拟从数据库中获取数据,代码为:
//模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName("dylanz"); userInDb.setRoleName("adminUser");

  • 我写了4个方法getUserByName(String userName)、
    updateUser(User user)、deleteUser(User user)、getUserByNameAndDoMultiCaching(String userName)分别用于演示查询缓存、更新缓存、删除缓存,以及同一个方法使用混合缓存注解;
  • 缓存注解中使用了2个基本属性(还有其他属性),value(也可以用cacheNames)和key,key如果是从方法体获取的,则需要在key前加#号,比如key="#userName"或者key = "'id_'+#userName",当然也可以写死如key="test123";
  • 缓存注解中的key属性值可以从对象中获取,如:key = "'id_'+#user.userName";
  • 缓存注解的value(或cacheNames)属性、key属性共同构成了Redis服务端中完整的key,如value = "https://www.it610.com/article/content", key = "test123",则在Redis服务端,其完整key为:content::test123;
  • @Caching中可混合使用缓存注解@Cacheable、@CachePut、@CacheEvict,对应的属性是:cacheable、put、evict,复杂场景有帮助!
3). 编写演示用Controller类;
在controller包内新建UserController.java类,编写4个API,分别使用Service中的4个带有Spring Cache注解的方法们,代码如下:
package com.github.dylanz666.controller; import com.github.dylanz666.domain.User; import com.github.dylanz666.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * @author : dylanz * @since : 10/31/2020 */ @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @GetMapping("") public @ResponseBody User getUserByName(@RequestParam String userName) { return userService.getUserByName(userName); }@PutMapping("") public @ResponseBody User updateUser(@RequestBody User user) { return userService.updateUser(user); }@DeleteMapping("") public @ResponseBody String deleteUser(@RequestBody User user) { userService.deleteUser(user); return "success"; }@GetMapping("/multiCaching") public @ResponseBody User getUserByNameAndDoMultiCaching(@RequestParam String userName) { return userService.getUserByNameAndDoMultiCaching(userName); } }

至此,项目整体结构:
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
项目整体结构
5. Spring Cache + Redis 缓存演示;
1). 启动Redis服务端;
redis-server.exe redis.windows.conf

Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
启动Redis服务端 2). 启动Redis客户端,进入monitor模式; 启动:
redis-cli.exe -h 127.0.0.1 - p 6379

auth Redis!123

在Redis客户端,进入monitor模式,命令:
monitor

Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
进入monitor模式 3). 启动项目; Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
启动项目 4). postman访问Controller中的API;
  • 首先在设置缓存前,直接调用获取user的API(GET方法):http://127.0.0.1:8080/api/user?userName=dylanz
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 1 - postman Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 1 - 监控窗口 我们会看到,第一次调用API时,执行了方法体,并完成了第一次Redis缓存!
  • 调用更新user的API(PUT方法):http://127.0.0.1:8080/api/user
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 2 - postman Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 2 - 监控窗口
  • 更新后,再次调用获取user的API:
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
缓存更新后再次调用API 1 - postman Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
缓存更新后再次调用API 1 - 监控窗口 我们会看到,缓存确实被更新了!
  • 调用删除user的API(DELETE方法):http://127.0.0.1:8080/api/user
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 3 - postman Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 3 - 监控窗口
  • 缓存删除后,再次调用获取user的API:
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
缓存删除后,再次调用获取user的API - postman Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
缓存删除后,再次调用获取user的API - 监控窗口 我们会看到,缓存被删除后,再次获取缓存则会自动再次执行方法体,并完成新一轮的缓存!
  • 调用拥有混合缓存注解的API(GET方法):http://127.0.0.1:8080/api/user/multiCaching?userName=dylanz
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 4 - postman Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
调用API 4 - 监控窗口 【Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)】同时为了更直观的让大家看到缓存有起到效果,我在Service层的方法内,都往控制台打印了一些文字,如:io operation : getUserByName()...
我分别各调用了3次具有更新缓存的API、删除缓存的API、获取缓存的API、混合使用缓存注解的API,控制台打印: Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
控制台打印 我们会发现:更新缓存、删除缓存、混合使用缓存注解的缓存,每次都有执行方法体,而获取缓存只执行了一次方法体,这跟我们之前介绍缓存注解的时候是一致的,再次验证了之前对缓存注解的认识!
6. 配置类方式配置和管理Redis缓存;
在config包内创建Redis配置类RedisConfig.java,并编写如下代码:
package com.github.dylanz666.config; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; 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.serializer.*; import java.time.Duration; /** * @author : dylanz * @since : 10/31/2020 */ @Configuration @EnableCaching public class RedisConfig { private final Duration defaultTtl = Duration.ofMinutes(1); /** * 配置自定义redisTemplate */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisSerializer stringRedisSerializer = new StringRedisSerializer(); RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setValueSerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(stringRedisSerializer); return redisTemplate; }/** * 配置缓存管理器 */ @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisSerializer stringRedisSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(defaultTtl) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer)) .disableCachingNullValues(); return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build(); } }

稍微解读一下: 1). 默认情况下Redis的序列化会使用jdk序列化器JdkSerializationRedisSerializer,而该序列化方式,在存储内容时除了属性的内容外还存了其它内容在里面,总长度长,且不容易阅读,因此我们自定义了序列化策略:redisTemplate(),使用该自定义序列化方式后,存储在Redis的内容就比较容易阅读了!
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
配置前:不容易阅读 Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
配置后:容易阅读 2). Spring Cache注解方式,默认是没有过期概念的,即设置了缓存,则一直有效,这显然不能完全满足使用要求,而RedisConfig中的CacheManager给我们提供了一种方式来设置缓存过期策略!
3). cacheManager()用于配置缓存管理器,我们在方法内配置了一个entryTtl为1分钟,TTL即Time To Live,代表缓存可以生存多久,我没有在cacheManager()方法内指明哪个缓存,因此该TTL是针对所有缓存的,是全局性的,也是我们设置的默认TTL;
4). 我们可以给不同的key设置不同的TTL,但hard code的缓存key就有点多啦,此处不介绍啦!
5). 该配置类使用了注解@EnableCaching,则Spring Boot入口类App.java中的@EnableCaching注解就可以删除啦!
7. 动态缓存有效期的实现;
动态缓存有效期的实现有多种方式,如:
1). 在缓存注解上使用不同的自定义的CacheManager;
2). 自定义Redis缓存有效期注解;
3). 利用在Spring Cache缓存注解中value属性中设置的值,设置缓存有效期;
1). 在缓存注解上使用不同的自定义的CacheManager; 在第6步中,我们使用了一个全局的CacheManager,其实这个可以更灵活,可以用于给不同的缓存设置不同的TTL,也即缓存过期。
我们可以在RedisConfig.java中编写多个不同名字的CacheManager,每个CacheManager使用不同的TTL。
然后缓存注解中使用不同的CacheManager,就能达到不同缓存有不同TTL的目的啦,并且没有在CacheManager中hard code缓存key,演示代码:
  • RedisConfig.java演示代码:
package com.github.dylanz666.config; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; 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.serializer.*; import java.time.Duration; /** * @author : dylanz * @since : 10/31/2020 */ @Configuration @EnableCaching public class RedisConfig { private final Duration defaultTtl = Duration.ofMinutes(1); private final Duration ttl10s = Duration.ofSeconds(10); /** * 配置自定义redisTemplate */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisSerializer stringRedisSerializer = new StringRedisSerializer(); RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setValueSerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(stringRedisSerializer); return redisTemplate; }/** * 配置缓存管理器 */ @Bean @Primary public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisSerializer stringRedisSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(defaultTtl) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer)) .disableCachingNullValues(); return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build(); }@Bean public CacheManager cacheManager10s(RedisConnectionFactory redisConnectionFactory) { RedisSerializer stringRedisSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(ttl10s) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer)) .disableCachingNullValues(); return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build(); } }

注意:当有多个CacheManager时,要设置一个默认的CacheManager,即在默认的CacheManager上添加@Primary注解,否则启动项目会报错!
  • 在想演示的缓存注解上添加cacheManger属性,并指定RedisConfig.java中的CacheManager,如:
@CachePut(value = "https://www.it610.com/article/content", key = "'id_'+#user.userName", cacheManager = "cacheManager10s") public User updateUser(User user) { System.out.println("io operation : updateUser()"); //模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName(user.getUserName()); userInDb.setRoleName(user.getRoleName()); return userInDb; }

那么,使用了cacheManager10s的缓存,就会按我们的设置,只缓存10秒钟,其他没设置cacheManager的缓存,则会缓存1分钟,亲测有效!
自定义cacheManager的方式提供了一定的灵活性,但当缓存有效期越来越多样时,RedisConfig.java中的代码就会越来越多,越来越难以管理。接下来介绍另外一种更为灵活的方式!
2). 自定义Redis缓存有效期注解; 这种解题思路是:自定义Redis缓存有效期注解,通过切面编程,在切面中完成Redis缓存有效期的设置。
关于切面编程,可以参考之前文章:Spring Boot之面向切面编程:Spring AOP; (1). 编写时间单位枚举类;
出于灵活性考虑,我将在自定义的注解中使用2个属性,一个为duration(时间大小)属性,一个为unit(时间单位)属性,duration与unit结合使用,可以灵活的设置缓存的有效期!
项目内新建constant包,在包内新建时间单位枚举类,编写代码如下:
package com.github.dylanz666.constant; /** * @author : dylanz * @since : 11/02/2020 */ public enum DurationUnitEnum { NANO, MILLI, SECOND, MINUTE, HOUR, DAY }

(2). 自定义注解;
项目内新建annotation包,在包内新建注解类Ttl.java(名字随意),编写注解类:
package com.github.dylanz666.annotation; import com.github.dylanz666.constant.DurationUnitEnum; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author : dylanz * @since : 11/01/2020 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface Ttl { //设置缓存有效时间,单位为秒,默认60秒 long duration() default 60; //时间单位,默认为秒,SECOND,可选的有:NANO,MILLI,SECOND,MINUTE,HOUR,DAY DurationUnitEnum unit() default DurationUnitEnum.SECOND; }

(3). 创建切面编程类,并完成自定义注解的逻辑实现;
经过实验,如果没有在切面中对Spring Cache的缓存注解、自定义的动态缓存有效期注解进行排序,则可能出现自定义的注解先执行,Spring Cache的缓存注解后执行,导致缓存有效期被默认的TTL覆盖,即:自定义的注解没起到作用!
因此,需要分别对Spring Cache的缓存注解、自定义的动态缓存有效期注解编写切面类,并使用@Order进行切面排序! 在config包内新建2个切面类:AOPConfig.java和
AOPConfig2.java。
AOPConfig.java代码:
package com.github.dylanz666.config; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; /** * @author : dylanz * @since : 11/02/2020 */ @Configuration @Aspect @Order(1) public class AOPConfig { @Around("@annotation(org.springframework.cache.annotation.Cacheable)") public Object cacheableAop(final ProceedingJoinPoint joinPoint) throws Throwable { return joinPoint.proceed(); }@Around("@annotation(org.springframework.cache.annotation.CachePut)") public Object cachePutAop(final ProceedingJoinPoint joinPoint) throws Throwable { return joinPoint.proceed(); }@Around("@annotation(org.springframework.cache.annotation.CacheEvict)") public Object cacheEvictAop(final ProceedingJoinPoint joinPoint) throws Throwable { return joinPoint.proceed(); }@Around("@annotation(org.springframework.cache.annotation.Caching)") public Object cachingAop(final ProceedingJoinPoint joinPoint) throws Throwable { return joinPoint.proceed(); } }

AOPConfig2.java代码:
package com.github.dylanz666.config; import com.github.dylanz666.annotation.Ttl; import com.github.dylanz666.constant.DurationUnitEnum; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.time.Duration; /** * @author : dylanz * @since : 11/01/2020 */ @Configuration @Aspect @Order(2) public class AOPConfig2 { @Autowired private StringRedisTemplate stringRedisTemplate; @After("@annotation(com.github.dylanz666.annotation.Ttl)") public void simpleAop2(final JoinPoint joinPoint) throws Throwable { //获取一些基础信息 String methodName = joinPoint.getSignature().getName(); Class targetClass = joinPoint.getTarget().getClass(); Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes(); Method objMethod = targetClass.getMethod(methodName, parameterTypes); //获取想要设置的缓存有效时长 Ttl cacheDuration = objMethod.getDeclaredAnnotation(Ttl.class); long duration = cacheDuration.duration(); DurationUnitEnum unit = cacheDuration.unit(); //获取缓存注解的values/cacheNames和key String[] values = getValues(objMethod); String key = getKey(objMethod); //准备好变量名与变量值的上下文绑定 Parameter[] parameters = objMethod.getParameters(); Object[] args = joinPoint.getArgs(); StandardEvaluationContext ctx = new StandardEvaluationContext(); for (int i = 0; i < parameters.length; i++) { ctx.setVariable(parameters[i].getName(), args[i]); }//获取真实的key,而不是变量形式的key ExpressionParser expressionParser = new SpelExpressionParser(); Expression expression = expressionParser.parseExpression(key); String realKey = expression.getValue(ctx, String.class); //设置缓存生效时长,即过期时间 setExpiration(values, realKey, duration, unit); }private void setExpiration(String[] values, String realKey, long duration, DurationUnitEnum unit) throws InterruptedException { Duration expiration; if (unit == DurationUnitEnum.NANO) { expiration = Duration.ofNanos(duration); } else if (unit == DurationUnitEnum.MILLI) { expiration = Duration.ofMillis(duration); } else if (unit == DurationUnitEnum.MINUTE) { expiration = Duration.ofMinutes(duration); } else if (unit == DurationUnitEnum.HOUR) { expiration = Duration.ofHours(duration); } else if (unit == DurationUnitEnum.DAY) { expiration = Duration.ofDays(duration); } else { expiration = Duration.ofSeconds(duration); } for (String value : values) { String redisKey = value + "::" + realKey; stringRedisTemplate.expire(redisKey, expiration); } }private String[] getValues(Method objMethod) { Cacheable cacheable = objMethod.getDeclaredAnnotation(Cacheable.class); CachePut cachePut = objMethod.getDeclaredAnnotation(CachePut.class); CacheEvict cacheEvict = objMethod.getDeclaredAnnotation(CacheEvict.class); //value if (cacheable != null && cacheable.value().length > 0) { return cacheable.value(); } if (cachePut != null && cachePut.value().length > 0) { return cachePut.value(); } if (cacheEvict != null && cacheEvict.value().length > 0) { return cacheEvict.value(); } //cacheNames if (cacheable != null && cacheable.cacheNames().length > 0) { return cacheable.cacheNames(); } if (cachePut != null && cachePut.cacheNames().length > 0) { return cachePut.cacheNames(); } if (cacheEvict != null && cacheEvict.cacheNames().length > 0) { return cacheEvict.cacheNames(); } return new String[]{}; }private String getKey(Method objMethod) { Cacheable cacheable = objMethod.getDeclaredAnnotation(Cacheable.class); CachePut cachePut = objMethod.getDeclaredAnnotation(CachePut.class); CacheEvict cacheEvict = objMethod.getDeclaredAnnotation(CacheEvict.class); if (cacheable != null && !cacheable.key().equals("")) { return cacheable.key(); } if (cachePut != null && !cachePut.key().equals("")) { return cachePut.key(); } if (cacheEvict != null && !cacheEvict.key().equals("")) { return cacheEvict.key(); } return ""; } }

由于@Caching比较复杂,我们没有将Ttl.java应用到@Caching注解哦,读者可自行探索!
(3). 使用自定义的缓存有效期注解;
只需要在有使用@Cacheable、@CachePut、@CacheEvict注解的方法上再添加使用@Ttl注解即可,如:
@Cacheable(value = "https://www.it610.com/article/content", key = "'id_'+#userName") @Ttl(duration = 10, unit = DurationUnitEnum.SECOND) public User getUserByName(String userName) { System.out.println("io operation : getUserByName()"); //模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName("dylanz"); userInDb.setRoleName("adminUser"); return userInDb; }

和:
@CachePut(value = "https://www.it610.com/article/content", key = "'id_'+#user.userName") @Ttl(duration = 1, unit = DurationUnitEnum.HOUR) public User updateUser(User user) { System.out.println("io operation : updateUser()"); //模拟从数据库中获取数据 User userInDb = new User(); userInDb.setUserName(user.getUserName()); userInDb.setRoleName(user.getRoleName()); return userInDb; }

注:如果@Ttl注解内没有duration,则默认为60,如果unit单位没有,则默认为秒,如:
  • @Ttl:代表缓存60秒;
  • @Ttl(duration = 10):代表缓存10秒;
  • @Ttl(duration = 10,DurationUnitEnum.MINUTE):代表缓存10分钟;
大家可以根据自己需要再进行调整。
至此项目整体结构:
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
项目整体结构 演示一下:
Spring|Spring Boot之集成Redis(三)(Spring Cache + Redis)
文章图片
动态缓存有效期演示 同时我们也能在API的返回中,看到缓存失效的效果!
3). 利用在Spring Cache缓存注解中value属性中设置的值,设置缓存有效期; 这种方式同样可以利用切面编程,在获取到value属性后,截取我们想设置的缓存有效期时长,然后设置缓存有效期。
例如,我们有一个Spring Cache缓存注解:
@CacheEvict(value = "https://www.it610.com/article/content#3600", key = "'id_'+#user.userName")

参照自定义注解的代码,我们可以很轻松获取到value为 "content#3600",然后可以截取3600为缓存有效期时长,而单位可以默认设置为秒(根据实际要求设置),最后还是一样去设置redis key的expiration。整体与自定义注解非常像,我们就不做过多介绍啦!
也有使用自定义的RedisCacheManager来实现的,未来有机会再深入哈!
上述3种方式都可以完成动态缓存有效期的设置,而我最喜欢的莫过于:自定义注解的方式,该方式灵活、易于理解,用户编码时也非常简单! 至此Spring Cache + Redis完成灵活的Redis操作到此告一段落啦,Spring Boot项目中使用Redis缓存再上层楼,有没有Get到?
如果本文对您有帮助,麻烦点赞、关注! 谢谢!

    推荐阅读