SpringBoot 使用Aspect切面自定义Redis缓存注解-超个性化定制(集中管理缓存key、过期时间expire)以及模板化使用Redistemplete

       前言:springboot自带的RedisCahe注解虽然使用方便,但是在使用过程中缓存key很难集中管理,在更改或查找缓存Key时往往需要在服务层各个方法上去寻找注解,更难的是有些场景不适合使用注解,需要Redistemplate缓存模板写在代码块;如果只使用Redistemplate缓存模板又会增加我们的代码量,相比于注解的更方便,所以有没有一种既能使用注解进行缓存又能使用Redistemplate模板进行缓存的方式而且又能集中管理缓存key以及过期时间呢?往下看。。。

首先创建一个包package用来存放缓存相关类

为了集中管理缓存时间和缓存key,需要自定一个枚举方法,用来放所有的缓存key以及过期时间

CacheKeyEnum.java:

import lombok.Getter;

@Getter
public enum CacheKeyEnum {

   //缓存key,过期时间,时间类型(这个在CacheMethod.java内有相关定义)
    TEST_ONE("TEST_ONE_KEY_",1,"min"), 

    //设置缓存不过期,永久缓存
    TEST_ONE("TEST_ONE_KEY_",-1,null);



    public final String cacheKey;
    public final Integer cacheTime;
    public final String timeType;


    CacheKeyEnum(String cacheKey,Integer cacheTime,String timeType) {
        this.cacheKey = cacheKey;
        this.cacheTime = cacheTime;
        this.timeType = timeType;
    }

}

定义RedisTemplate缓存模板,这个就不详细描述,可以自己去看RedisTemplate模板的使用介绍

CacheMethod.java:

import com.alibaba.fastjson2.JSONObject;
import jakarta.annotation.Resource;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


@Component
@Log4j2
public class CacheMethod {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 检查是否有key
     * @param cacheKeyEnum
     * @return
     */
    public Boolean hasKey(CacheKeyEnum cacheKeyEnum,String key) {
        return stringRedisTemplate.hasKey(cacheKeyEnum.cacheKey + key);
    }

    /**
     * 设置缓存
     * @param cacheKeyEnum
     * @param key
     * @param value
     */
    public void set(CacheKeyEnum cacheKeyEnum,String key,Object value) {
        if(!Objects.equals(value,null)) {
            log.info("==> 正在加载{}缓存",cacheKeyEnum.cacheKey + key);
            stringRedisTemplate.opsForValue().set(cacheKeyEnum.cacheKey + key, JSONObject.toJSONString(value));
            if (cacheKeyEnum.cacheTime != -1)
                this.setTimeOut(cacheKeyEnum.cacheKey + key, cacheKeyEnum.cacheTime, cacheKeyEnum.timeType);
            log.info("==> 加载{}缓存完成",cacheKeyEnum.cacheKey + key);
        }
    }

    /**
     * 获取缓存(返回String类型)
     * @param cacheKeyEnum
     * @param key
     * @return
     */
    public Object get(CacheKeyEnum cacheKeyEnum,String key) {
        log.info("==> 正在获取{}缓存",cacheKeyEnum.cacheKey + key);
        return stringRedisTemplate.opsForValue().get(cacheKeyEnum.cacheKey + key);
    }

    /**
     * 删除key
     * @param cacheKeyEnum
     * @param key
     */
    public void del(CacheKeyEnum cacheKeyEnum,String key) {
        log.info("==> 正在删除{}缓存",cacheKeyEnum.cacheKey + key);
        stringRedisTemplate.delete(cacheKeyEnum.cacheKey + key);
        log.info("==> 删除{}缓存完成",cacheKeyEnum.cacheKey + key);
    }


    /**
     * 管理 hashMap
     * put
     * get
     * delete
     * entries
     * keys
     * values
     * @return
     */
    public HashOperations<String, Object, Object> opsMap() {
        return stringRedisTemplate.opsForHash();
    }


    /**
     * 管理 list
     * leftPush
     * rightPush
     * size
     * leftPop
     * rightPop
     * range
     * remove
     * @return
     */
    public ListOperations<String, String> opsList() {
        return stringRedisTemplate.opsForList();
    }

    /**
     * 设置key失效时间
     * @param time_type time_type
     * @param key     key
     * @param timeout timeout
     */
    public void setTimeOut(String key, long timeout, String time_type){
        switch (Objects.requireNonNull(time_type)) {
            case "sec" -> stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
            case "min" -> stringRedisTemplate.expire(key, timeout, TimeUnit.MINUTES);
            case "hour" -> stringRedisTemplate.expire(key, timeout, TimeUnit.HOURS);
            case "day" -> stringRedisTemplate.expire(key, timeout, TimeUnit.DAYS);
            default -> {
            }
        }
    }
}

接下来就是定义缓存注解,这儿定义了3个参数,第一个为可变key,就是平常缓存时需要在key后面加一个可变参数用来区分key;第二个为枚举值,上面我们定义的枚举值;第三个为当需要缓存null值时的条件,true是可以缓存null值,false不能缓存null值,默认不存null值

Cache.java

import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    
    //注解参数-传入的key(比如说userId...) -可不传参数
    String key() default ""; 

    //缓存定义的枚举值 -必传参数
    CacheKeyEnum value() default CacheKeyEnum.PUBLIC;
    
    //当缓存数据为null值时是否进行缓存 -可不传参数
    boolean unless() default false;
}

最后就是使用切面对@Cache注解进行处理,这也是最重要的一步

首先需要引入aspect相关依赖包,在pom.xml文件中引入下面依赖包,如已引入切面依赖则忽略!

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.9</version>
        </dependency>

接下来就是对@Cache标记了方法进行处理

第一步,需要对@Cache的方法进行拦截,在切面中使用pointCut()进行拦截,只要是使用了@Cache注解的方法都会被拦截

第二步,就是对拦截的方法进行下一步处理,比如说当redis中存在缓存key时,那么久直接返回缓存数据,不需要在调用service进行查询操作在返回值;当Redis没有缓存key时,需要调用service进行查询且放入缓存;以及对返回值进行转型,根据注解的方法返回值返回相同类型的值

第三步,因为要设置key的可变参数,所有需要注解直接传入方法的参数,这里要使用到spel表达式,同样是使用切面方法获取到方法的入参,通过spel表达式取得参数值

接下来看正式代码

CacheInterceptor.java:


import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;



@Aspect
@Component
public class CacheInterceptor {


    private final SpelExpressionParser parserSpel = new SpelExpressionParser();
    private final DefaultParameterNameDiscoverer parameterNameDiscoverer= new DefaultParameterNameDiscoverer();

    @Resource
    private CacheMethod cacheMethod;



    @Pointcut("@annotation(com.mm.cache.Cache)")
    private void pointCutCache() {}


    /**
     * 注解处理
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("pointCutCache()")
    private Object redisCache(ProceedingJoinPoint pjp) throws Throwable {
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        Cache cache = method.getAnnotation(Cache.class);
        /* 获取cacheKeyEnum中定义的key */
        CacheKeyEnum cacheKeyEnum = cache.value();
        /* 获取方法的入参值 */
        String key =  generateKeyBySpEL(cache.key(), pjp);
        /* 当缓存key存在时直接返回值 */
        if (cacheMethod.hasKey(cacheKeyEnum,key)) {
            String redisData = (String) cacheMethod.get(cacheKeyEnum,key);
            /* 获取方法的返回值类型,使缓存值转型返回对应类型的值,下面定义的是平常常见的类型,可以自己在定义一些 */
            return switch (method.getReturnType().getTypeName()) {
   
                case "java.util.List" -> this.strToClass(redisData, ArrayList.class);
                case "java.util.Map" -> this.strToClass(redisData, Map.class);
                case "java.lang.String" -> redisData.replace("\"", "");
                default -> this.strToClass(redisData, method.getReturnType());
            };
        } else {
            /* key不存在时调用方法获得值以后缓存到Redis */
            Object obj = pjp.proceed();
            if (!Objects.equals(obj,null) || cache.unless()) {
                cacheMethod.set(cacheKeyEnum,key,obj);
            }
            return obj;
        }
    }


    /**
     * 获取方法入参-spel表达式
     * @param key
     * @param pjp
     * @return
     */
    public String generateKeyBySpEL(String key, ProceedingJoinPoint pjp) {
        try {
            Expression expression = parserSpel .parseExpression(key);
            EvaluationContext context = new StandardEvaluationContext();
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Object[] args = pjp.getArgs();
            String[] paramNames = parameterNameDiscoverer.getParameterNames(methodSignature.getMethod());
            for(int i = 0 ; i < args.length ; i++) {
                assert paramNames != null;
                context.setVariable(paramNames[i], args[i]);
            }
            return Objects.requireNonNull(expression.getValue(context)).toString();
        } catch (SpelEvaluationException e) {
            return key;
        } catch (IllegalArgumentException e) {
            return "";
        }
    }



    /**
     * 字符串转实体
     * @param str
     * @param cls
     * @return
     */
    public <T> T strToClass(String str,Class<T> cls) {
        return JSONObject.parseObject(str, cls);
    }

}

 注解使用

    /* 使用spel传入参数值 */
    @Cache(value = CacheKeyEnum.TEST_ONE, key = "#userCode")
    public List<String> test(String userCode) {}

    /* 传入固定参数 */
    @Cache(value = CacheKeyEnum.TEST_ONE, key = "1231")
    public List<String> test(String userCode) {}

     /* 不使用参数 */
    @Cache(value = CacheKeyEnum.TEST_ONE)
    public List<String> test(String userCode) {}

RedisTemplate使用

public class Test {

    @Resource
    private CacheMethod cacheMethod;


    public string test(String key, String value) {

        /* 缓存 */
         cacheMethod.set(CacheKeyEnum.TEST_ONE,key,value)

        
        /* 获取缓存 */
        cacheMethod.get(CacheKeyEnum.TEST_ONE,key)
    
        
        
        
    }


}



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二九筒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值