如何使用spring-aop实现简单的自定义缓存框架

阅读:475
作者:majingjing
发布:2019-09-24 09:54:53

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

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

自定义缓存实现

定义缓存注解 @MajjCache
/**
 * 缓存标示注解
 *
 * @Author JingjingMa
 * @Date 2019/9/15 12:40
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MajjCache {

    /**
     * 缓存键的名称
     * 支持占位符替换
     * <p>
     * ${c} = className
     * ${m} = methodName
     * ${p} = param[0].toString+param[1].toString+...+param[n].toString
     * ${cmp} = ${c}.${m}[${p}]
     * #{\d} = p[\d] 比如param[0]第0个参数值
     * </p>
     *
     * @return
     */
    String name() default "";

    /**
     * 缓存时间(单位秒s,默认1小时)
     * ps: 如果type=Request,则此处的ttl无效
     *
     * @return
     */
    int ttl() default 60 * 60;

    /**
     * 缓存类型(默认Redis)
     *
     * @return
     */
    Type type() default Type.Redis;

    /**
     * 缓存类型
     */
    enum Type {
        Redis, Request
    }

}

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

  • cache-key 支持表达式
  • 根据动态key设置有效期
  • 支持缓存方式[Redis,Request,...]
利用aop功能,织入缓存
@Component
@Aspect
@Slf4j
public class MajjCacheAop {

    @Autowired
    StringRedisTemplate redisTemplate;

    Pattern pattern = Pattern.compile("\\#\\{\\d+\\}");

    /**
     * 定义AOP扫描路径
     * 第一个注解只扫描aopTest方法
     */
    @Pointcut(value = "@annotation(cn.majingjing.tmblog.common.anno.MajjCache)")
    public void cache() {
        log.info("---MajjCacheAop-cache()---");
    }

    @Around("cache()")
    public Object around(ProceedingJoinPoint joinPoint) throws Exception {
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            String className = method.getDeclaringClass().getName();
            String methodName = method.getName();
            Object[] args = joinPoint.getArgs();
            Class<?> returnType = method.getReturnType();

            MajjCache majjCache = method.getAnnotation(MajjCache.class);
            String cacheKey = cacheKey(majjCache, className, methodName, args);

            Object proceed;
            if (cacheKey.length() > 0) {
                //从缓存中取数据
                proceed = cacheGet(majjCache, cacheKey, returnType);
                if (Tools.isNull(proceed)) {
                    //从实际方法中获取数据
                    proceed = joinPoint.proceed();

                    //往缓存中写数据
                    cacheSet(majjCache, cacheKey, proceed);
                }
            } else {
                log.warn("{}.{}的缓存配置错误,请检查name?", className, methodName);
                //从实际方法中获取数据
                proceed = joinPoint.proceed();
            }

            return proceed;
        } catch (Throwable e) {
            log.error("", e);
            throw new Exception(e);
        }
    }

    /**
     * 往缓存中写数据
     *
     * @param majjCache
     * @param cacheKey
     * @param cacheObject
     */
    private void cacheSet(MajjCache majjCache, String cacheKey, Object cacheObject) {
        if (majjCache.type().equals(MajjCache.Type.Redis)) {
            String _cacheObject = JSONObject.toJSONString(cacheObject);
            redisTemplate.opsForValue().set(cacheKey, _cacheObject, majjCache.ttl(), TimeUnit.SECONDS);
        } else if (majjCache.type().equals(MajjCache.Type.Request)) {
            HttpServletRequest request = SpringContextServletHolder.getRequest();
            request.setAttribute(cacheKey, cacheObject);
        }
    }

    /**
     * 从缓存中获取数据
     *
     * @param cacheKey
     * @param majjCache
     * @return
     */
    private <T> T cacheGet(MajjCache majjCache, String cacheKey, Class<T> clz) {
        if (majjCache.type().equals(MajjCache.Type.Redis)) {
            // 没有被缓存
            if (!redisTemplate.hasKey(cacheKey)) {
                return null;
            }
            String cacheObject = redisTemplate.opsForValue().get(cacheKey);
            T t = JSONObject.parseObject(cacheObject, clz);
            return t;
        } else if (majjCache.type().equals(MajjCache.Type.Request)) {

            HttpServletRequest request = SpringContextServletHolder.getRequest();
            Object cacheObject = request.getAttribute(cacheKey);
            return (T) cacheObject;
        }
        return null;
    }

    /**
     * cache-key 生成
     *
     * @param majjCache
     * @param className
     * @param methodName
     * @param args
     * @return
     */
    private String cacheKey(MajjCache majjCache, String className, String methodName, Object... args) {
        String cacheName = majjCache.name();
        if (Tools.isEmpty(cacheName)) {
            return "";
        }

        //${}
        if(cacheName.contains("${")){
            // ${cmp}
            if (majjCache.name().contains("${cmp}")) {
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append(className);
                stringBuilder.append(".");
                stringBuilder.append(methodName);
                stringBuilder.append("[");
                for (Object obj : args) {
                    stringBuilder.append(obj.toString());
                }
                stringBuilder.append("]");

                cacheName = cacheName.replace("${cmp}", stringBuilder.toString());
            }
            // ${c}
            if (majjCache.name().contains("${c}")) {
                cacheName = cacheName.replace("${c}", className);
            }
            // ${m}
            if (majjCache.name().contains("${m}")) {
                cacheName = cacheName.replace("${m}", methodName);
            }
            // ${p}
            if (majjCache.name().contains("${p}")) {
                StringBuilder stringBuilder = new StringBuilder();
                for (Object obj : args) {
                    stringBuilder.append(obj.toString());
                }
                cacheName = cacheName.replace("${p}", stringBuilder.toString());
            }
        }

        //#{}
        if(cacheName.contains("#{")){
            Matcher matcher = pattern.matcher(cacheName);
            while (matcher.find()) {
                String key = matcher.group();
                int argIndex = Integer.parseInt(key.substring(2, key.length() - 1));
                System.out.println(key + "\t" + argIndex);
                cacheName = cacheName.replace(key, args[argIndex].toString());
            }
        }


        return cacheName;
    }

}

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

  • 简单的使用 StringRedisTemplate 来完成复合对象的存储,实际项目可自定义数据类型的模版
  • 可以根据不同的 MajjCache.Type 来写个多态
  • cache-key 生成可以参考SpEL表达式
  • 需考虑异常的处理
织入缓存切入点,在需要的方法上增加缓存注解
@FeignClient(value = "xxx",path = "/xxx-xxx")
public interface BlogService extends BlogApi {
    @Override
    @MajjCache(name = IConstants.CacheKey.BLOG_HOTLIST, ttl = 24*60*60)
    @GetMapping("/blog/aaa")
    List<xxx> aaa(@RequestParam(value = "rows", defaultValue = "10") Integer rows);

    @Override
    //@MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 60*60)
    xxx bbb(@RequestParam(value = "aid") String aid,
                   @RequestParam(value = "token") String token);

    @Override
    @MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 60)
    xxx ccc(@PathVariable(value = "aid") String aid);

    @Override
    @MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 10)
    xxx prev(@PathVariable(value = "aid") String aid);

    @Override
    @MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 5)
    xxx ddd(@PathVariable(value = "aid") String aid);

}

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