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

2024-03-15 20:28

本文主要是介绍通过spring boot/redis/aspect 防止表单重复提交【防抖】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、啥是防抖
 

所谓防抖,一是防用户手抖,二是防网络抖动。在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 {@Resourceprivate 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);}

这篇关于通过spring boot/redis/aspect 防止表单重复提交【防抖】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/813168

相关文章

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、