目录
一、实现思路
二、定义缓存注解
三、aop 切面处理
四、使用方式
五、灵活的运用
六、总结
前几天有同学看了 SpringBoot整合RedisTemplate配置多个redis库 这篇文章,提问spring cache 能不能也动态配置多个redis库。介于笔者没怎么接触过,所以后来简单看了一下相关资料,感觉跟笔者以前实现过的一个功能很相似,希望能给这位同学一点思路或者方案。
一、实现思路
通过 spring aop 的方式,切入点为我们自定义的注解,通过 @Around 注解环绕通知,在调用方法前检查 reids 中是否存在我们设置的缓存,有则直接返回,并在调用方法后,设置我们的数据到redis 缓存中。
二、定义缓存注解
定义的注解的修饰范围为类方法上,key 变量用于设置 redis 缓存的 key 值,并支持el表达式写法,这个跟 spring cache 是类似的;expire 变量用于设置 redis 缓存的失效时间。
/**
* Redis缓存注解
*
* @Author Liurb
* @Date 2022/12/3
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyRedisCache {
/**
* 缓存的key
* 支持el表达式
*/
String key() default "";
/**
* 默认失效时间为1天,单位为秒
*/
long expire() default 86400;
}
注解的变量可以根据自己的使用场景添加,到时候在 aop 的环绕通知方法内可以获取这部分的变量值。
三、aop 切面处理
定义我们切面的切入点为我们上面创建的注解。
/**
* 定义切入点为 MyRedisCache 注解
*/
@Pointcut("@annotation(org.liurb.springboot.advance.demo.class3.annotation.MyRedisCache))")
public void redisCachePointcut() {
}
环绕通知,注意 @Around 内的写法,这样就可以在 doAround 方法内获取到方法上的注解,从而获取到注解设置的变量值。
/**
* 环绕通知
*
* 可以用来在调用一个具体方法前(判断缓存是否存在)和调用后(设置缓存)来完成一些具体的任务
*
* @param joinPoint
* @param myRedisCache
* @return
* @throws Throwable
*/
@Around("redisCachePointcut() && @annotation(myRedisCache)")
public Object doAround(ProceedingJoinPoint joinPoint, MyRedisCache myRedisCache) throws Throwable {
//todo...
}
然而,我们还需要实现el表达式,大概原理为,使用方法参数的值来注入替换el表达式上的变量。
/**
* 获取el表达式的redis key
*
* @param joinPoint
* @param key
* @return
*/
private String elKey(ProceedingJoinPoint joinPoint, String key) {
// 表达式上下文
EvaluationContext context = new StandardEvaluationContext();
String[] parameterNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames(); // 参数名
Object[] args = joinPoint.getArgs(); // 参数值
for (int i=0; i<args.length; i++) {//设置evaluation提供上下文变量
context.setVariable(parameterNames[i], args[i]);
}
// 表达式解析器
ExpressionParser parser = new SpelExpressionParser();
// 解析
String redisKey = parser.parseExpression(key).getValue(context, String.class);
return redisKey;
}
// spring cache 的el表达式写法,自动注入参数 user 的值到表达式中
@Cacheable(value = "users", key = "#user.userCode" condition = "#user.age < 35")
public User getUser(User user) {
//todo...
return user;
}
我们还需要知道切面方法上的返回值是什么,这样我们才能够将缓存里面的内容反序列化到返回值上。
/**
* 获取方法的返回值的类型
*
* @param joinPoint
* @return
*/
private Class getReturnType(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取method对象
Method method = signature.getMethod();
//获取方法的返回值的类型
Class returnType = method.getReturnType();
return returnType;
}
完整的环绕通知方法内容如下,主要加上一些判空的处理。
/**
* 环绕通知
*
* 可以用来在调用一个具体方法前(判断缓存是否存在)和调用后(设置缓存)来完成一些具体的任务
*
* @param joinPoint
* @param myRedisCache
* @return
* @throws Throwable
*/
@Around("redisCachePointcut() && @annotation(myRedisCache)")
public Object doAround(ProceedingJoinPoint joinPoint, MyRedisCache myRedisCache) throws Throwable {
//统一的缓存前缀
StringBuilder redisKeySb = new StringBuilder("my_redis_cache").append(":");
//注解上定义的redis key
String key = myRedisCache.key();
if (StrUtil.isBlank(key)) {
throw new RuntimeException("key 不能为空");
}
//获取el表达式的key
String elKey = this.elKey(joinPoint, key);
//拼接key
redisKeySb.append(elKey);
String redisKey = redisKeySb.toString();
//查缓存
Object result = coreRedisUtil.get(redisKey);
if (result != null) {//存在缓存
if (result instanceof String) {//缓存一般为json字符串,所以这里需要进行返回类型的转换
String jsonText = result.toString();
//获取接口的返回值
Class returnType = this.getReturnType(joinPoint);
//使用fastjson转换到对应的类型
return JSON.parseObject(jsonText, returnType);
}
}
//缓存不存在
try {
//执行方法
result = joinPoint.proceed();
} catch (Throwable e) {
//方法抛异常
throw new RuntimeException(e.getMessage(), e);
}
//判断是否为null
if (result != null) {
//设置失效时间(秒)
long expire = myRedisCache.expire();
//使用fastjson转为json字符串,设置缓存
coreRedisUtil.set(redisKey, JSON.toJSONString(result), Duration.ofSeconds(expire));
}
//返回结果
return result;
}
四、使用方式
aop 和 注解 我们都写好了,接来下就看一下怎么运用到方法上来。
@MyRedisCache(key = "'user:id:'+#id")
@Override
public StudentVo getUser(int id) {//缓存key使用参数用户id
Student student = studentService.getById(id);
if (student != null) {
StudentVo vo = new StudentVo();
vo.setId(student.getId());
vo.setName(student.getName());
vo.setAge(student.getAge());
vo.setSex(student.getSex());
return vo;
}
return null;
}
只要我们将 @MyRedisCache 注解打在我们需要使用缓存的方法实现上,通过变量 key ,我们可以定义这个方法的 redis 缓存 key ,可以看到我们的 key 使用了el表达式,需要将参数 id 注入其中。
接下来,我们写一个单元测试看看效果。
调用方法后,可以看到已经跳入到环绕通知方法内,并获取到方法上我们设置的key值。
在el表达式处理方法上,可以看到调试面板上方法上的参数名称和参数值。
可以看到处理完后,我们的 redisKey 变量已经替换注入了参数的值。
因为我们是第一次执行,所以缓存里面肯定是没有内容的。
所以这时候需要执行这个方法拿到它的返回数据。
下一步就跳入到方法体内执行代码行了。
执行后,环绕通知的result值已经是方法体返回的数据了,这时候我们就可以根据 key 设置我们的缓存了。
这时候可以看到缓存已经设置成功了。
接下来,我们在执行一下这个方法,参数已经一样的,看看效果。
可以看到已经能够从缓存读取到刚才我们设置的缓存内容,key也是一样的。
通过fastjson反序列化为对象,也没问题。这样一个简单的缓存功能就实现了。
五、灵活的运用
有了上面的例子,接下来解答一下那位同学的问题,就是如何能够动态实现使用不同的redis库呢?
因为具体的场景笔者不太清楚,这边可以有两种方案,一种为在注解上增加一个redis库的变量,在切面内获取此变量进行处理;另外一种,可以通过key的规范约束来处理,如key中包含 student 就使用1库,包含 teacher就使用2库。
说一下笔者之前使用的场景,这种方法主要是用在远程接口的调用上,因为有些接口查询数据的时效比较长,所以就想缓存一下,而且当时这类接口还挺多的,就不想每个接口都写一遍缓存处理。
所以,笔者这边使用的注解还多了一个 successFiled 变量,用于对返回结果判断是否查询成功。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyRedisCache {
/**
* 缓存的key
* 支持el表达式
*/
String key() default "";
/**
* 默认失效时间为1天,单位为秒
*/
long expire() default 86400;
/**
* 对返回数据进行缓存的判断依据
* 形式如"#result.code==0"
*/
String successFiled() default "";
}
这个值也是通过el表达式来判断,方法跟上面也是一样的,只是这里有个默认的变量名为 result
/**
* 判断返回结果是否为成功
*
* @param result
* @param successFiled
* @return
*/
private boolean isSuccess(Object result, String successFiled) {
// 表达式上下文
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("result", result);
// 表达式解析器
ExpressionParser parser = new SpelExpressionParser();
return parser.parseExpression(successFiled).getValue(context, Boolean.class);
}
六、总结
有时候使用框架不一定能灵活使用在多场景,毕竟框架的设计原则是约束大于配置,很多东西都是别人定义好的,其实有时候也可以通过一些简单的方式来实现自己的需求。
笔者也很抗拒那种一来就找框架的思维,要实现一个功能就非得先加个大炮来打蚊子,这种想法只会让自己变得越来越懒,可能有些同学会抬杠说不要重复造轮子,但是能造出自己的轮子不是很牛的一件事情嘛。
所以有时候看看一些开源的项目,看看别人的设计思路,实现的方式,这样自己也可以模仿写出类似的功能,多看多学多实践多积累。