springboot3使用自定义注解+AOP+redis优雅实现防重复提交

 

⛰️个人主页:     蒾酒

🔥系列专栏:《spring boot实战》

🌊山高路远,行路漫漫,终有归途


目录

写在前面

实现思路

实现步骤

1.定义防重复提交注解

2.编写一个切面去发现该注解然后执行防重复提交逻辑

3.测试

依赖条件

1.接口上标记防重复提交注解

2.接口测试

写在最后


写在前面

本文介绍了springboot开发后端服务中,防重复提交功能的设计与实现,坚持看完相信对你有帮助。

同时欢迎订阅springboot系列专栏,持续分享spring boot的使用经验。

实现思路

通过定义一个防重复提交的自定义注解,再通过AOP的前置通知拦截带有该注解的方法,执行防重复提交逻辑,需要拼接一个唯一的key,如果redis中不存在则代表第一次请求,将这个key存入redis,设置注解类中指定的过期时间,遇到下次重复提交请求,直接抛出对应异常,全局异常处理返回对应信息即可。

需要注意

这个key的生成需要考虑有token和无token情况,同时满足唯一性。

  • 有 token;可以用 token+请求参数,做为唯一值!
  • 无 token:可以用请求路径+请求参数,做为唯一值!

实现步骤

1.定义防重复提交注解

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;/*** @author mijiupro*/
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {/*** 锁定时间,默认5000毫秒*/int interval() default 5000;/*** 锁定时间单位,默认毫秒*/TimeUnit timeUnit() default TimeUnit.MILLISECONDS;/*** 提示信息*/String message() default "不允许重复提交,请稍后再试!";}

2.编写一个切面去发现该注解然后执行防重复提交逻辑

因为缓存的key有拼接请求参数,所以遇到文件类型的参数需要进行过滤,拼接逻辑以及参数过滤方法都在下面代码中。

import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import com.mijiu.commom.aop.annotation.RepeatSubmit;
import com.mijiu.commom.exception.GeneralBusinessException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;import java.util.Collection;
import java.util.Map;
import java.util.Objects;/*** @author mijiupro*/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {private final StringRedisTemplate redisTemplate;public RepeatSubmitAspect(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}@Before("@annotation(repeatSubmit)")public void before(JoinPoint joinPoint, RepeatSubmit repeatSubmit) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = null;if (attributes != null) {request = attributes.getRequest();}//请求参数拼接String requestParams = argsArrayToString(joinPoint.getArgs());String authorizationHeader = null;if (request != null) {authorizationHeader = request.getHeader("Authorization");}String submitKey = null;if (authorizationHeader != null) {//如果存在token则通过token+请求参数生成唯一标识String token = StringUtils.removeStart(authorizationHeader, "Bearer ");submitKey= SecureUtil.md5(token+":"+requestParams);} else{//不存在token则通过请求url+参数生成唯一标识if (request != null) {submitKey = SecureUtil.md5(request.getRequestURL().toString()+":"+requestParams);}}//缓存keyString cacheKey = "repeat_submit:"+submitKey;if (Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey))) {throw new GeneralBusinessException(repeatSubmit.message());}redisTemplate.opsForValue().set(cacheKey, "1", repeatSubmit.interval(), repeatSubmit.timeUnit());}/*** 参数拼接* @param args  参数数组* @return 拼接后的字符串*/private String argsArrayToString(Object[] args){StringBuilder params = new StringBuilder();if(args!= null && args.length > 0){for(Object o:args){if(Objects.nonNull(o)&&!isFilterObject(o)){try {params.append(JSONUtil.toJsonStr(o)).append(" ");}catch (Exception e){log.error("参数拼接异常:{}",e.getMessage());}}}}return params.toString().trim();}/*** 判断是否需要过滤的对象。* @param o  对象* @return true:需要过滤;false:不需要过滤*/private boolean isFilterObject(final Object o) {Class<?> c = o.getClass();//如果是数组且类型为文件类型的需要过滤if(c.isArray()){return  c.getComponentType().isAssignableFrom(MultipartFile.class);}//如果是集合且类型为文件类型的需要过滤else if(Collection.class.isAssignableFrom(c)){Collection collection = (Collection) o;for(Object value:collection){return value instanceof MultipartFile;}}//如果是Map且类型为文件类型的需要过滤else if(Map.class.isAssignableFrom(c)){Map map = (Map) o;for(Object value:map.entrySet()){Map.Entry entry = (Map.Entry) value;return entry.getValue() instanceof MultipartFile;}}//如果是文件类型的需要过滤return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse|| o instanceof BindingResult;}}

3.测试

依赖条件

redis:

Spring Boot3整合Redis_springboot3整合redis-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136067550?spm=1001.2014.3001.5502

全局异常捕获:

Spring Boot3自定义异常及全局异常捕获_全局异常捕获 自定义异常-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/136110267?spm=1001.2014.3001.5502

swagger3:

Spring Boot3整合knife4j(swagger3)_springboot3 knife4j-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/135761392?spm=1001.2014.3001.5502

hutool工具包:

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.25</version>
</dependency>

1.接口上标记防重复提交注解

随便写个测试接口添加防重复提交注解设置间隔5000毫秒

    @PostMapping("/add")@RepeatSubmit(interval= 5000)public void test(@RequestBody User user){//添加用户的操作逻辑。。。}

2.接口测试

第一次提交

可以看到对应缓存已经存入redis了

5s内第二次提交

写在最后

springboot使用自定义注解+AOP+redis优雅实现防重复提交到这里就结束了,本文介绍了一种通用的防重复提交的实现方式,代码逻辑清晰。任何问题评论区或私信讨论,欢迎指正。

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

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

相关文章

消息队列:揭开异步通信的神秘面纱,揭示其无可替代的价值与应用场景

在现代分布式系统架构中&#xff0c;消息队列&#xff08;Message Queue, MQ&#xff09;作为一种核心的中间件技术&#xff0c;其价值与重要性日益凸显。本文将深入剖析消息队列的使用好处&#xff0c;探讨其适用场景&#xff0c;并揭示为何在众多系统设计中&#xff0c;消息队…

RAGFlow:基于OCR和文档解析的下一代 RAG 引擎

一、引言 在人工智能的浪潮中&#xff0c;检索增强生成&#xff08;Retrieval-Augmented Generation&#xff0c;简称RAG&#xff09;技术以其独特的优势成为了研究和应用的热点。RAG技术通过结合大型语言模型&#xff08;LLMs&#xff09;的强大生成能力和高效的信息检索系统…

书生·浦语大模型实战营 | 第2次学习笔记

前言 书生浦语大模型应用实战营 第二期正在开营&#xff0c;欢迎大家来学习。&#xff08;参与链接&#xff1a;课程升级&#xff0c;算力免费&#xff0c;书生浦语实战营第二期学员招募&#xff5c;活动预告https://mp.weixin.qq.com/s/YYSr3re6IduLJCAh-jgZqg&#xff09; …

GFS部署实验

目录 1、部署环境 ​编辑 2、更改节点名称 3、准备环境 4、磁盘分区&#xff0c;并挂载 5. 做主机映射--/etc/hosts/ 6. 复制脚本文件 7. 执行脚本完成分区 8. 安装客户端软件 1. 创建gfs 2. 安装解压源包 3. 安装 gfs 4. 开启服务 9、 添加节点到存储信任池中 1…

SpringBoot项目如何国际化操作,让你可以随意切换语言

1.前言 最近接触的项目需要中文/英文或者其他国家语言的切换&#xff0c;在后台的时候有一个选择&#xff0c;你可以选择中文还是英文&#xff0c;或者其他语言&#xff0c;选择完毕界面语言就都变了&#xff0c;咱不知道前端怎么操作的&#xff0c;但是后台在处理提示语的时候…

SVN(Subversion)代码版本管理

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言SVN(Subversion)和Git的比较SVN进行版本控制的基本步骤前言 认知有限,望大家多多包涵,有什么问题也希望能够与大家多交流,共同成长! 本文先…

MySQL-8. mysql索引

说起提高数据库性能&#xff0c;索引是最物美价廉的东西了。不用加内存&#xff0c;不用改程序&#xff0c;不用调sql&#xff0c;查询速度就可能提高百倍干倍。 索引本身会占用空间&#xff0c;以空间换时间。索引的代价&#xff1a;磁盘占用&#xff0c;影响DML语句效率&…

软件SPI读写W25Q64

文章目录 前言接线引脚定义图 软件SPI读写W25Q64代码规划代码实现ThisSPI.cThisW25Q64.cmain.c 前言 SPI介绍&#xff1a;https://blog.csdn.net/qq_53922901/article/details/137142038 W25Q64介绍&#xff1a; https://blog.csdn.net/qq_53922901/article/details/137197048…

C++性能测试工具

使用示例main.cpp // g-13 -O3 -stdc17 main.cpp profile.cpp #include <iostream> #include <chrono> #include <stdint.h> #include <mutex> // std::mutex#include "profile.h" #include "profile_rdtsc.h"std::mut…

Java基于SpringBoot+Vue 的医院预约挂号系统

博主介绍&#xff1a;✌程序员徐师兄、10年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447…

【问题记录】使用Audition播放时低8位数据会被修改

一&#xff0c;问题现象记录 使用Audition连接UAC播放采样点数据比较大的时候&#xff0c;低8位的数据会被修改。如果对低8位数据敏感的情况&#xff0c;需要使用其他播放器进行播放。 二&#xff0c;问题复现 1&#xff0c;使用C代码生成一个PCM文件&#xff1a; #include…

智慧水库解决方案(打造水库智慧监测体系)

​作为一名水利自动化系统集成商,最近我司接手了一个智慧水库建设项目。这个项目位于一座山区的大型水库,目的是对其进行现代化、智能化改造,提升供水、防洪等管理水平。&#xff08;key-iot.com.cn&#xff09; 在方案设计之初,我们组织了现场勘测,全面了解水库的实际情况。这…

win11系统和ubuntu双系统首次连接网线上网流程

硬件准备 首先需要将网线连接到电脑&#xff0c;另一头可以连接交换机或者路由器 上网前需要拨号上网&#xff0c;如果是连的路由器&#xff0c;那么一台路由器上拨号一次就行了。 如果是连的交换机需要拨号上网 这里踩的第一个坑是刚开始电脑连的是交换机1又连的交换机2&…

【Python】如何将轨迹node路径匹配到路网edge信息

外面纷纷扰扰里面乱乱糟糟 我们别再闹了 这个冬天已然很冷了 我们靠在一起好吗 我们聊些有趣的事情 别把气氛弄的如此低沉 我知道这几天你心烦 每个人的活都不简单 &#x1f3b5; 毛不易《别再闹了》 在地理信息系统&#xff08;GIS&#xff09;和路网分析…

Pandas追加写入文件的时候写入到了第一行

# 原代码 def find_money(file_path, account, b_account, money, type_word, time):file pd.read_excel(file_path)with open(money.csv, a, newline, encodingutf-8) as f:for i in file.index:省略中间的代码if 省略中间的代码:file.loc[[i]].to_csv(f,indexFalse)find_sam…

使用 kustomize 对 kubernetes 对象进行声明式管理

补丁实战 策略合并补丁 基准文件&#xff1a;/test/bases/deploy.yml apiVersion: apps/v1 kind: Deployment metadata:namespace: sharkname: my-nginx spec:selector:matchLabels:run: my-nginxreplicas: 2template:metadata:labels:run: my-nginxspec:containers:- name:…

STM32中C编程引入C++程序

C具备类的创建思想很实用于实际场景多相似性的框架搭建&#xff1b;同种类型或相似类型的C的优势明显因此进行相互嵌套使用 需要在C中使用C类的话&#xff0c;你可以通过C的“extern "C"”语法来实现。这允许你在C代码中使用C的链接方式&#xff0c;而在C代码中使用…

4月5日排序算法总结(1)

冒泡排序 利用每趟都确定出一个最大值或者最小值 如果需要排一个从小到大的数组&#xff0c;那么我们每一趟都要确定一个最大值放在最后&#xff0c;一共有n个数&#xff0c;我们最多需要排列n-1趟就可以了&#xff0c;我们可以改进自己的代码&#xff0c;利用一个flag标记&a…

【Linux】UDP编程【上】{诸多编程接口/小白入门式讲解}

文章目录 0.预备知识0.1套接字0.2TCP/UDP0.3大小端问题 1.socket 常见API1.1socket1.2各个接口1.3int bind();1.3网络头文件四件套1.4bzero1.5recvfrom1.6sendto() 2.UDP编程2.1服务器编程2.2客户端编程2.3运行测试2.3.1本机通信2.3.2popen2.3.3strcasestr2.3.4回顾C11智能指针…

微电网优化:基于肝癌算法(Liver Cancer algorithm, LCA)的微电网优化(提供MATLAB代码)

一、微电网优化模型 微电网是一个相对独立的本地化电力单元&#xff0c;用户现场的分布式发电可以支持用电需求。为此&#xff0c;您的微电网将接入、监控、预测和控制您本地的分布式能源系统&#xff0c;同时强化供电系统的弹性&#xff0c;保障您的用电更经济。您可以在连接…