目录
一、实现思路
二、定义缓存注解
三、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 的环绕通知方法内可以获取这部分的变量值。
定义我们切面的切入点为我们上面创建的注解。
/*** 定义切入点为 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
// 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 keyString key = myRedisCache.key();if (StrUtil.isBlank(key)) {throw new RuntimeException("key 不能为空");}//获取el表达式的keyString elKey = this.elKey(joinPoint, key);//拼接keyredisKeySb.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);}//判断是否为nullif (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")@Overridepublic StudentVo getUser(int id) {//缓存key使用参数用户idStudent 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);}
有时候使用框架不一定能灵活使用在多场景,毕竟框架的设计原则是约束大于配置,很多东西都是别人定义好的,其实有时候也可以通过一些简单的方式来实现自己的需求。
笔者也很抗拒那种一来就找框架的思维,要实现一个功能就非得先加个大炮来打蚊子,这种想法只会让自己变得越来越懒,可能有些同学会抬杠说不要重复造轮子,但是能造出自己的轮子不是很牛的一件事情嘛。
所以有时候看看一些开源的项目,看看别人的设计思路,实现的方式,这样自己也可以模仿写出类似的功能,多看多学多实践多积累。