如何使用spring-aop实现简单的自定义缓存框架
分类:软件编程
阅读:21
作者:皇太极
发布:2019-09-24 09:54:53

spring 集成的一套标准的cache框架不能很好的定义缓存的有效期, 必须提前单独的对每个缓存key指定过期时间.这个操作还是不太灵活.

下面将利用spring-aop (aspectj) 来完成自定义缓存框架的简单实现

自定义缓存实现

定义缓存注解 @MajjCache
  1. /**
  2. * 缓存标示注解
  3. *
  4. * @Author JingjingMa
  5. * @Date 2019/9/15 12:40
  6. */
  7. @Target({ElementType.METHOD})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Documented
  10. public @interface MajjCache {
  11. /**
  12. * 缓存键的名称
  13. * 支持占位符替换
  14. * <p>
  15. * ${c} = className
  16. * ${m} = methodName
  17. * ${p} = param[0].toString+param[1].toString+...+param[n].toString
  18. * ${cmp} = ${c}.${m}[${p}]
  19. * #{\d} = p[\d] 比如param[0]第0个参数值
  20. * </p>
  21. *
  22. * @return
  23. */
  24. String name() default "";
  25. /**
  26. * 缓存时间(单位秒s,默认1小时)
  27. * ps: 如果type=Request,则此处的ttl无效
  28. *
  29. * @return
  30. */
  31. int ttl() default 60 * 60;
  32. /**
  33. * 缓存类型(默认Redis)
  34. *
  35. * @return
  36. */
  37. Type type() default Type.Redis;
  38. /**
  39. * 缓存类型
  40. */
  41. enum Type {
  42. Redis, Request
  43. }
  44. }

从上述的注解可以看出支持的功能还是很强的,后续还可以不断增强

  • cache-key 支持表达式
  • 根据动态key设置有效期
  • 支持缓存方式[Redis,Request,…]
利用aop功能,织入缓存
  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class MajjCacheAop {
  5. @Autowired
  6. StringRedisTemplate redisTemplate;
  7. Pattern pattern = Pattern.compile("\\#\\{\\d+\\}");
  8. /**
  9. * 定义AOP扫描路径
  10. * 第一个注解只扫描aopTest方法
  11. */
  12. @Pointcut(value = "@annotation(cn.majingjing.tmblog.common.anno.MajjCache)")
  13. public void cache() {
  14. log.info("---MajjCacheAop-cache()---");
  15. }
  16. @Around("cache()")
  17. public Object around(ProceedingJoinPoint joinPoint) throws Exception {
  18. try {
  19. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  20. Method method = signature.getMethod();
  21. String className = method.getDeclaringClass().getName();
  22. String methodName = method.getName();
  23. Object[] args = joinPoint.getArgs();
  24. Class<?> returnType = method.getReturnType();
  25. MajjCache majjCache = method.getAnnotation(MajjCache.class);
  26. String cacheKey = cacheKey(majjCache, className, methodName, args);
  27. Object proceed;
  28. if (cacheKey.length() > 0) {
  29. //从缓存中取数据
  30. proceed = cacheGet(majjCache, cacheKey, returnType);
  31. if (Tools.isNull(proceed)) {
  32. //从实际方法中获取数据
  33. proceed = joinPoint.proceed();
  34. //往缓存中写数据
  35. cacheSet(majjCache, cacheKey, proceed);
  36. }
  37. } else {
  38. log.warn("{}.{}的缓存配置错误,请检查name?", className, methodName);
  39. //从实际方法中获取数据
  40. proceed = joinPoint.proceed();
  41. }
  42. return proceed;
  43. } catch (Throwable e) {
  44. log.error("", e);
  45. throw new Exception(e);
  46. }
  47. }
  48. /**
  49. * 往缓存中写数据
  50. *
  51. * @param majjCache
  52. * @param cacheKey
  53. * @param cacheObject
  54. */
  55. private void cacheSet(MajjCache majjCache, String cacheKey, Object cacheObject) {
  56. if (majjCache.type().equals(MajjCache.Type.Redis)) {
  57. String _cacheObject = JSONObject.toJSONString(cacheObject);
  58. redisTemplate.opsForValue().set(cacheKey, _cacheObject, majjCache.ttl(), TimeUnit.SECONDS);
  59. } else if (majjCache.type().equals(MajjCache.Type.Request)) {
  60. HttpServletRequest request = SpringContextServletHolder.getRequest();
  61. request.setAttribute(cacheKey, cacheObject);
  62. }
  63. }
  64. /**
  65. * 从缓存中获取数据
  66. *
  67. * @param cacheKey
  68. * @param majjCache
  69. * @return
  70. */
  71. private <T> T cacheGet(MajjCache majjCache, String cacheKey, Class<T> clz) {
  72. if (majjCache.type().equals(MajjCache.Type.Redis)) {
  73. // 没有被缓存
  74. if (!redisTemplate.hasKey(cacheKey)) {
  75. return null;
  76. }
  77. String cacheObject = redisTemplate.opsForValue().get(cacheKey);
  78. T t = JSONObject.parseObject(cacheObject, clz);
  79. return t;
  80. } else if (majjCache.type().equals(MajjCache.Type.Request)) {
  81. HttpServletRequest request = SpringContextServletHolder.getRequest();
  82. Object cacheObject = request.getAttribute(cacheKey);
  83. return (T) cacheObject;
  84. }
  85. return null;
  86. }
  87. /**
  88. * cache-key 生成
  89. *
  90. * @param majjCache
  91. * @param className
  92. * @param methodName
  93. * @param args
  94. * @return
  95. */
  96. private String cacheKey(MajjCache majjCache, String className, String methodName, Object... args) {
  97. String cacheName = majjCache.name();
  98. if (Tools.isEmpty(cacheName)) {
  99. return "";
  100. }
  101. //${}
  102. if(cacheName.contains("${")){
  103. // ${cmp}
  104. if (majjCache.name().contains("${cmp}")) {
  105. StringBuilder stringBuilder = new StringBuilder();
  106. stringBuilder.append(className);
  107. stringBuilder.append(".");
  108. stringBuilder.append(methodName);
  109. stringBuilder.append("[");
  110. for (Object obj : args) {
  111. stringBuilder.append(obj.toString());
  112. }
  113. stringBuilder.append("]");
  114. cacheName = cacheName.replace("${cmp}", stringBuilder.toString());
  115. }
  116. // ${c}
  117. if (majjCache.name().contains("${c}")) {
  118. cacheName = cacheName.replace("${c}", className);
  119. }
  120. // ${m}
  121. if (majjCache.name().contains("${m}")) {
  122. cacheName = cacheName.replace("${m}", methodName);
  123. }
  124. // ${p}
  125. if (majjCache.name().contains("${p}")) {
  126. StringBuilder stringBuilder = new StringBuilder();
  127. for (Object obj : args) {
  128. stringBuilder.append(obj.toString());
  129. }
  130. cacheName = cacheName.replace("${p}", stringBuilder.toString());
  131. }
  132. }
  133. //#{}
  134. if(cacheName.contains("#{")){
  135. Matcher matcher = pattern.matcher(cacheName);
  136. while (matcher.find()) {
  137. String key = matcher.group();
  138. int argIndex = Integer.parseInt(key.substring(2, key.length() - 1));
  139. System.out.println(key + "\t" + argIndex);
  140. cacheName = cacheName.replace(key, args[argIndex].toString());
  141. }
  142. }
  143. return cacheName;
  144. }
  145. }

由于只是简单示例,此处代码揉杂在一个aop类里面
如果用于生产环境请考虑如下优化

  • 简单的使用 StringRedisTemplate 来完成复合对象的存储,实际项目可自定义数据类型的模版
  • 可以根据不同的 MajjCache.Type 来写个多态
  • cache-key 生成可以参考SpEL表达式
  • 需考虑异常的处理
织入缓存切入点,在需要的方法上增加缓存注解
  1. @FeignClient(value = "xxx",path = "/xxx-xxx")
  2. public interface BlogService extends BlogApi {
  3. @Override
  4. @MajjCache(name = IConstants.CacheKey.BLOG_HOTLIST, ttl = 24*60*60)
  5. @GetMapping("/blog/aaa")
  6. List<xxx> aaa(@RequestParam(value = "rows", defaultValue = "10") Integer rows);
  7. @Override
  8. //@MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 60*60)
  9. xxx bbb(@RequestParam(value = "aid") String aid,
  10. @RequestParam(value = "token") String token);
  11. @Override
  12. @MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 60)
  13. xxx ccc(@PathVariable(value = "aid") String aid);
  14. @Override
  15. @MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 10)
  16. xxx prev(@PathVariable(value = "aid") String aid);
  17. @Override
  18. @MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 5)
  19. xxx ddd(@PathVariable(value = "aid") String aid);
  20. }

如上代码已经完成了,在feignclient 请求服务时,如果有@MajjCache则会优先检查缓存,如果命中则不会调用feign服务.还实现了缓存时间动态指定的功能.