前言: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)
}
}