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

news2024/9/20 1:19:40

目录

一、实现思路

二、定义缓存注解

三、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/60759.html

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

相关文章

【Graph】NetworkX官方基础教程

NetworkX官方基础教程图的基础知识1.1 图&#xff08;graph&#xff09;及其分类1.2 节点的度&#xff08;degree&#xff09;1.3 子图&#xff08;subgraph&#xff09;1.4 连通图1.5 图的矩阵表示NetworkX概述NetworkX基础教程1. 创建图2. 节点3. 边4. 清空图5. 图可视化6. 访…

基于javaweb框架的springboot mybatis宠物商城源码含论文设计文档

在互联网高速发展、信息技术步入人类生活的情况下&#xff0c;电子贸易也得到了空前发展。网购几乎成为了人人都会进行的活动。近几年来&#xff0c;养宠物更是成为人们生活中重要的娱乐内容之一&#xff0c; 人们越来越多的讲感情也寄托给了宠物&#xff0c;以给自己另一个感情…

自动驾驶--预测技术

根据百度技术培训中心课程整理( https://bit.baidu.com/productsBuy?id72) 背景简介 无人车系统从算法模块可分为三个部分&#xff0c;首先是感知通过对传感器数据和环境信息进行计算来解决周围有什么的问题&#xff0c;其次是预测&#xff0c;根据感知信息预测环境下一步将…

Java单元测试

1. 序言 1.1 工作中要求进行单元测试 毕业进入公司时&#xff0c;为了锻炼笔者的Java基础&#xff0c;老大给笔者分配了平台化开发的工作&#xff0c;基于Spring Boot Mybatis的Java Web后端开发一个人干后端开发&#xff0c;且以前也没有后端开发的经验&#xff0c;所以只是…

CTF之序列化__toString

序列化简介 本质上serialize()和unserialize&#xff08;&#xff09;在php内部的实现上是没有漏洞的&#xff0c;漏洞的主要产生是由于应用程序在处理对象&#xff0c;魔术函数以及序列化相关问题时导致的。 当传给unserialize()的参数可控时&#xff0c;那么用户就可以注入精…

【应用】Docker Swarm

Docker SwarmDocker Swarm 集群配置配置前准备初始化 SwarmSwarm 常用命令Portainer 集群管理Docker Swarm 集群配置 masternode1node2192.168.86.133192.168.86.131192.168.86.139 配置前准备 关闭各个节点服务器的防火墙 systemctl stop firewalld systemctl disable fire…

ATF问题二则:EL3可能没有实现吗? aarch32中的S-EL1是什么?

最近两个问题&#xff0c;戳到了我的知识盲点&#xff0c;当然我这个菜鸡ATF哪里都是盲点。 问题一&#xff1a;EL3可能没有实现吗&#xff1f; 问题二&#xff1a;bl2是aarch32, 那么bl2是S-EL1&#xff0c;bl31也是S-EL1? 1、EL3可能没有实现吗&#xff1f; The Armv8-A …

基于MATLAB的一级倒立摆控制仿真,带GUI界面操作显示倒立摆动画,控制器控制输出

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 一个可以活动的小车上立着一根不稳定随时会倒下的杆。小车的轮子由电机控制&#xff0c;可以控制小车电机的转动力矩M。同时&#xff0c;也可以获取小车轮子转动的圈数N&#xff08;可以精确到小…

java计算机毕业设计ssm实验室设备管理系统5k648(附源码、数据库)

java计算机毕业设计ssm实验室设备管理系统5k648&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xf…

162.基于Django-rest_framework的身份验证和权限

1. 概述 到目前为止&#xff0c;程序的API对任何人都可以编辑或删除&#xff0c;没有任何限制。我们希望有一些更高级的行为&#xff0c;进行身份验证和权限分配&#xff0c;以确保&#xff1a; 数据始终与创建者相关联只有经过身份验证的用户才能创建数据只有数据的创建者可…

嵌入式Linux上ifpulgd的使用配置与qemu模拟验证

问题引入 最近在项目开发中收到了一个非常简单的需求&#xff0c;我们的嵌入式Linux板卡需要在检测到网口插拔后重新配置网络&#xff0c;这在pc环境中非常常见。但是在这个项目的默认SDK中并没有相关配置&#xff0c;稍微查询了一下&#xff0c;在一般pc上通常使用Ifpulgd,并…

[附源码]Python计算机毕业设计Django企业售后服务管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

【数据结构】树的概念与堆的实现

树的概念与堆的实现1、什么是树1.1 树的概念1.2 树的相关概念1.3 树的表示2、二叉树概念及结构2.1 概念2.2 特殊的二叉树2.3 二叉树的性质2.4 二叉树的存储结构3、二叉树的顺序结构及实现3.1 二叉树的顺序结构3.2 堆的概念及结构3.3 堆的实现3.3.1 创建一个堆3.3.2 初始化堆3.3…

【计算机毕业设计】基于JSP的网上购物系统的设计与实现

分类号&#xff1a;TP315 U D C&#xff1a;D10621-408-(2007)5883-0 密 级&#xff1a;公 开 编 号&#xff1a;2003214012 学位论文 基于JSP的网上购物系统的设计与实现 基于JSP的网上购物系统的设计与实现 摘 要 近年来&#xff0c;随着Internet的迅速崛起&#xff0c…

内存 分页、交换空间

目录 1. 分页 1.1 地址转换 1.2 页表存在哪里 1.3 列表中究竟有什么 1.4 分页的优缺点 2. 快速地址转换&#xff08;TLB&#xff09; 2.1 TLB 的基本算法 2.2 谁来处理 TLB 未命中 2.2.1 硬件处理 2.2.2 软件&#xff08;操作系统&#xff09;处理 2.3 TLB 的内容 …

[附源码]Python计算机毕业设计SSM精准扶贫系统(程序+LW)

项目运行 环境配置&#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…

(免费分享)基于springboot,vue公司财务系统

该系统是一个简单的公司财务管理系统&#xff0c;包含用户基本信息管理&#xff08;员工管理&#xff0c;管理员管理&#xff09;&#xff0c;工资管理&#xff08;员工工资管理&#xff0c;管理员工资管理&#xff09;&#xff0c;业务管理&#xff08;员工业务管理&#xff0…

Windows server 2012搭建用户隔离FTP站点

Windows server 2012搭建用户隔离FTP站点 系统添加FTP功能创建FTP登陆账户和其使用的文件夹D盘根目录下创建FTP站点主目录ftp文件夹ftp下创建用户主目录localuser&#xff08;名称不可更改&#xff0c;实现用户隔离必要步骤&#xff09;Localuser文件夹下创建对应用户的文件夹…

opencv入门笔记(二)

目录图像运算位运算位与运算位或运算取反运算异或运算位运算特点示例&#xff1a;位运算示例加法运算示例&#xff1a;查看三种加法运算的区别滤波器均值滤波中值滤波高斯滤波双边滤波示例&#xff1a;查看多种滤波器的处理效果视频处理示例&#xff1a;打开笔记本电脑内置摄像…

轻量化神经网络(移动设备上的神经网络)的整体框架

提示&#xff1a;不断更新中 文章目录一、为什么要引入轻量化神经网络二、模型压缩(Model Compression)参数修建低秩因子分解参数量化知识蒸馏人工神经架构设计三、自动压缩和神经架构搜索(Automated Compression and Neural Architecture Search)自动模型压缩(Automated Model…