在 Web
应用中,防止重复提交是一个重要的功能,主要意义如下:
1. 避免重复操作
用户可能因网络延迟、误操作或多次点击提交按钮,导致同一请求被多次发送到服务器。
防止重复提交可以避免重复执行相同的业务逻辑(如重复下单、重复支付等),确保数据一致性。
2. 提升系统性能
重复提交会增加服务器的负载,尤其是对于耗时的操作(如文件上传、数据库写入等)。
防止重复提交可以减少不必要的资源消耗,提升系统性能。
3. 防止数据错误
重复提交可能导致数据重复插入或状态不一致(如订单状态错误、库存扣减异常等)。
防止重复提交可以确保数据的准确性和完整性。
4. 提升用户体验
通过防止重复提交,可以避免用户因误操作而收到重复的响应或错误提示。
提供友好的提示(如“请勿重复提交”),提升用户体验。
5. 增强安全性
防止恶意用户通过重复提交攻击系统(如重复请求消耗资源、刷单等)。
通过防重机制(如 Token 验证),增强系统的安全性。
AOP优势
在项目中使用 AOP
(面向切面编程)注解实现防重复提交具有以下优势:
1. 代码解耦:将日志记录逻辑与业务逻辑分离,避免代码重复,提升代码可读性和可维护性。
2. 灵活性与扩展性:通过注解可以灵活地控制哪些方法需要记录日志,便于扩展和修改日志记录逻辑。
3. 非侵入性:无需修改现有业务代码,只需在方法上添加注解即可实现日志记录。
4. 集中管理:日志记录逻辑集中在切面中,便于统一管理和维护。
5. 提高开发效率:通过注解方式快速实现日志功能,减少重复代码编写。
定义注解
在 xiaomayi-common/xiaomayi-idempotent
模块中定义的防重复提交的 AOP
切面 RepeatSubmit
文件,内容如下:
js
package com.xiaomayi.idempotent.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 自定义重复提交注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(ms),默认5秒;小于此时间提交被视为重复提交
*/
int interval() default 5000;
/**
* 事件单位
*
* @return 返回结果
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 重复提交错误提示
*/
String message() default "不允许重复提交,请稍候再试";
}
注解实现
js
package com.xiaomayi.idempotent.aspect;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson2.JSON;
import com.xiaomayi.core.config.TokenConfig;
import com.xiaomayi.core.constant.CacheConstant;
import com.xiaomayi.core.exception.BizException;
import com.xiaomayi.core.utils.MessageUtils;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.core.utils.ServletUtils;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.idempotent.annotation.RepeatSubmit;
import com.xiaomayi.redis.utils.RedisUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/**
* <p>
* 自定义重复提交验证AOP切面
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Aspect
public class RepeatSubmitAspect {
private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();
/**
* 切点之前
*
* @param point 切点
* @param repeatSubmit 重复提交验证注解
* @throws Throwable 异常处理
*/
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 获取注解间隔时间(ms),默认5秒;
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
// 提交间隔时间不能小于1秒
if (interval < 1000) {
throw new BizException("重复提交间隔时间不能小于'1'秒");
}
// 获取网络请求
HttpServletRequest request = ServletUtils.getRequest();
// 设置请求地址作为缓存KEY值
String url = request.getRequestURI();
// 请求参数拼接处理
String params = paramsExecute(point.getArgs());
// 获取请求头作为唯一键
String submitKey = StringUtils.trimToEmpty(request.getHeader(TokenConfig.getHeader()));
// MD5加密
submitKey = SecureUtil.md5(submitKey + ":" + params);
// 设置参数拼接作为唯一标识:常量KEY+请求URL+消息头加密值
String cacheRepeatKey = CacheConstant.REPEAT_SUBMIT_KEY + url + submitKey;
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
// 设置缓存
KEY_CACHE.set(cacheRepeatKey);
} else {
// 重复提交返回提示语
String message = repeatSubmit.message();
if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
}
throw new BizException(message);
}
}
/**
* 请求处理结束后执行处理
*
* @param joinPoint 切点
* @param repeatSubmit 重复请求注解
* @param jsonResult 返回结果
*/
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof R<?> r) {
try {
// 成功则不删除redis数据 保证在有效时间内无法重复提交
if (r.getCode() == 0) {
return;
}
RedisUtils.deleteObject(KEY_CACHE.get());
} finally {
KEY_CACHE.remove();
}
}
}
/**
* 请求处理结束后异常处理
*
* @param joinPoint 切点
* @param repeatSubmit 重复请求注解
* @param exception 异常处理
*/
@AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "exception")
public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception exception) {
// 根据缓存KEY删除缓存对象
RedisUtils.deleteObject(KEY_CACHE.get());
// 删除缓存KEY
KEY_CACHE.remove();
}
/**
* 请求参数拼接处理
*
* @param objects 请求参数
* @return 返回结果
*/
private String paramsExecute(Object[] objects) {
StringJoiner params = new StringJoiner(" ");
// 数组判空
if (ArrayUtil.isEmpty(objects)) {
return params.toString();
}
// 遍历参数
for (Object param : objects) {
if (ObjectUtil.isNotNull(param) && !isFilterObject(param)) {
params.add(JSON.toJSONString(param));
}
}
// 返回结果
return params.toString();
}
/**
* 判断是否需要过滤的对象
*
* @param object 对象
* @return 返回结果
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object object) {
Class<?> clazz = object.getClass();
if (clazz.isArray()) {
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) object;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) object;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return object instanceof MultipartFile || object instanceof HttpServletRequest || object instanceof HttpServletResponse
|| object instanceof BindingResult;
}
}
添加依赖
在 pom.xml
配置文件中引入以下依赖:
js
<!-- 幂等性模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-idempotent</artifactId>
</dependency>
注解使用
在需要设置防重复提交的方法上添加以下注解:
js
@RepeatSubmit(interval = 1, timeUnit = TimeUnit.SECONDS)
使用案例:
js
package com.xiaomayi.admin.controller.demo;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.idempotent.annotation.RepeatSubmit;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.RequestType;
import com.xiaomayi.security.annotation.RequestIgnore;
import com.xiaomayi.security.utils.SecurityUtils;
import com.xiaomayi.system.dto.user.UserAddDTO;
import com.xiaomayi.system.dto.user.UserUpdateDTO;
import com.xiaomayi.system.entity.User;
import com.xiaomayi.system.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 测试案例 前端控制器
* 特别备注:此文件非项目本身有效文件,仅仅是工程师编写测试案例使用,留个备份未删除
* 实际项目使用时请删除此文件,以免造成其他影响
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@RequestIgnore
@RestController
@RequestMapping("/test")
@AllArgsConstructor
public class TestController {
/**
* 幂等性验证
*
* @return 返回结果
*/
@RepeatSubmit(interval = 1, timeUnit = TimeUnit.SECONDS)
@PostMapping("/idem")
public R idem() {
return R.ok();
}
}
总结
集成防止重复提交功能的意义在于:
避免重复操作,确保数据一致性。
提升系统性能和安全性。
改善用户体验,减少误操作。
通过合理的防重机制(如 幂等性设计
等),可以有效解决重复提交问题。