赞
踩
目录
前几天有同学看了 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<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);
- }
有时候使用框架不一定能灵活使用在多场景,毕竟框架的设计原则是约束大于配置,很多东西都是别人定义好的,其实有时候也可以通过一些简单的方式来实现自己的需求。
笔者也很抗拒那种一来就找框架的思维,要实现一个功能就非得先加个大炮来打蚊子,这种想法只会让自己变得越来越懒,可能有些同学会抬杠说不要重复造轮子,但是能造出自己的轮子不是很牛的一件事情嘛。
所以有时候看看一些开源的项目,看看别人的设计思路,实现的方式,这样自己也可以模仿写出类似的功能,多看多学多实践多积累。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。