Spring AOP实现Redis缓存方法

发布于 2020-04-23  337 次阅读


对于常常访问的数据,直接用Redis缓存可以很好的减少数据库压力,提升响应速度,实现思路如下

  • 利用注解,标注于需要缓存的方法上,利用AOP截取进行处理
  • 缓存的key名称可为SpringEL表达式,可选是否转为MD5减少key长度,方法的结果则为转换为JSON,所以注意Redis在序列化器要选择Json而不是String
  • 在AOP的环绕通知中,如果缓存存在直接返回该缓存,不存在则缓存当前方法的结果
  • 在进行增删改操作的方法上添加删除缓存的注解,及时清除对应的缓存

首先先新建三个注解,分别是

  • EnableCache:开启缓存
    • 参数expireTime:过期时间
    • 参数useMd5Key:是否使用MD5加密key
    • 参数keyName:key名称,可为EL表达式,格式#{#param} or #{#param.value}
    • 参数condition:缓存条件,EL表达式,比如#{#param.value>5}
  • DisableCache:禁用缓存
  • DeleteCache:删除缓存
    • 参数useMd5Key:是否使用MD5加密key
    • 参数keyName:key名称,可为EL表达式,格式#{#param} or #{#param.value}
    • 参数condition:缓存条件,EL表达式,比如#{#param.value>5}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableCache {
    /**
     * 缓存时间,单位(秒)
     *
     * @return
     */
    long expireTime() default 60 * 60L;

    /**
     * 是否使用MD5的key名称
     *
     * @return
     */
    boolean useMd5Key() default false;

    /**
     * 自定义key名称,可为EL表达式,格式#{#param} or #{#param.value}
     *
     * @return
     */

    String keyName() default "";

    /**
     * 缓存条件,EL表达式,比如#{#param.value>5}
     *
     * @return
     */
    String condition() default "";
}

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeleteCache {
    /**
     * 自定义key名称,可为EL表达式,格式#{#param} or #{#param.value}
     *
     * @return
     */

    String keyName() default "";
    /**
     * 是否使用MD5的key名称
     *
     * @return
     */
    boolean useMd5Key() default false;
    /**
     * 删除缓存条件,EL表达式,比如#{#param.value>5}
     *
     * @return
     */
    String condition() default "";
}

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableCache {
}

然后需要一个工具类SpringExpressionUtil用于解析Spring EL表达式

/**
 * @author : LoneKing
 */
public class SpringExpressionUtil {
    /**
     * 转换EL表达式结果
     *
     * @param key
     * @param method
     * @param args
     * @param useMd5Key
     * @return key
     */
    public static String parseKey(String key, Method method, Object[] args, boolean useMd5Key) {
        Object returnVal = parse(key, method, args);
        //这块这么做,是为了Object和String都可以转成String类型的,可以作为key
        String jsonKey = JsonUtil.serialize(returnVal);
        //转换成md5,是因为redis的key过长,并且这种大key的数量过多,就会占用内存,影响性能
        if (useMd5Key) {
            jsonKey = SecurityUtil.getMD5(jsonKey);
        }
        return returnVal == null ? null : jsonKey;
    }

    /**
     * 获取EL表达式(条件)结果
     *
     * @param key
     * @param method
     * @param args
     * @return true or false
     */
    public static boolean parseCondition(String expression, Method method, Object[] args) {
        if (StringUtils.isEmpty(expression)) {
            return true;
        }
        Object returnVal = parse(expression, method, args);
        return Objects.equals(returnVal, true);
    }

    /**
     * 获取EL表达式结果
     *
     * @param expression
     * @param method
     * @param args
     * @return
     */
    public static Object parse(String expression, Method method, Object[] args) {
        // 获取被拦截方法参数名列表
        LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNameArr = parameterNameDiscoverer.getParameterNames(method);
        // 使用SpringEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        // SpringEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 把方法参数放入SpringEL上下文中
        for (int i = 0; i < paramNameArr.length; i++) {
            context.setVariable(paramNameArr[i], args[i]);
        }
        ParserContext parserContext = new TemplateParserContext();

        Object returnVal = parser.parseExpression(expression, parserContext)
                .getValue(context, Object.class);

        return returnVal;
    }
}

最后就是重要的AOP了,没什么可说的,其中用到的RedisUtil代码就不放了,添加/删除Redis缓存。

注意:如果在控制器上添加EnableCache注解,代表其下所有方法开启缓存,就不要设置keyName这个参数了,默认值为全类名_方法名_参数序列化

/**
 * @author : LoneKing
 */
@Component
@Aspect
@Slf4j
public class RedisCacheAspect {
    private final RedisUtil redisUtil;

    public RedisCacheAspect(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    /**
     * 开启缓存的切点
     */
    @Pointcut("@within(com.loneking.annotation.EnableCache) || @annotation(com.loneking.annotation.EnableCache)")
    private void cachePointcut() {
    }

    /**
     * 删除缓存的切点
     */
    @Pointcut("@within(com.loneking.annotation.DeleteCache) || @annotation(com.loneking.annotation.DeleteCache)")
    private void deleteCachePointcut() {
    }

    /**
     * 环绕通知 开启缓存
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("cachePointcut()")
    public Object enableCache(ProceedingJoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getTarget();
        Signature sig = joinPoint.getSignature();
        MethodSignature ms = (MethodSignature) sig;
        Method method = target.getClass().getMethod(ms.getName(), ms.getParameterTypes());
        var cacheModel = getCacheModel(method, joinPoint);
        Object result;
        try {
            //如果允许缓存
            if (cacheModel.getAllowCache()) {
                //如果缓存条件通过
                if (cacheModel.getCacheCondition()) {
                    //获取redis缓存内容
                    Object cacheData = redisUtil.get(cacheModel.getKeyName());
                    if (cacheData != null) {
                        result = cacheData;
                        log.info("触发缓存:" + cacheModel.getKeyName());
                    } else {
                        result = joinPoint.proceed();
                        redisUtil.set(cacheModel.getKeyName(), result, cacheModel.getExpireTime());
                    }
                } else {
                    result = joinPoint.proceed();
                }
            } else {
                result = joinPoint.proceed();
            }
        } catch (SystemException e) {
            throw e;
        } catch (Exception e) {
            throw e;
        }

        return result;
    }

    /**
     * 环绕通知 删除缓存
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("deleteCachePointcut()")
    public Object deleteCache(ProceedingJoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getTarget();
        Signature sig = joinPoint.getSignature();
        MethodSignature ms = (MethodSignature) sig;
        Method method = target.getClass().getMethod(ms.getName(), ms.getParameterTypes());
        DeleteCache deleteCache = AnnotationUtils.findAnnotation(method, DeleteCache.class);
        if (deleteCache != null) {
            //转换Spring EL表达式
            var delCacheCondition = SpringExpressionUtil.parseCondition(
                    deleteCache.condition(), method, joinPoint.getArgs());
            if (delCacheCondition) {
                String keyName = getKeyName(deleteCache.keyName(), deleteCache.useMd5Key(), method, joinPoint);
                redisUtil.del(keyName);
                log.info("删除缓存:" + keyName);
            }
        }
        Object result;
        result = joinPoint.proceed();
        return result;
    }

    /**
     * 获取key
     *
     * @param useMd5Key
     * @param method
     * @param joinPoint
     * @return
     * @expressionKeyName key名称,可为EL表达式
     */
    private String getKeyName(String expressionKeyName, boolean useMd5Key, Method method, ProceedingJoinPoint joinPoint) {
        String defaultKeyName = joinPoint.getTarget().getClass().toString() + "_" + method.getName() + "_"
                + JsonUtil.serialize(joinPoint.getArgs());
        if (StringUtils.isNotEmpty(expressionKeyName)) {
            //转换Spring EL表达式
            return SpringExpressionUtil.parseKey(
                    expressionKeyName, method, joinPoint.getArgs(), useMd5Key);
        } else {
            if (useMd5Key) {
                return SecurityUtil.getMD5(defaultKeyName);
            } else {
                return defaultKeyName;
            }
        }
    }

    /**
     * 获取方法注解的缓存信息
     *
     * @param method
     * @param joinPoint
     * @return
     */
    private RedisCacheModel getCacheModel(Method method, ProceedingJoinPoint joinPoint) {
        //获取方法上的开启缓存注解
        EnableCache methodEnableCache = AnnotationUtils.findAnnotation(method, EnableCache.class);
        //获取方法上的禁用缓存注解
        DisableCache methodDisableCache = AnnotationUtils.findAnnotation(method, DisableCache.class);
        //获取方法所在类的开启缓存注解
        EnableCache controllerEnableCache = AnnotationUtils.findAnnotation(method.getDeclaringClass(), EnableCache.class);
        //是否启用缓存 该方法未禁用缓存 且 方法/控制器上有开启缓存的注解
        var allowCache = methodDisableCache == null && (controllerEnableCache != null || methodEnableCache != null);
        Type methodReturnType = method.getAnnotatedReturnType().getType();
        //不缓存void返回类型的方法
        allowCache = allowCache && !"void".equals(methodReturnType.getTypeName());
        EnableCache usedEnableCache = null;
        boolean cacheCondition = false;
        long expireTime = 1L;
        String keyName = "";
        //如果允许缓存
        if (allowCache) {
            //优先获取方法上的缓存注解
            usedEnableCache = methodEnableCache != null ? methodEnableCache : controllerEnableCache;
            //获取过期时间
            expireTime = usedEnableCache.expireTime();
            keyName = getKeyName(usedEnableCache.keyName(), usedEnableCache.useMd5Key(), method, joinPoint);
            //转换Spring EL表达式条件
            cacheCondition = SpringExpressionUtil.parseCondition(
                    usedEnableCache.condition(), method, joinPoint.getArgs());

        }
        return new RedisCacheModel(cacheCondition, allowCache, keyName, expireTime);
    }

    @Data
    @AllArgsConstructor
    private class RedisCacheModel {
        /**
         * 缓存条件
         */
        private Boolean cacheCondition;
        /**
         * 允许缓存
         */
        private Boolean allowCache;
        /**
         * key名称
         */
        private String keyName;
        /**
         * 过期时间,单位(秒)
         */
        private Long expireTime;
    }

}

食用方法如下

@Controller
public class HomeController {
    @GetMapping("/cache")
    @ResponseBody
    @EnableCache(keyName = "home_cache_#{#param}", condition = "#{#param!='3'}") //开启缓存
    public String cache(String param) {
        return JsonUtil.serialize(new String[]{"c", "a", "c", "h", "e"});
    }

    @GetMapping("/delCache")
    @ResponseBody
    @DeleteCache(keyName = "home_cache_#{#param}", condition = "#{#param!='5'}") //删除缓存
    public String delCache(String param) { 
        return Result.build().successJson();
    }
}

访问/cache就会发现redis缓存本次结果了了,二次访问就会直接调用redis的缓存

或者

@Controller
@EnableCache  //直接在控制器上开启缓存,其下所有方法将缓存,除非用了DisableCache
public class HomeController {
    @GetMapping("/editor")
    public String editor() {
        return "/modeler";
    }

    @GetMapping("/cache")
    @ResponseBody
    public String cache(String param) {
        return JsonUtil.serialize(new String[]{"c", "a", "c", "h", "e"});
    }

    @GetMapping("/cache2")
    @ResponseBody
    @DisableCache
    public String cache2(String param) {
          return JsonUtil.serialize(new String[]{"c", "a", "c", "h", "e", "2"});
    }
}

这样的话每个方法所缓存的key名称就是全类名_方法名_参数序列化