【springboot进阶】使用aop + 注解方式,简单实现spring cache功能

news2024/11/23 8:35:46

目录

一、实现思路

二、定义缓存注解

三、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);
    }

六、总结

有时候使用框架不一定能灵活使用在多场景,毕竟框架的设计原则是约束大于配置,很多东西都是别人定义好的,其实有时候也可以通过一些简单的方式来实现自己的需求。

笔者也很抗拒那种一来就找框架的思维,要实现一个功能就非得先加个大炮来打蚊子,这种想法只会让自己变得越来越懒,可能有些同学会抬杠说不要重复造轮子,但是能造出自己的轮子不是很牛的一件事情嘛。

所以有时候看看一些开源的项目,看看别人的设计思路,实现的方式,这样自己也可以模仿写出类似的功能,多看多学多实践多积累。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/59180.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java开发】 Spring 08 :访问 Web 资源( 借助 RestTemplate or WebClient )

web 资源就是运行在服务器上的资源&#xff0c;比如放到 web 下的页面 js 文件、图片、css等&#xff0c;web资源分为静态web资源和动态web资源两类&#xff0c;接下来访问的就是动态资源&#xff08;页面返回的数据是动态的&#xff0c;由后端程序产生&#xff09;&#xff0…

Android 使用元数据

Android 使用元数据 前提介绍Metadata 有时候为安全起见&#xff0c;某个参数要给某个活动专用&#xff0c;并不希望其他活动也能获取该参数&#xff0c;也就是要使用第三方SDK时。Activity提供了元数据&#xff08;Metadata&#xff09;的概念&#xff0c;元数据是一种描述其…

C++类和对象(二)构造函数、析构函数、拷贝构造函数

目录 1.类的6个默认成员函数 2. 构造函数 2.1 概念 2.2 特性 3.析构函数 3.1 概念 3.2 特性 4. 拷贝构造函数 4.1 概念 4.2 特征 1.类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中真的什么都没有吗&#xff1f;并不是&#xff0c;…

【菜菜的sklearn课堂笔记】聚类算法Kmeans-聚类算法的模型评估指标

视频作者&#xff1a;菜菜TsaiTsai 链接&#xff1a;【技术干货】菜菜的机器学习sklearn【全85集】Python进阶_哔哩哔哩_bilibili 可以只看轮廓系数和卡林斯基-哈拉巴斯指数 不同于分类模型和回归&#xff0c;聚类算法的模型评估不是一件简单的事。在分类中&#xff0c;有直接结…

【尚硅谷】Java数据结构与算法笔记02 - 队列

文章目录一、使用场景二、队列介绍三、数组模拟队列3.1 思路分析3.2 Java代码实现3.3 问题分析与优化四、数组模拟环形队列4.1 思路分析4.2 Java代码实现一、使用场景 银行排队&#xff0c;先到先得测核酸&#xff0c;先到先测 二、队列介绍 队列是一个有序列表, 可以用数组…

硬盘压缩将C盘拓展成D盘和E盘

硬盘压缩将C盘拓展成D盘和E盘1. 现状2. 硬盘压缩2.1 进入计算机管理2.2 磁盘管理压缩卷3. 分配新盘符3.1 查看盘符是否被占用3.2 新建D盘刚安装好系统的电脑有可能只有一个C盘&#xff0c;我们工作学习的时候远远不够&#xff0c;那怎么拓展其他盘符呢&#xff1f; 接下来让我们…

PyQt5基础练习1

0. 本文学习地址 1. PyQt5是由一系列Python模块组成 超过620个类&#xff0c;6000函数和方法。能在诸如Unix、Windows和Mac OS等主流操作系统上运行。 1.1 PyQt5有两种证书 GPL商业证书 2. 实验1 实现简单的窗体 2.1 完整代码 #!/usr/bin/python3 # -*- coding: utf-8 -*…

专业尖端远心光学,高精度视觉检测解决者

随着机器视觉系统在精密检测领域的广泛应用&#xff0c;在精密光学测量系统中&#xff0c;由于普通光学镜头会存在一定的制约因素&#xff0c;如影像的变形、视角选择而造成的误差、不适当光源干扰下造成边界的不确定性等问题&#xff0c;进而影响测量的精度。为弥补普通镜头应…

155. RESTframe的请求和响应

1.请求和响应 REST framework引入了2个新的对象&#xff1a;Request和Response 1.1 Request 包结构&#xff1a;rest_framework.request.Request 该对象扩展了常规的HttpRequest &#xff0c;增加了对REST框架灵活的请求解析和请求认证的支持 主要属性&#xff1a; data 这个…

基于PHP+MySQL毕业生档案管理系统

毕业生档案管理系统是信息时代的产物&#xff0c;它是学校档案管理部门的一个好帮手。有了它不再需要繁重的纸质登记&#xff0c;有了它档案管理员不在需要繁重的工作&#xff0c;一些成绩信息和奖惩等基本信息可以由管理人员及时的对信息进行查询、更新、修改和删除&#xff0…

SpringBoot_启动原理分析

一共分为三部分来解析: 一 依赖导入原理 二 springboot 包扫描原理三 springboot自动配置原理一 依赖导入原理 父项目进行版本控制 ctrl 点击spring-boot-starter-parent 进入 继续点击,进入spring-boot-dependencies 这里管理着springboot中所有依赖的版本,版本…

38 | Linux 磁盘空间异常爆满

1 场景 收到告警 找到对应的服务器&#xff1a;df -hl 要找到导致磁盘空间满的目录或文件。 2 找占用空间大的目录或文件 2.1方式一 在根目录下&#xff0c;通过du -hs命令&#xff0c;列出各目录所占空间大小 命令&#xff1a;du -hs * 之后再用同样的方法继续到对应目…

[附源码]计算机毕业设计springboot医学图像管理平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

EXSI-NFS实验

A-EXSI-NFS实验 2022年3月25日 8:20 iptables -F iptables-save systemctl stop firewalld #setenforce [0|1]命令修改SELinux当前的运行模式&#xff08;0为禁用&#xff0c;1为启用&#xff09; getenforce Enforcing setenforce 0 getenforce Permissive NFS 两台CentOS 7即…

【车载开发系列】UDS诊断---读取内存($0x23)

【车载开发系列】UDS诊断—读取内存&#xff08;$0x23&#xff09; UDS诊断---读取内存&#xff08;$0x23&#xff09;【车载开发系列】UDS诊断---读取内存&#xff08;$0x23&#xff09;一.概念定义二.注意事项三.报文格式1&#xff09;报文请求2&#xff09;肯定响应3&#x…

一、Node.js 环境安装 (详)

1. 下载Node.js 首先进入node.js官网&#xff0c;选择下载这一项&#xff0c;此时映入眼帘的可以看到有两项LTS(长期维护版本)和Current(最新的版本)&#xff0c;一般在开发会选择左边进行下载安装&#xff0c;具稳定性以及有长期维护。那么下面演示的是64位的Windows操作系统&…

PCI bar 解析

只要是接入系统的 pci 设备就需要和系统软件进行交互&#xff0c;设备和系统之间的交流主要包含以下两部分&#xff1a; 1&#xff0c;系统要能访问到设备的寄存器 用于控制设备行为&#xff0c;包括DMA&#xff0c;数据收发等&#xff1b;设备通过寄存器报告自身的状态&…

Nginx部署vue项目和配置代理

Nginx部署vue项目和配置代理目录概述需求&#xff1a;实现思路分析1.一般前后端分离的项目需要进行跨域2.微服务代理3.vuenginx实现服务端跨域4.网页解析器参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&am…

【问题解决】Android JDK版本不匹配导致崩溃踩坑记录

【问题解决】Android JDK版本不匹配导致崩溃踩坑记录部分机型反馈崩溃问题谷歌回复与解决方案Android打包脱糖操作对比与排查总结前几天同事遇到一个非常诡异的报错&#xff0c;紧急处理后&#xff0c;趁着周末仔细研究了一下原因&#xff0c;觉得还挺有意思的&#xff0c;所以…

制作一个谷歌浏览器插件,实现网页数据爬虫

一、什么是浏览器插件 浏览器插件&#xff0c;基于浏览器的原有功能&#xff0c;另外增加新功能的工具&#xff0c;是可定制浏览体验的小型软件程序&#xff0c;让用户可以根据个人需要或偏好来定制浏览器。 如拦截网页中的广告、划词翻译、倍速视频等等。 Chrome、edge等浏…