当前位置: 首页 > news >正文

在SpringBoot使用AOP防止接口重复提交

前言

防止接口重复提交有跟多种方法,可以在前端做处理。同样在后端也能处理,而且后端的处理也有很多中方法。最先能想到的就是加锁,也可以直接在该接口的实现过程中进行处理(可以参考防止数据重复提交的6种方法(超简单)!),本文主要介绍另一种借助AOP实现的方法。

AOP

关于AOP就不做过多赘述,可以参考我的另一篇文章Spring框架(下半部分 -AOP)。主要是借助它能增强方法的功能,对接口做以下处理,这个方法跟直接在接口种处理相似,话不多说,我们直接开始吧。

自定义注解

我们要灵活的使用AOP,注解是必不可少的,能帮我们更加便捷灵活的处理。我们先创建一个Submit注解,有该注解的接口就是我们要使用AOP处理的接口。

package com.blog.annotation;import java.lang.annotation.*;@Documented
@Retention(RetentionPolicy.RUNTIME) // 注解的存活时间
@Target(ElementType.METHOD) // 作用在方法上
public @interface Submit {/*** 提交的间隔时间* 默认是10s* @return*/long expire() default 10000;
}

AOP的实现

其实使用AOP都有一个很创建的模板,我先贴出来,然后解释。

@Aspect
@Component
@Slf4j
public class SubmitAspect {@Pointcut("@annotation(com.blog.annotation.Submit)")public void pt() {}@Around("pt()")public Object around(ProceedingJoinPoint point) {}
}

@Pointcut("@annotation(com.blog.annotation.Submit)")就是切入点表达式,它的参数就是指定我们要处理,@Around("pt()")表明我们使用环绕通知来处理。具体的在我刚刚提到的另一篇博客中,感兴趣的可以仔细的了解一下。

接下来我们就要考虑该如何实现,防止接口重复提交就是说如果该接口提交过了,再来一次提交我们就不让他去执行,直接返回。现在就有一个问题了,我们该如何知道这个接口提交没提交过?我们是不是可以把提交过的接口保存下来,如果来了一个提交我们就去查找,如果找到了我们就不如他提交。

if (接口 not in 接口集合) {return "请勿重复提交";
}
// 说明接口没有提交,我们就执行该接口的方法
// 最重要的一点是把该接口存储到接口集合中
...执行提交操作...
接口集合.insert(接口)

所以我们就需要考虑使用哪些集合?这个接口该怎么存储?怎么执行原方法的操作?什么时候用户还能再次提交代码?等等,这些都是我们要考虑的问题。

关于集合的使用,我们首先能想到的是list、set、map等等,但是考虑到并发安全,我们应该使用线程安全的集合例如ConcurrentHashMap、CopyOnWriteArrayList等等。我们还要解决什么时候用户还能再次提交代码,我们可以设置一个实现,所以更加推荐ConcurrentHashMap,其key值就是我们为每一个接口构建的key(使用类名+方法名),value就是我们设置的时间。

想到这还有一个问题,我们为每一个接口构建key,如果有多个用户那么他们的key就是一样的,可事实上每个用户的同一接口的key一定是不能一样的,否则他提交了我提交不了,这凭什么?所以我们再构建每一个接口的key时加上当前用户的唯一标识,使用该用户的id就行。

那么又该如何获取到当前用户的id呢? 在这里我们ThreadLocal就可以,ThreadLocal也是很重要的,如果不是很了解,建议花点时间去认识它。在这里我们只需要知道,他是独立于线程之外的,每一个线程又一个独自的ThreadLocal ,也就是说,我们把每一个用户都存储在ThreadLocal 中。要的时候直接get就行。

到这里其实核心的问题都已经解决了,剩下的就是一些细节问题,在自己写的时候就能注意到。这里给出我的实现。我使用的redis实现,因为它设置过期时间会自动清除,不需要我们手动去清除,再加上redis是天生支持高并发。
SUBMIT_KEY_PREFIX和NOT_SUBMIT_REPEATEDLY都是一个常量而已,不用过多注意。

package com.blog.aspect;import com.alibaba.fastjson.JSON;
import com.blog.annotation.Submit;
import com.blog.utils.JWTUtils;
import com.blog.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.Duration;import static com.blog.domain.vo.ErrorCode.NOT_SUBMIT_REPEATEDLY;
import static com.blog.utils.ConstantValue.SUBMIT_KEY_PREFIX;@Aspect
@Component
@Slf4j
public class SubmitAspect {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Pointcut("@annotation(com.blog.annotation.Submit)")public void pt() {}@Around("pt()")public Object around(ProceedingJoinPoint point) {try {
//            User user = UserThreadLocal.get();
//            String UserId = user.getId();// 假设这里是从ThreadLocal获取到的用户id。String UserId = "123456";Signature signature = point.getSignature();// 获取当前类名String className = point.getTarget().getClass().getSimpleName();// 获取当前方法名String methodName = signature.getName();// 拿到该方法Method method = ((MethodSignature) signature).getMethod();// 获取Submit注解Submit annotation = method.getAnnotation(Submit.class);// 获取过期时间long expire = annotation.expire();// 设置key值,每个用户对与每一个接口的key都是一样的String key = SUBMIT_KEY_PREFIX + DigestUtils.md5Hex(UserId) + "::" + className + "::" + methodName;// 首先查看是否已经提交过String value = redisTemplate.opsForValue().get(key);if (StringUtils.isNoneEmpty(value)) {return Result.error(NOT_SUBMIT_REPEATEDLY.getCode(), NOT_SUBMIT_REPEATEDLY.getMsg());}// 没有提交过就执行原方法Object proceed = point.proceed();redisTemplate.opsForValue().set(key, JSON.toJSONString(proceed), Duration.ofMillis(expire));return proceed;} catch (Throwable throwable) {throwable.printStackTrace();}return Result.error(-999, "系统异常");}
}

测试

接下来我们使用ApiPost进行测试,由于我们给定了id,所以我们只能测试单用户的,如果想测试多用户的,可以在请求路径中加上一个id,来模拟多用户。

间隔0ms,调用5次,只有一次成功,失败的几次,这里就不截图了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e2fHCLrs-1720583474961)(https://i-blog.csdnimg.cn/direct/e0b09889def54e4494172c9edc1571e1.png)]
间隔11000ms,调用2次,每次都成功,这是因为我们的冷静窗口是10000ms。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zG3MVA4q-1720583474962)(https://i-blog.csdnimg.cn/direct/a0153a775a524fd0821df825fa3154ff.png)]

相关文章:

  • C# Bitmap类型与Byte[]类型相互转化详解与示例
  • 需求分析|泳道图 ProcessOn教学
  • Games101——光珊化——深度缓存——shading着色 1
  • 旷野之间3 – CTO 应具备的技能
  • 【ARMv8/v9 GIC 系列 5.1 -- GIC GICD_CTRL Enable 1 of N Wakeup Function】
  • 记一次mysql导出到达梦数据库
  • 8.5结构体嵌套结构体
  • ONNX加载模型问题总结
  • 筛斗数据:数据提取技术,驱动业务增长的新引擎
  • 人工智能+影像组学的交叉课题,患者的临床特征如何收集与整理|顶刊专题汇总·24-07-10
  • ChatGPT 5.0:一年后的猜想
  • 为何Expo成为React Native官方推荐框架?
  • 连续6年夺冠 6项细分领域第一,中电金信持续领跑中国银行业IT解决方案市场
  • python学习-类
  • 小程序开发页面获取小程序assess文件夹下所有图片
  • [ 一起学React系列 -- 8 ] React中的文件上传
  • [deviceone开发]-do_Webview的基本示例
  • 【面试系列】之二:关于js原型
  • Apache Pulsar 2.1 重磅发布
  • CNN 在图像分割中的简史:从 R-CNN 到 Mask R-CNN
  • gitlab-ci配置详解(一)
  • in typeof instanceof ===这些运算符有什么作用
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • JS基础篇--通过JS生成由字母与数字组合的随机字符串
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • Spring-boot 启动时碰到的错误
  • 如何编写一个可升级的智能合约
  • 什么是Javascript函数节流?
  • 正则学习笔记
  • 《码出高效》学习笔记与书中错误记录
  • MyCAT水平分库
  • 阿里云服务器如何修改远程端口?
  • ​​​​​​​​​​​​​​Γ函数
  • #if #elif #endif
  • (1) caustics\
  • (12)Linux 常见的三种进程状态
  • (2)空速传感器
  • (31)对象的克隆
  • (C语言)fread与fwrite详解
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (MIT博士)林达华老师-概率模型与计算机视觉”
  • (ZT) 理解系统底层的概念是多么重要(by趋势科技邹飞)
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (附源码)springboot教学评价 毕业设计 641310
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (九)信息融合方式简介
  • (亲测有效)推荐2024最新的免费漫画软件app,无广告,聚合全网资源!
  • (三十)Flask之wtforms库【剖析源码上篇】
  • (十)DDRC架构组成、效率Efficiency及功能实现
  • (算法设计与分析)第一章算法概述-习题
  • (一) storm的集群安装与配置
  • (转)利用ant在Mac 下自动化打包签名Android程序
  • (转)视频码率,帧率和分辨率的联系与区别
  • (最简单,详细,直接上手)uniapp/vue中英文多语言切换
  • *++p:p先自+,然后*p,最终为3 ++*p:先*p,即arr[0]=1,然后再++,最终为2 *p++:值为arr[0],即1,该语句执行完毕后,p指向arr[1]