首页 > 其他分享 >自定义缓存组件 代替 Spring@Cache缓存注解

自定义缓存组件 代替 Spring@Cache缓存注解

时间:2024-09-03 13:53:09浏览次数:11  
标签:缓存 自定义 Spring user key org import id

自定义缓存组件 代替 Spring@Cache缓存注解

    在实现上述功能之前先来点基础的,redis在SpringBoot项目中常规的用法,好对缓存和redis客户端的使用有一定了解。

   1.添加依赖 redis客户端依赖(连接redis服务端必备 )

  1. <!-- 客户端依赖二选一 -->
  2. <dependency>
  3. <groupId>redis.clients</groupId>
  4. <artifactId>jedis</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.lettuce</groupId>
  8. <artifactId>lettuce-core</artifactId>
  9. </dependency>
  10. <!-- redis所需依赖 -->
  11. <dependency>
  12. <groupId>org.springframework.data</groupId>
  13. <artifactId>spring-data-redis</artifactId>
  14. </dependency>
  15. <!-- mysql依赖-->
  16. <dependency>
  17. <groupId>mysql</groupId>
  18. <artifactId>mysql-connector-java</artifactId>
  19. </dependency>
  20. <!-- mybaties 依赖-->
  21. <dependency>
  22. <groupId>org.mybatis.spring.boot</groupId>
  23. <artifactId>mybatis-spring-boot-starter</artifactId>
  24. </dependency>

   2. yaml配置项

  1. server:
  2. port: 8702
  3. spring:
  4. application:
  5. name: eurekaClient8702 #此处切记不能用a_b命名
  6. datasource:
  7. driver-class-name: com.mysql.jdbc.Driver
  8. url: jdbc:mysql://127.0.0.1:3306/test
  9. username: root
  10. password: admin
  11. redis:
  12. host: 192.168.32.130
  13. port: 6379
  14. timeout: 20000

 3. 配置类

  1. /**
  2. * @author Heian
  3. * @time 19/07/07 16:59
  4. * @description:配置类
  5. */
  6. @Configuration
  7. @EnableCaching//开启缓存注解
  8. //@Profile ("single")
  9. public class webconfig {
  10. // 配置Spring Cache注解功能:指定缓存类型redis
  11. @Bean
  12. public CacheManager redisCacheManage(RedisConnectionFactory redisConnectionFactory) {
  13. System.out.println ("-----------------Spring 定义CacheManager的缓存类型-----------------");
  14. RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
  15. RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
  16. RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
  17. return cacheManager;
  18. }
  19. }

下面进行一些简单的案例测试:

测试一:往redis存值(当然,redis服务必须是开启的)

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class CustomersApplicationTests {
  4. // 直接注入StringRedisTemplate,则代表每一个操作参数都是字符串
  5. @Autowired
  6. private StringRedisTemplate stringRedisTemplate;
  7. //测试一:存值
  8. @Test
  9. public void setByCache() {
  10. stringRedisTemplate.opsForValue().set("k1", "我是k1");//如果是中文stringRedisTemplate会默认采用String类的序列化机制
  11. }
  12. }

 测试二:对象缓存功能(先查看缓存中有无该对象,有则直接读取;无则加载到mysql数据库并添加到到redis缓存中)

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class CustomersApplicationTests {
  4. // 参数可以是任何对象,默认由JDK序列化
  5. @Resource
  6. private RedisTemplate<Integer,User> redisTemplate;
  7. @Autowired
  8. private UserService userService;
  9. @Test//对象缓存功能
  10. public void findUser() {
  11. User user = null;
  12. int id = 1;
  13. // 1、 判定缓存中是否存在
  14. user = (User) redisTemplate.opsForValue().get(id);
  15. if (user != null) {
  16. System.out.println("从缓存中读取到值:" + user.toString ());
  17. }else {
  18. // 2、不存在则读取数据库
  19. user = userService.getUserFromDB (id);
  20. // 3、 同步存储value到缓存。
  21. redisTemplate.opsForValue().set(id, user);
  22. System.out.println (user.toString ());
  23. }
  24. }
  25. }
  1. /**
  2. * @author Heian
  3. * @time 19/07/07 17:24
  4. * @description:
  5. */
  6. @Service
  7. public class UserService {
  8. @Autowired
  9. private UserDao userDao;
  10. //从数据库获取代当前User对象
  11. public User getUserFromDB(int id) {
  12. User user = userDao.getUserById (id);
  13. return user;
  14. }
  15. /* @Cacheable 支持如下几个参数:
  16. * value:缓存位置名称,不能为空,如果使用EHCache,就是ehcache.xml中声明的cache的name
  17. * key:缓存的key,默认为空,既表示使用方法的参数类型及参数值作为key,支持Sp EL表达式
  18. * condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持Sp EL表达式
  19. */
  20. // value~单独的缓存前缀
  21. // key缓存key 可以用springEL表达式 cache-1:123
  22. @Cacheable(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
  23. public User findUserById(int id) {
  24. // 读取数据库
  25. User user = new User(id, "Heian",26);
  26. System.out.println("从数据库中读取到数据:" + user);
  27. return user;
  28. }
  29. @CacheEvict(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
  30. public void deleteUserById(int id) {
  31. // 先数据库删除,成功后,删除Cache
  32. // 先判断Cache里面是不是有?有则删除
  33. System.out.println("用户从数据库删除成功,请检查缓存是否清除~~" + id);
  34. }
  35. // 如果数据库更新成功,更新redis缓存
  36. @CachePut(cacheManager = "redisCacheManage", value = "cache-1", key = "#user.id", condition = "#result ne null")
  37. public User updateUser(User user){
  38. // 先更新数据库,更成功
  39. // 更新缓存
  40. // 读取数据库
  41. System.out.println("数据库进行了更新,检查缓存是否一致");
  42. return user; // 返回最新内容,代表更新成功
  43. }
  44. }

备注:此时数据库是有id=1的数据的,但缓存中也没有,ok,执行,第一次执行发现缓存中没有此对象,然后查询mysql数据库将返回的对象添加在缓存中。第二次执行会直接从数据库中查的,如下图。

测试三:利用spring的注解实现缓存功能

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class CustomersApplicationTests {
  4. @Autowired
  5. private UserService userService;
  6. @Test
  7. public void testSpringCache(){
  8. User user = userService.findUserById (1);
  9. System.out.println("测试类:" +user.toString ());
  10. }
  11. }
  1. /**
  2. * @author Heian
  3. * @time 19/07/07 17:24
  4. * @description:
  5. */
  6. @Service
  7. public class UserService {
  8. @Autowired
  9. private UserDao userDao;
  10. /* @Cacheable 支持如下几个参数:
  11. * value:缓存位置名称,不能为空,如果使用EHCache,就是ehcache.xml中声明的cache的name
  12. * key:缓存的key,默认为空,既表示使用方法的参数类型及参数值作为key,支持Sp EL表达式
  13. * condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持Sp EL表达式
  14. */
  15. // value~单独的缓存前缀
  16. // key缓存key 可以用springEL表达式 cache-1:123
  17. @Cacheable(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
  18. public User findUserById(int id) {
  19. // 读取数据库
  20. User user = userDao.getUserById (id);
  21. System.out.println("从数据库中读取到数据:" + user);
  22. return user;
  23. }
  24. @CacheEvict(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
  25. public void deleteUserById(int id) {
  26. // 先数据库删除,成功后,删除Cache
  27. // 先判断Cache里面是不是有?有则删除
  28. System.out.println("用户从数据库删除成功,请检查缓存是否清除~~" + id);
  29. }
  30. // 如果数据库更新成功,更新redis缓存
  31. @CachePut(cacheManager = "redisCacheManage", value = "cache-1", key = "#user.id", condition = "#result ne null")
  32. public User updateUser(User user){
  33. // 先更新数据库,更成功
  34. // 更新缓存
  35. // 读取数据库
  36. System.out.println("数据库进行了更新,检查缓存是否一致");
  37. return user; // 返回最新内容,代表更新成功
  38. }
  39. }

备注:点击执行时,首先会执行service中的查询的代码,然后spring注解@Cacheable会默默的往redis存入缓存,当你执行第二遍时,不在执行service里的逻辑代码,而是直接从缓存中拿到值了。

那么正题来了,如果我们不使用Spring的注解,或者说想使用的更加灵活的使用,又该如何使用呢?肯定又要用到Aop了。现在就对Aop进行一个简单的了解,并且使用Aop自定义一个注解,完成和@Cacheable同样的功能。

Aop概念

       AOP是Spring提供的两个核心功能之一:IOC(控制反转),AOP(Aspect Oriented Programming 面向切面编程);IOC有助于应用对象之间的解耦,AOP可以实现横切关注点和它所影响的对象之间的解耦,它通过对既有的程序定义一个横向切入点,然后在其前后切入不同的执行内容,来拓展应用程序的功能,常见的用法如:打开事务和关闭事物,记录日志,统计接口时间等。AOP不会破坏原有的程序逻辑,拓展出的功能和原有程序是完全解耦的,因此,它可以很好的对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度大大降低,提高了部分程序的复用性和灵活性。

实现aop切面,主要有以下几个关键点需要了解:

  1. @Aspect,此注解将一个类定义为一个切面类;
  2. @Pointcut,此注解可以定义一个切入点,可以是规则表达式,也可以是某个package下的所有函数,也可以是一个注解等,其实就是执行条件,满足此条件的就切入;
  3. 然后可以定义切入位置,我们可以选择在切入点的不同位置进行切入:
  4. @Before在切入点开始处切入内容;
  5. @After在切入点结尾处切入内容;
  6. @AfterReturning在切入点return内容之后切入内容(可以用来对返回值做一些处理);
  7. @Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容;
  8. @AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑;

自定义注解进行一个实现来替代Spring的CacheManage注解

第一:首先自定义注解类,运行于方法之上

  1. package com.example.customers.annotations;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. /**
  7. * @author Heian
  8. * @time 19/07/12 8:21
  9. * @description:自定义缓存组件
  10. */
  11. @Target (ElementType.METHOD)//作用于方法之上
  12. @Retention (RetentionPolicy.RUNTIME)//运行期间生效
  13. public @interface MyRedisCache {
  14. //key值:存在redis的key,可以使用springEL表达式,可以使用方法执行的一些参数
  15. String key();
  16. }

第二:设置切面类,这里可以使用环绕@Around(等价于@Before+@After),逻辑和上述类似,先定义切点,指出切入的对象,然后再你要切入的逻辑,我这里的逻辑和上述类似:

  1.  通过joinpoint拿到签名,然后取得对应注解上的方法和注解标记的参数 
  2. 根据SpringEl提供的类来解析注入值,SpringEL好处可参考博文:https://blog.csdn.net/u011305680/article/details/80271423
  3. 先从缓存中拿值,拿不到从数据库拿,并存于redis,以便于下次取值可以直接去缓存中拿。
  1. package com.example.customers.aspect;
  2. import com.example.customers.annotations.MyRedisCache;
  3. import com.example.customers.entity.User;
  4. import org.aspectj.lang.ProceedingJoinPoint;
  5. import org.aspectj.lang.annotation.Around;
  6. import org.aspectj.lang.annotation.Aspect;
  7. import org.aspectj.lang.annotation.Pointcut;
  8. import org.aspectj.lang.reflect.MethodSignature;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.core.DefaultParameterNameDiscoverer;
  11. import org.springframework.data.redis.core.RedisTemplate;
  12. import org.springframework.expression.EvaluationContext;
  13. import org.springframework.expression.Expression;
  14. import org.springframework.expression.ExpressionParser;
  15. import org.springframework.expression.spel.standard.SpelExpressionParser;
  16. import org.springframework.expression.spel.support.StandardEvaluationContext;
  17. import org.springframework.stereotype.Component;
  18. import java.io.Reader;
  19. import java.lang.reflect.Method;
  20. import java.util.Map;
  21. /**
  22. * @author Heian
  23. * @time 19/07/12 8:30
  24. * @description:自定义注解的切面
  25. */
  26. @Component
  27. @Aspect
  28. public class MyRedisCacheAspect {
  29. @Autowired
  30. private RedisTemplate redisTemplate;
  31. //返回类型任意,方法参数任意,方法名任意
  32. @Pointcut(value = "@annotation(com.example.customers.annotations.MyRedisCache)")
  33. public void myredisPointcut(){
  34. }
  35. @Around ("myredisPointcut()")
  36. public Object myredisAround(ProceedingJoinPoint joinPoint){
  37. Object obj = null;
  38. try {
  39. MethodSignature signature = (MethodSignature)joinPoint.getSignature ();
  40. //取得使用注解的方法 有了方法就能:方法的参数、返回值类型、注解、该方法的所在类等等
  41. Method method =signature.getMethod ();
  42. // 通过反射拿到该方法的注解类的比如PostMapping的方法 参数必须是注解
  43. MyRedisCache myRedisCache = method.getAnnotation (MyRedisCache.class);
  44. String key = myRedisCache.key ();//#{userid}
  45. EvaluationContext context = new StandardEvaluationContext ();
  46. // joinPoint 取该调用注解方法的传来的具体参数的值如:1
  47. Object[] args = joinPoint.getArgs();
  48. DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
  49. // 取该调用注解方法的参数如:id
  50. String[] parameterNames = discover.getParameterNames(method);
  51. for (int i = 0; i < parameterNames.length; i++) {
  52. context.setVariable(parameterNames[i], args[i].toString());// id :1
  53. }
  54. //拿到key值后 然后进行解析
  55. ExpressionParser parser = new SpelExpressionParser ();
  56. Expression expression = parser.parseExpression (key);//"#id" 取得自定义注解的key的内容
  57. String realKey = expression.getValue(context).toString();//映射#id" --> id
  58. // 1、 判定缓存中是否存在
  59. obj = redisTemplate.opsForValue ().get (realKey);
  60. if (obj != null) {
  61. System.out.println("从缓存中读取到值:" + obj);
  62. return obj;
  63. }
  64. // 2、不存在则执行方法,相当于我们Method.invoke(对象,"setName()");
  65. obj = joinPoint.proceed();
  66. //3、并且存于redis中
  67. redisTemplate.opsForValue ().set (realKey,obj);
  68. } catch (Throwable e) {
  69. e.printStackTrace ();
  70. }
  71. return obj;
  72. }
  73. }

第三:定义Controller层类和Service类进行测试

  1. package com.example.customers.controller;
  2. import com.example.customers.annotations.MyRedisCache;
  3. import com.example.customers.entity.User;
  4. import com.example.customers.service.AopService;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.web.bind.annotation.*;
  9. import java.util.concurrent.TimeUnit;
  10. /**
  11. * @author Heian
  12. * @time 19/07/11 14:07
  13. * @description:学习Aop测试
  14. */
  15. @RestController
  16. @RequestMapping("/StudyAop")
  17. public class AopController {
  18. private Logger logger = LoggerFactory.getLogger(AopController.class);
  19. @Autowired
  20. private AopService aopService;
  21. @GetMapping("test1")
  22. public String test1(){
  23. logger.info ("进入test1()方法");
  24. return "欢迎进入Aop的学习1";
  25. }
  26. @GetMapping("test2")
  27. public String test2() throws InterruptedException{
  28. TimeUnit.SECONDS.sleep (5);
  29. return "欢迎进入Aop的学习2";
  30. }
  31. @PostMapping("test3")
  32. public User test3(@RequestParam int id){
  33. User user = aopService.implRedisCache (id);
  34. logger.info ("返回的对象" + user.toString ());
  35. return user;
  36. }
  37. }
  1. package com.example.customers.service;
  2. import com.example.customers.annotations.MyRedisCache;
  3. import com.example.customers.dao.UserDao;
  4. import com.example.customers.entity.User;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Service;
  7. /**
  8. * @author Heian
  9. * @time 19/07/12 11:29
  10. * @description:
  11. */
  12. @Service
  13. public class AopService {
  14. @Autowired
  15. private UserDao userDao;
  16. @MyRedisCache(key = "#id")
  17. public User implRedisCache(int id){
  18. User user = userDao.getUserById (id);
  19. System.out.println ("从数据库中读的对象为"+user.toString ());
  20. return user;
  21. }
  22. }

备注:这里使用Postman测试(我已在本地redis服务清空了数据flushdb,所以第一次访问是没有缓存的)

第一次访问:

第二次访问:(接口耗时也减少了)

ok,至此完成,这就利用AOP实现了两个功能:1.统计接口耗时和访问次数   2.自定义缓存组件。

最近单徐循环的歌曲:水木年华的一首《中学时代》

                          

 

 

原文链接:https://blog.csdn.net/qq_40826106/article/details/95240629

标签:缓存,自定义,Spring,user,key,org,import,id
From: https://www.cnblogs.com/sunny3158/p/18394408

相关文章