使用Redission和Aop以及注解实现接口幂等性

news2025/1/16 19:09:19

关于什么是接口幂等性这里不再赘述,本文将使用分布式锁来解决接口幂等性的问题。


本文接口幂等判断标准:
String name = IP + 请求方式 + URI + 参数摘要值
当相同的name来临时,且上一个相同name对于的接口还未正常执行完毕,则判断为重复调用接口。

举例:
IP为10.10.10.10的客户端以Get请求方式调用/test接口,参数为a=1&b=2,当第一次调用接口后,服务端还没有处理完成,客户端又重复发起相同的请求,则会被服务端认作重复提交。


本篇文章所使用的相关技术或框架:

技术或框架使用原因
Spring boot Aop对被调用的接口方法进行增强
Redission存储以及管理锁,主要是使用里面的看门狗机制
自定义注解用于标注哪些接口方法需要实现接口幂等性
反射在Aop中获取被调用method上面的注解,根据有没有自定义注解来判断该方法是否开启接口幂等性
Servlet获取客户端的IP、请求方式、请求URI、参数列表
Jackson将接口参数从Object转为Json格式的字符串
MD5根据获得的Json字符串,计算其摘要值

前置需求
导入依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
			
		<!--用于将byte数组转为16进制字符串-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.14</version>
        </dependency>

编写Redisson配置类

@Configuration
public class RedissionConfig {
    @Bean
    public RedissonClient redisson(){
        // 单机模式
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://xxx:6379")
                .setPassword("xxx")
                .setDatabase(0);
        return Redisson.create(config);
    }
}

具体实现思路

编写自定义注解,写在被调用的method上面,用于标注是否需要开启接口幂等性

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimit {
    boolean enable() default true;
}	

在这里插入图片描述

不写该注解,或者写成@RequestLimit(enable = false). ,则对于该方法不会开启接口幂等性

编写Aop,对Controller Method进行增强

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zq.test.annotations.RequestLimit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class RequestAop {
	//当同一用户、相等请求方法、相同URI、相同参数重复请求时,返回值为这个,自己改下
    private final Object resultObject=new String("系统繁忙");

    @Autowired
    private RedissonClient redissonClient;
	
	//切面路径,自己改
    @Pointcut(value = "execution(* com.zq.test.controller..*.*(..))")
    public void pointCut(){}

    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //获取被调用的方法签名
        MethodSignature methodSignature= (MethodSignature) point.getSignature();
        //获取被调用的方法
        Method method=methodSignature.getMethod();
        //获取该方法上面的@RequestLimit注解
        RequestLimit requestLimit = method.getAnnotation(RequestLimit.class);
        Object result=null;
        RLock lock=null;
        //如果该方法上面有这个注解,并且没有主动关闭接口幂等性
        if(requestLimit!=null&&requestLimit.enable()){
            //获取当前进程的ServletRequest对象
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            //获取请求参数名
            String[] params = methodSignature.getParameterNames();
            //获取请求参数值
            Object[] args = point.getArgs();
            //将参数名和参数值转成Map,且一一对应
            Map<String, Object> maps = new HashMap<>();
            for(int i=0; i<params.length; i++){
                maps.put(params[i], args[i]);
            }
            //根据 客户端IP + http请求方式 + 请求的URI + 参数的摘要值 来计算锁名
            String lockName=getIpAddr(request)+'|'+request.getMethod()+'|'+request.getRequestURI()+'|'+md5Encrypt(maps);
    
            lock = redissonClient.getLock(lockName);
            //从redis中尝试再20ms内获取该锁,若是超过20ms也没获取到,则放弃,并返回false,如果获取到,则返回true,并自动上锁
            if(!lock.tryLock(20, TimeUnit.MILLISECONDS)){
                log.info("系统繁忙");
                //没有获取到,表名接口被重复调用
                return resultObject;
            }
        }
        //获取到锁
        try {
            log.info("------------------------执行开始------------------------");
            result=point.proceed();
        } finally {
            //如果获取到锁,且处于上锁状态,已经执行完毕,释放锁
            if(lock!=null&&lock.isLocked())
                lock.unlock();
            log.info("------------------------执行结束------------------------");
        }
        return result;
    }

    //将Map转为Json字符串,再根据Json字符串计算摘要值,并返回,用于判断是不是相同参数的请求重复提交了
    public String md5Encrypt(Map<String, Object> maps) throws JsonProcessingException, NoSuchAlgorithmException {
        ObjectMapper objectMapper = new ObjectMapper();
        String json = null;
        json = objectMapper.writeValueAsString(maps);
        MessageDigest md5 = MessageDigest.getInstance("md5");
        md5.update(json.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = md5.digest();
        md5.reset();
        //bytes数组转16进制字符串
        return Hex.encodeHexString(bytes);
    }
		
	//获取客户端IP,实际上这里可以直接用UserId替代
    public String getIpAddr(HttpServletRequest request) {
        String ipAddress = request.getHeader("X-Forwarded-For");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    log.error("根据网卡获取本机配置的IP异常", e);
                }

            }
        }

        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.indexOf(",") > 0) {
            ipAddress = ipAddress.split(",")[0];
        }

        return ipAddress;
    }

}

以上代码还有可以继续改进的地方,例如将IP换成UserIDlockName可以继续计算其摘要值,缩短lockName的长度

上述代码在实际使用中,需要修改的地方如下:
在这里插入图片描述

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

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

相关文章

Python ·信用卡欺诈检测【Catboost】

Python 信用卡欺诈检测【Catboost】 提示&#xff1a;前言 Python 信用卡欺诈检测 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录Python 信用卡欺诈检测【Catboost】前言一、导入包二、加载数据三、数据可视化四、…

鱼雷的发射角设置

过年嘛 放松个五六天啥的 玩了个猎杀潜航的游戏觉得那玩意挺有意思的开年了 要美赛 写个设置鱼雷发射角的小程序玩玩 游戏嘛,反正大概简易版就是这个框架,自己补充呗 各种设定啥的,没怎么关心,就是总结一下里面的平面几何..水个文章玩玩顺便练习一下pptx绘图美赛的时候估计还是…

30. PyQuery: 基于HTML的CSS选择器

目录 前言 导包 基本用法 按标签选择 标签链式操作 简便链式&#xff1a;后代选择器 类选择器 id 选择器 属性/文本选择器&#xff08;重点&#xff09; 改进多标签拿属性方法 快速总结 PyQuery的强大功能&#xff1a;修改源代码 添加代码块 修改/添加属性 删…

java spring IOC xml方式注入(数组 list集合 map集合 set集合)类型属性

我们先创建一个基本的java项目 然后引入 spring 的基本依赖 然后在src下创建一个包 我这里叫 collectiontype 和我同名 会避免一些找不到资源的麻烦 毕竟说 你们开发代码大部分会在这篇文章拿过去 当然 名称是看自己去取的 只是和我同名会方便一些 直接复制过去就好了 然后在…

C语言函数定义

函数是一段可以重复使用的代码&#xff0c;用来独立地完成某个功能&#xff0c;它可以接收用户传递的数据&#xff0c;也可以不接收。接收用户数据的函数在定义时要指明参数&#xff0c;不接收用户数据的不需要指明&#xff0c;根据这一点可以将函数分为有参函数和无参函数。将…

ARP渗透与攻防(七)之Ettercap Dns劫持

系列文章 ARP渗透与攻防(一)之ARP原理 ARP渗透与攻防(二)之断网攻击 ARP渗透与攻防(三)之流量分析 ARP渗透与攻防(四)之WireShark截获用户数据 ARP渗透与攻防(五)之Ettercap劫持用户流量 ARP渗透与攻防(六)之限制网速攻击 ARP-Ettercap Dns劫持 1.什么是DNS 1.概念 DNS是D…

汇编语言学习 下

本文承接汇编语言学习笔记 上 上篇文章记录了汇编语言寄存器&#xff0c;汇编语言基本组成部分&#xff0c;数据传送指令&#xff0c;寻址指令&#xff0c;加减法指令&#xff0c;堆栈&#xff0c;过程&#xff0c;条件处理&#xff0c;整数运算的内容 高级过程 大多数现代编程…

通信原理简明教程 | 信号、信道与噪声

文章目录1 信号与系统的基本概念2 傅里叶变换与信号的频谱2.1 信号的频谱2.2 傅里叶变换的常用性质2.3 信号的能量谱、功率谱与自相关函数2.4 信号的带宽3 线性系统与滤波器3.1 线性系统及其频率特性3.2 系统响应的频域求解3.3 滤波器2.4 信道及其特性4.1 信道的分类4.2 信道的…

兔年伊始谈闲书:三体-乌合之众-百年孤独 和《猫城记》(随笔)

引言 “ 各位读者&#xff0c;我们都兔年大吉&#xff0c;新春快乐。我本想写一篇《公有云换帅转舵后的错误经验和正经歪念》&#xff0c;但刚过年就炮火味十足&#xff0c;觉得有失体面。所以就写了篇读书的散记杂记。本文和云计算行业完全无关&#xff0c;就是一个有常识性阅…

[数据结构基础]排序算法第一弹 -- 直接插入排序和希尔排序

一. 排序的概念及分类 1.1 排序的概念 排序&#xff0c;就是使一串数据&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 1.2 常见的排序算法 图1.1按照排序算法的思想&#xff0c;将排序分为四大类&#xff1a;插入排序、选择排序…

OpenGL相关库及其关系概述

目录 1、OpenGL 2. .GLUT 3、Freeglut 4、glew 5、glfw 6、glad 1、OpenGL OpenGL只有框架没有实现&#xff0c;换句话说就是OpenGL只有函数声明没有源文件实现&#xff0c;类似于接口和虚函数。所有的实现是显卡生产商提供。比如NVIDIA或者AMD就要自己实现OpenGL函数内容…

前后端登录逻辑讲解-本文使用RSA加密过程-附代码

文章目录一、项目环境二、生成公钥私钥1.生成私钥2.查看私钥3.生成公钥4.查看公钥三、安装依赖包四、自测加解密1.纯前端自测2.前后端联调①前端登录代码改为&#xff1a;②后端登录接口代码&#xff1a;③验证五、与数据库密码进行对比&#xff08;数据库是进行了bcryptjs加密…

关于非授权访问的逻辑漏洞挖掘

简介 挖洞的时候日常笔记&#xff0c;很多细节都写得不好&#xff0c;师傅们不要介意 开始 获取目标&#xff1a;https://fofa.info/ 在fofa上输入body“后台登录”&#xff0c;可以看到一大堆目标&#xff0c;今天我打算去测试逻辑漏洞 进入目标地址 随意输入用户名和密码&…

Python LC Loan贷款数据集 统计分析 数据挖掘 研究报告

实验代码&#xff1a;https://download.csdn.net/download/Amzmks/87396462 首先读表 将有空值的列和完全相同的列删除 将数值型数据单独挑出来 将数值型数据从string转为float 用方差阈值法筛选特征较为明显的部分数值型数据 将文本型数据单独挑出来 去除所有的可能的头…

计算机存储系统

前言何为存储系统&#xff1f;存储系统是指计算机中由存放程序和数据的各种存储设备、控制部件及管理信息调度的设备&#xff08;硬件&#xff09;和算法&#xff08;软件&#xff09;所组成的系统。为何需要存储系统&#xff1f;信息是人类认知外界的方式&#xff0c;最初的信…

对CAS的理解

CAS的概念&#xff08;compare and swap&#xff09;:比较并交换我们首先要明白&#xff0c;自旋是一个纯用户态的操作&#xff0c;本身并没有进行加锁&#xff0c;所执行的代码&#xff0c;是在并发环境下执行的&#xff0c;没有锁。我们举一个例子来说明自旋加CAS的作用&…

Day05 C++STL入门基础知识三——String容器(上)概念-构造-赋值-拼接-查找-替换【全面深度剖析+例题代码展示】

文章目录1. 基本概念1.1 本质1.2 string与char*的区别1.3 特点2. 构造函数2.1 四种构造2.2 举例展示3. 赋值操作3.1 七种赋值函数原型&#xff08;operator等号赋值/assign成员函数赋值&#xff09;3.2 举例展示4. 拼接操作4.1 七种拼接函数原型4.2 举例展示5. 查找操作5.1 八种…

分布式概念

目录一、什么是分布式二、分布式与集群的关系三、软件架构演变四、RPC是什么1.RPC思想原理2.服务之间的交互可以用两种方式五、分布式思想与基本概念1.高并发2.高可用3.注册中心4.负载均衡5.服务雪崩6.熔断7.限流8.API网关9.服务跟踪10.弹性云一、什么是分布式 《分布式系统原…

Python流程控制语句之循环语句

上一篇&#xff1a;Python流程控制语句之选择语句 文章目录前言一、while 循环二、for 循环1. 进行数值循环2. 遍历字符串三、循环嵌套总结前言 生活中很多问题都无法一次解决&#xff0c;就像谚语所说&#xff1a;罗马不是一天建成的。一些事物必须周而复始地运转才能保证其存…

linux并发控制详解

目录 1.并发控制 1.1.并发概念 1.2.并发问题 2.多CPU核心 3.解决 4.中断屏蔽 5.原子操作 6.自旋锁 7.自旋锁衍生读写自旋锁 7.1.自旋锁与读写自旋锁的对比&#xff1a; 8.读写自旋锁衍生顺序锁 9.RCU 10.信号量 11.互斥体&#xff08;互斥锁&#xff09; 11.1.互…