通过spring boot/redis/aspect 防止表单重复提交【防抖】

news2025/1/15 6:47:03

一、啥是防抖
 

所谓防抖,一是防用户手抖,二是防网络抖动。在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,进而生成重复的数据记录。要针对用户的误操作,前端通常会实现按钮的loading状态,阻止用户进行多次点击。而对于网络波动造成的请求重发问题,仅靠前端是不行的。为此,后端也应实施相应的防抖逻辑,确保在网络波动的情况下不会接收并处理同一请求多次。

一个理想的防抖组件或机制,我觉得应该具备以下特点:

逻辑正确,也就是不能误判;

响应迅速,不能太慢;

易于集成,逻辑与业务解耦;

良好的用户反馈机制,比如提示“您点击的太快了”

二、思路解析
前面讲了那么多,我们已经知道接口的防抖是很有必要的了,但是在开发之前,我们需要捋清楚几个问题。

2.1.哪一类接口需要防抖?
接口防抖也不是每个接口都需要加,一般需要加防抖的接口有这几类:

用户输入类接口:比如搜索框输入、表单输入等,用户输入往往会频繁触发接口请求,但是每次触发并不一定需要立即发送请求,可以等待用户完成输入一段时间后再发送请求。

按钮点击类接口:比如提交表单、保存设置等,用户可能会频繁点击按钮,但是每次点击并不一定需要立即发送请求,可以等待用户停止点击一段时间后再发送请求。

滚动加载类接口:比如下拉刷新、上拉加载更多等,用户可能在滚动过程中频繁触发接口请求,但是每次触发并不一定需要立即发送请求,可以等待用户停止滚动一段时间后再发送请求。

2.2.如何确定接口是重复的?
防抖也即防重复提交,那么如何确定两次接口就是重复的呢?首先,我们需要给这两次接口的调用加一个时间间隔,大于这个时间间隔的一定不是重复提交;其次,两次请求提交的参数比对,不一定要全部参数,选择标识性强的参数即可;最后,如果想做的更好一点,还可以加一个请求地址的对比。

  • 定义一个RequestLock,配置超时时间、异常消息、分组标识(用户标识)
/**
 * 请求锁,防止重复提交
 *
 * @author xt
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLock {
    /**
     * 过期时间
     *
     * @return
     */
    long expire() default 3;

    /**
     * 异常提示
     *
     * @return
     */
    String message() default "您的操作太快了,请稍后重试";

    /**
     * 参数分隔符
     *
     * @return
     */
    String delimiter() default "|";

    /**
     * 时间单位
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 前缀(从请求header key)
     *
     * @return
     */
    String group() default "loginuserid";
}

  • 定义一个aspect 实现对注解RequestLock的endpoint进行拦截
@EnableAspectJAutoProxy
@Aspect
@Configuration
@Order
public class RequestLockAspect {
    @Resource
    private RedisTemplate redisTemplate;

    @Pointcut("execution(public * * (..)) && @annotation(org.xt.shisui.redis.duplicate.RequestLock)")
    public void endpointPointcut() {
    }

    @Around("endpointPointcut()")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        if (redisTemplate != null) {
            String key = RequestLockKeyGenerator.getLockKey(joinPoint);
            Boolean success = redisTemplate.opsForValue().setIfAbsent(key, new byte[0], requestLock.expire(), requestLock.timeUnit());
            if (Boolean.FALSE.equals(success)) {
                return Response.no(requestLock.message());
            }
        }
        return joinPoint.proceed();
    }
}
  • 根据请求参数构建RequestLock锁的key,即Redis存储的key

/**
 * 根据请求参数构建锁的key
 *
 * @author xt
 * @date 2022-07-15 14:21
 */
public class RequestLockKeyGenerator {
    public static String getLockKey(ProceedingJoinPoint joinPoint) {
        String ipAddress = null, group = null;
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //方法名称
        String methodName = signature.getName();
        //类路径
        String declaringTypeName = signature.getDeclaringTypeName();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        if (attributes != null) {
            //加上请求中的ip和分组标识,防止错误拦截
            HttpServletRequest request = attributes.getRequest();
            ipAddress = request.getRemoteAddr();
            group = request.getHeader(requestLock.group());
        }
        final Object[] args = joinPoint.getArgs();
        final Parameter[] parameters = method.getParameters();
        StringBuilder params = new StringBuilder();
        String delimiter = requestLock.delimiter();
        for (int i = 0; i < parameters.length; i++) {
            //忽略特殊参数,如图片、大文本等,如果是存hashcode 可以不需要这个注解
            final RequestLockKeyIgnore keyIgnore = parameters[i].getAnnotation(RequestLockKeyIgnore.class);
            if (keyIgnore != null) {
                continue;
            }
            Object arg = args[i];
            if (arg != null) {
                params.append(delimiter).append(arg);
            }
        }
        StringBuilder result = new StringBuilder();
        result.append(declaringTypeName).append(delimiter).append(methodName).append(delimiter).append(ipAddress).append(delimiter).append(delimiter).append(group).append(params.hashCode());
        return result.toString();
    }
}

  • 如果Redis存储请求参数字符串,可以增加特殊参数忽略注解,如图片等属性,建议用hashcode
/**
 * 忽略该参数,防止一些base64字符串被当做主键
 *
 * @author xt
 * @date 2022-01-05 14:37
 */
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLockKeyIgnore {
}
  • 具体使用demo
    @RequestLock(expire = 5)
    @ApiOperation("新增")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public Response<ChatSpeechcraftCategoryCreateResp> create(@RequestBody @Validated ChatSpeechcraftCategoryCreateReq req, final HttpServletRequest request) throws SimpleException {
        return chatSpeechcraftCategoryApiService.create(req);
    }

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

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

相关文章

解决ubuntu 22.04新内核6.5.0-15无法编译NVIDIA显卡驱动

这里的新内核应该包括6.5.*系列的 文章目录 遇到的问题&#xff1a; 遇到的问题&#xff1a; 今天我在安装NVIDIA显卡驱动发现了一个问题&#xff0c;主要日志如下所示&#xff1a; make[3]: *** [scripts/Makefile.build:251: /tmp/selfgz1310041/NVIDIA-Linux-x86_64-550.5…

综合利用Cisco Packet Tracer模拟器配置园区网

1. 内容 1.在课室交换机中创建各个课室的VLAN&#xff0c;并将1-20端口平均分配给各个课室。 2.使用课室交换机的每个端口只能接入一台计算机&#xff0c;发现违规就丢弃未定义地址的包。3.网络内部使用DHCP分配各课室的IP地址&#xff0c;在课室交换机按照第一题划分的VLAN地…

蜡烛图K线图采用PictureBox控件绘制是实现量化交易的第一步非python量化

用vb6.0开发的量化交易软件 VB6量化交易软件的演示视频演示如上 股票软件中的蜡烛图是非常重要的一个东西&#xff0c;这里用VB6.0自带的Picture1控件的Line方法就可以实现绘制。 关于PictureBox 中的line 用法 msdn 上的说明为如下所示 object.Line [Step] …

C#使用迭代算法计算斐波那契数列通项

目录 1.斐波纳契数列 2.迭代一次产生1个新的通项 3.迭代一次产生2个新的通项 1.斐波纳契数列 斐波纳契数列的定义是&#xff0c;它的第一项和第二项均为1&#xff0c;以后各项都为前两项之和。 公式如下&#xff1a; F(n) F(n-1) F(n-2) 其中&#xff0c;F(1) 0,…

CTP-API开发系列之十:v6.7.0-Python版封装(Windows/Linux)(附源码)

CTP-API开发系列之十&#xff1a;v6.7.0-Python版封装&#xff08;Windows/Linux&#xff09;&#xff08;附源码&#xff09; CTP-API开发系列之十&#xff1a;v6.7.0-Python版封装&#xff08;Windows/Linux&#xff09;&#xff08;附源码&#xff09;资源获取准备工作Windo…

实验2 芯片测试算法设计

一、【实验目的】 &#xff08;1&#xff09;理解分治策略的设计思想&#xff1b; &#xff08;2&#xff09;熟悉将伪码转换为可运行的程序的方法&#xff1b; &#xff08;3&#xff09;能够根据算法的要求设计具体的实例。 二、【实验内容】 有n片芯片&#xff0c;其中好芯片…

蓝桥杯每日一题:血色先锋队

今天浅浅复习巩固一下bfs 答案&#xff1a; #include<iostream> #include<algorithm> #include<cstring>using namespace std; typedef pair<int,int> PII;const int N510; int n,m,a,b; int dist[N][N]; PII q[N*N]; int hh0,tt-1;int dx[]{1,0,-1,…

蓝桥杯[OJ 1621]挑选子串-CPP-双指针

目录 一、题目描述&#xff1a; 二、整体思路&#xff1a; 三、代码&#xff1a; 一、题目描述&#xff1a; 二、整体思路&#xff1a; 要找子串&#xff0c;则必须找头找尾&#xff0c;找头可以遍历连续字串&#xff0c;找尾则是要从头的基础上往后遍历&#xff0c;可以设头…

【spring】@Import 注解学习

Import 介绍 Import 是 Spring 框架中的一个注解&#xff0c;用于导入配置类或组件。它可以将一个或多个配置类或组件导入到当前的配置类或组件中&#xff0c;从而实现配置的复用和组合。 在Spring Boot应用中&#xff0c;Import注解可以帮助我们更加灵活地组织和管理配置类。…

(学习日记)2024.03.09:UCOSIII第十一节:就绪列表

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

正点原子精英版TFTLCD代码移植

&#xff08;1&#xff09;将lcd.c和lcd.h加入到HEADWARE文件中 &#xff08;2&#xff09;将lcd.c加入到环境中 选择lcd.c即可。 &#xff08;3&#xff09;在FWLib中添加stm32f10x_fsmc.c

Spring Boot整合canal实现数据一致性解决方案解析-部署+实战

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 目录 1.前言 2.canal部署安装 3.Spring Boot整合canal 3.1数据库与缓存一致性问题…

一篇普通的生活周记

学习进度汇报&#xff1a; 这周主要是参考着视频敲完了一个vue2后台项目&#xff0c;主要是vue2element-ui,因为之前写项目的时候用过lay-ui&#xff0c;虽然是结合着node.js写的&#xff0c;但是大差不差&#xff0c;所以上手也很快。同时&#xff0c;学长发给我们了ruoyi项目…

【Vue3】Vue3中路由规则的 props 配置

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

页面侧边栏顶部固定和底部固定方法

顶部固定用于侧边栏低于屏幕高度----左侧边栏 底部固定用于侧边栏高于屏幕高度----右侧边栏 vue页面方法 页面布局 页面样式&#xff0c;因为内容比较多&#xff0c; 只展示主要代码 * {margin: 0;padding: 0;text-align: center; } .head {width: 100%;height: 88px;back…

Vue+SpringBoot打造高校宿舍调配管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能需求2.1 学生端2.2 宿管2.3 老师端 三、系统展示四、核心代码4.1 查询单条个人习惯4.2 查询我的室友4.3 查询宿舍4.4 查询指定性别全部宿舍4.5 初次分配宿舍 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的…

数据结构--线性表

1.线性表的定义&#xff1a; 存在唯一的一个被称为“第一个”的数据元素&#xff1b; 存在唯一的一个被称为“最后一个”的数据元素&#xff1b; 除第一个之外&#xff0c;集合中的每一个数据元素都只有一个前驱&#xff1b; 除最后一个之外&#xff0c;集合中的每一个数据…

安卓百度地图API显示隐藏Marker

方法 BaiduMap.Marker.setVisible(boolean) 实现 List<Marker> list_marker new ArrayList<>(); boolean isShowMarker true;Override public boolean onCreateOptionsMenu(Menu menu) {String[] sm { "显隐信息", "显隐照片", "截…

【Python】新手入门学习:详细介绍迪米特原则(LoD)及其作用、代码示例

【Python】新手入门学习&#xff1a;详细介绍迪米特原则&#xff08;LoD&#xff09;及其作用、代码示例 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTor…

IDEA编译安卓源码TVBox(2)

一、项目结构&#xff1a;主要app和player app结构 二、增加遥控器按键选台 修改LivePlayActivity.java 1、声明变量 public String channelId "";public Timer timer new Timer();public Toast mToast;2、定义方法 private void mToastShow(String s){mToast …