AOP加自定义注解实现redis注解缓存

  很早之前学习了redis,在这次毕业设计中使用redis对热点数据进行缓存,其中使用spring-data-redis还有@Cacheable、@CachePut和@CacheEvict这3个注解。注解的方式避免了在业务方法中参杂太多非业务的逻辑代码,比如日志和事务,都会用AOP配合注解把共用部分抽离出来,同时业务方法也更纯粹简洁。在使用时发现,spring-data-redis虽是强大,但是许多功能用不上,我只需要简单的缓存添加、删除功能,于是基于自定义注解和AOP知识实现了3个缓存注解。


关键点

1、@Around("@annotation(myCacheAnnotation.Cache)"),使用spring的@Around注解,对我自定义的注解类Cache进行切入,也就是说,凡是使用到@Cache这个注解的方法,执行时会先执行@Around下的方法,这样就可以把之前写在业务方法中的缓存逻辑移动到这里,比如获取数据前先到redis服务器获取缓存,缓存不存在再到数据库中去获取;
2、Method m = ((MethodSignature) pjp.getSignature()).getMethod(); 这行代码中的m是代理对象,没有包含原方法上的注解;Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());ProceedingJoinPoint(上面的pjp)的this()返回spring生成的代理对象,target()返回被代理的目标对象,目标对象反射获取的method对象才包含注解;
3、java本身的反射功能中不能获取方法参数名,借助LocalVariableTableParameterNameDiscoverer类可以根据一个Method对象获取其方法参数名。

核心代码

AOP切面类

package myCacheAnnotation;

import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import service.IUserService;

import java.lang.reflect.Method;

/**
 * writer: holien
 * Time: 2018-01-18 11:20
 * Intent: aop配合cache注解实现缓存
 */
@Component
@Aspect
public class CacheAspect {

    @Autowired
    private JedisPool jedisPool;

    // 在使用Cache注解的地方切入此切点
    @Around("@annotation(myCacheAnnotation.Cache)")
    private Object handleCache(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("以下是缓存逻辑");
        // 获取切入的方法对象
        // 这个m是代理对象的,没有包含注解
        Method m = ((MethodSignature) pjp.getSignature()).getMethod();
        // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
        Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
        // 根据目标方法对象获取注解对象
        Cache cacheAnnotation = methodWithAnnotations.getDeclaredAnnotation(myCacheAnnotation.Cache.class);
        // 解析key
        String keyExpr = cacheAnnotation.key();
        Object[] as = pjp.getArgs();
        String key = parseKey(methodWithAnnotations, as, keyExpr);
        // 注解的属性本质是注解里的定义的方法
//        Method methodOfAnnotation = a.getClass().getMethod("key");
        // 注解的值本质是注解里的定义的方法返回值
//        String key = (String) methodOfAnnotation.invoke(a);
        // 到redis中获取缓存
        Jedis jedis = jedisPool.getResource();
        String cache = jedis.get(key);
        if (cache == null) {
            // 若不存在,则到数据库中去获取
            Object result = pjp.proceed();
            // 从数据库获取后存入redis
            System.out.println("从数据库获取的结果以JsonString形式存入redis中");
            jedis.set(key, JSON.toJSONString(result));
            // 若有指定过期时间,则设置
            int expireTime = cacheAnnotation.expire();
            if (expireTime != -1) {
                jedis.expire(key, expireTime);
            }
            return result;
        } else {
            return JSON.parse(cache);
        }
    }

    // 参数2为方法参数值,参数3为注解中某个属性的值,若含有#则为一个表达式
    // 待解决,解析#user.id的问题
    private String parseKey(Method method, Object[] argValues, String expr) {
        if (expr.contains("#")) {
            String paramName = expr.substring(expr.indexOf('#') + 1);
            // 获取方法参数名列表
            LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
            String[] paramNames = discoverer.getParameterNames(method);
            for (int i = 0; i < paramNames.length; i++) {
                if (paramNames[i].equals(paramName)) {
                    return expr.substring(0, expr.indexOf('#')) + argValues[i].toString();
                }
            }
            throw new IllegalArgumentException("解析不了该参数,错误参数表达式");
        } else {
            // 不需要解析,直接返回
            return expr;
        }
    }

    // 待解决,解析#user.id的问题
    @Around("@annotation(myCacheAnnotation.CachePut)")
    private Object handleCachePut(ProceedingJoinPoint pjp) throws Throwable {
        return null;
    }

    @Around("@annotation(myCacheAnnotation.CacheEvict)")
    private Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("以下是删除缓存逻辑");
        // 获取切入的方法对象
        // 这个m是代理对象的,没有包含注解
        Method m = ((MethodSignature) pjp.getSignature()).getMethod();
        // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
        Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
        // 根据目标方法对象获取注解对象
        CacheEvict cacheEvictAnnotation = methodWithAnnotations.getDeclaredAnnotation(myCacheAnnotation.CacheEvict.class);
        // 解析key
        String keyExpr = cacheEvictAnnotation.key();
        Object[] as = pjp.getArgs();
        String key = parseKey(methodWithAnnotations, as, keyExpr);
        // 先删除数据库中的用户信息再删除缓存
        Object result = pjp.proceed();
        Jedis jedis = jedisPool.getResource();
        jedis.del(key);
        System.out.println("删除缓存中的用户信息");
        return result;
    }

    public static void main(String[] args) throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        IUserService userService = (IUserService) context.getBean("userService");
//        System.out.println(userService.getUserInfo(10));
        userService.deleteUserInfo(10);
    }

}

Cache注解类

package myCacheAnnotation;

import java.lang.annotation.*;

/**
 * writer: holien
 * Time: 2018-01-18 10:49
 * Intent: 自定义缓存注解(String、hash)
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
    String key();
    int expire() default -1;
}

不足与补充

此注解还可以使用Guava包中的布隆过滤器,对数据库和缓存中都不存在的查询放进过滤器,防止缓存击穿攻击;本来想借助SpEL来解析注解参数值,但是没有试验成功,只好自己写了个简单的对#XXX格式的参数进行解析,XXX只能是方法的其中一个参数名,不能是参数的属性或方法返回值,即不能解析#user.id或#user.getId();

总结
其实重复造轮子是没有必要的,但是以学习或特定业务为目的造个小轮子是值得的,这次的学习也让我体会到AOP和注解的强大之处,站在伟人的肩膀上看得更远。


你可能感兴趣的:(redis,spring)