Spring自定义注解实现redis缓存

一、前言

redis是分布式微服务中必用的基础组件之一,现在国内的大部分项目基本上用到,缓存是其主要作用之一,而在项目中频繁使用set()方法添加注解,会造成代码的重复和臃肿,对于开发经验不足的小白,甚至会因为缓存的添加不当直接影响到正常的业务流程,从而酿成事故,因此成熟的公司都会通过封装基础组件,实现通过注解自动添加redis缓存,本文会从原理出发,带领大家亲自实现自定义注解,完成redis缓存的开发,学会了,你可以在同事面前秀一把了。

二、自定义注解的参数说明

@Target:

注解的作用目标,即注解可以使用的位置,通常有
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包

@Retention 注解的保留位置

用来定义注解的生命周期的,并且在使用时需要指定RetentionPolicy,RetentionPolicy有三种策略,分别是:
SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

@Documented

注解只是用来做标识,没什么实际作用,了解就好。
如果使用@Documented标注了,在生成javadoc的时候就会把@Documented注解给显示出来。

三、自定义redis注解

JhRedisCache——添加redis缓存的注解

/**
 * @author ljx
 * @Description: 添加redis缓存的注解
 * @date 2020/6/9 4:11 下午
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JhRedisCache {
    /*
     * redis缓存中的key,支持spel表达式
     */
    String key() default "";
    /*
     * 缓存时间,默认缓存时间是一天 60*60*24
     */
    long expire() default 86400L;
    /*
     * 如果注解添加在返回list的方法上,则需要通过该字段指定list中的class类型
     */
    Class type() default Object.class;
}

JhRedisCacheEvict——删除redis缓存的注解

/**
 * @author ljx
 * @Description: 删除redis缓存的注解
 * @date 2020/6/10 3:38 下午
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JhRedisCacheEvict {
    String key() default "";
}

四、自定义AOP切面

/**
 * @author ljx
 * @Description: 注解切面
 * @date 2020/6/9 11:12 上午
 */
@Component
@Aspect
public class RedisCacheAspect {
    private static final String SPEL = "#";
    private static final String KEY_SEPARATOR = "_";
    private static final int TWO = 2;
    private static final Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class);

    private RedisClient redisClient;

    private AppInfo appInfo;

    /**
     * @Description: 在使用JhRedisCache注解的地方切入此切点,查询缓存是否在redis中存在,若已存在,则直接返回,否则查询数据库
     * @param pjp 切入点信息
     * @return java.lang.Object 方法返回值
     * @Author: ljx
     * @Date: 2020/6/9 4:20 下午
     */
    @Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCache)")
    private Object handleCache(final ProceedingJoinPoint pjp) throws Throwable {
        // 获取切入的方法对象
        // 这个m是代理对象的,没有包含注解
        Method m = ((MethodSignature) pjp.getSignature()).getMethod();
        // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
        Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
        // 根据目标方法对象获取注解对象
        JhRedisCache cacheAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCache.class);
        // 解析key
        String keyExpr = cacheAnnotation.key();
        Object[] as = pjp.getArgs();
        String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as);
        // 到redis中获取缓存
        String cache = null;
        try {
            cache = redisClient.get(key);
        } catch (Exception e) {
            logger.error("{}查询redis缓存异常:{}",keyExpr,e.getMessage());
        }
        if (StringUtils.isBlank(cache)) {
            // 若不存在,则到数据库中去获取
            Object result = pjp.proceed();
            // 从数据库获取后存入redis,若有指定过期时间,则设置
            try {
                long expireTime = cacheAnnotation.expire();
                if (expireTime > 0) {
                    redisClient.set(key,JSON.toJSONString(result), expireTime, TimeUnit.SECONDS);
                }else{
                    redisClient.set(key, JSON.toJSONString(result));
                }
            } catch (Exception e) {
                logger.warn("{}{}缓存redis异常:{}",keyExpr,e.getMessage(),result);
            }
            return result;
        }
        // 得到被代理的方法上的注解
        Class modelType = cacheAnnotation.type();
        // 得到被代理方法的返回值类型
        Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType();
        // 返回反序列化从缓存中拿到的json
        return deserialize(cache, returnType, modelType);

    }


    @Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCacheEvict)")
    private Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable {
        // 获取切入的方法对象
        // 这个m是代理对象的,没有包含注解
        Method m = ((MethodSignature) pjp.getSignature()).getMethod();
        // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
        Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
        // 根据目标方法对象获取注解对象
        JhRedisCacheEvict cacheEvictAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCacheEvict.class);
        // 解析key
        String keyExpr = cacheEvictAnnotation.key();
        Object[] as = pjp.getArgs();
        String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as);
        // 先删除数据库中的用户信息再删除缓存
        Object result = pjp.proceed();
        redisClient.delete(key);
        return result;
    }

    public RedisCacheAspect() {
    	// 初始化redisClient对象,不同的项目可能实现不同,此处是结合自己项目中的redis实现的
        appInfo= SpringContext.getBean(AppInfo.class);
        String appName = appInfo.getAppName();
        this.redisClient = SpringContext.getBean(appName, RedisClient.class);
    }

    /**
     * @Description: 解析注解中的key,支持spel表达式的解析
     * @param spelExpress 注解中spel表达式
     * @param method 方法对象
     * @param params 方法参数
     * @return java.lang.String
     * @Author: ljx
     * @Date: 2020/6/10 3:16 下午
     */
    private String getRedisKeyBySpel(String spelExpress, Method method, Object[] params) {
        String redisKey = appInfo.getAppName()+KEY_SEPARATOR+method.getName();
        // 如果为空,则默认服务名_方法名
        if (StringUtils.isBlank(spelExpress)){
            return redisKey;
        }
        // 如果不是spel表达式,则直接使用用户传入的key
        if(!spelExpress.contains(SPEL)){
            return spelExpress;
        }
        // 如果是spel表达式,但是参数为空,则默认服务名_方法名
        if(params==null){
            return redisKey;
        }
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext context = new StandardEvaluationContext();
        // spel表达式用到的变量,设置第一个参数
        context.setVariable("entity", params[0]);
        // 设置第二个参数
        if(params.length>1&&params[1]!=null){
            context.setVariable("entityTwo", params[1]);
        }
        // 设置第三个参数
        if(params.length>TWO&&params[TWO]!=null){
            context.setVariable("entityTrd", params[2]);
        }
        // 解析spel表达式
        Expression expression = parser.parseExpression(spelExpress, new TemplateParserContext());
        final Object value = expression.getValue(context);
        return redisKey + KEY_SEPARATOR+"_"+Objects.toString(value,"");
    }

    /**
     * @Description: FastJSON反序列化获得对象
     * @param json 从redis缓存中获取的字符串
     * @param clazz 添加注解的方法返回值的class类型
     * @param modelType 转换成list中的class类型
     * @return java.lang.Object
     * @Author: ljx
     * @Date: 2020/6/11 3:50 下午
     */
    private Object deserialize(String json, Class clazz, Class modelType) {
        return clazz.isAssignableFrom(List.class) ? JSON.parseArray(json, modelType) : JSON.parseObject(json, clazz);
    }

}

五、使用案例

简单的使用:

   @JhRedisCache(key = "#{#entity}")
    public TeachCenter selectTeacherCenter(Integer teachCenterId) {
        return teachCenterMapper.selectByPrimaryKey(teachCenterId);
    }

复杂使用:

    @JhRedisCache(key = "#{#entity.getProvinceId()}", type = TeachCenter.class)
    public List<TeachCenter> selectTeachCenterList(TeachCenterCommonRequest teacherCenterCommonRequest) {
        //获取地区id;
        Integer provinceId = teacherCenterCommonRequest.getProvinceId();
        //分页
        PageHelper.startPage(teacherCenterCommonRequest.getPageNum(), teacherCenterCommonRequest.getPageSize());
        //获取所有教学中心
        List<TeachCenter> teachCenterList = teachCenterMapper.selectByProvinceId(provinceId);
        return teachCenterList;
}        

本人亲测有效,并已在公司项目中大规模使用,因为依赖redis的配置,这里不再带领大家测试,有兴趣的小伙伴可以在项目中测试看看,欢迎有问题随时沟通。

你可能感兴趣的