简介
在我们日常的开发过程中,表单数据的提交前后端都需要做数据防重复。
本篇文章主要以Java后端基于自定义注解的的形式并参考美团GTIS防重系统实现的。
具体实现逻辑如下:
自定义防重注解
RepeatSubmit.java
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
int interval() default 5000;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 提示消息 支持国际化 格式为 {code}
*/
String message() default "{repeat.submit.message}";
}
定义切面类
RepeatSubmitAspect.java
@Slf4j
@RequiredArgsConstructor
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private RedissonClient redissonClient;
private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();
private static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) {
// 如果注解不为0 则使用注解数值
long interval = 0;
if (repeatSubmit.interval() > 0) {
interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
}
if (interval < 1000) {
throw new RuntimeException("重复提交间隔时间不能小于'1'秒");
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String nowParams = argsArrayToString(point.getArgs());
// 唯一值(没有消息头则使用请求地址)
String uri = request.getRequestURI();
String submitKey = request.getHeader("token");
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = REPEAT_SUBMIT_KEY + uri + submitKey;
RBucket<Object> bucket = redissonClient.getBucket(cacheRepeatKey);
if (bucket.setIfAbsent("", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
throw new RuntimeException(message);
}
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof Result) {
try {
Result<?> r = (Result<?>) jsonResult;
// 成功则不删除redis数据 保证在有效时间内无法重复提交
if (r.getCode() == 200) {
return;
}
String key = KEY_CACHE.get();
RBucket<Object> bucket = redissonClient.getBucket(key);
bucket.delete();
log.error("处理之后,删除完成......");
} finally {
KEY_CACHE.remove();
}
}
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
log.error("发生异常了。。。。。。");
String key = KEY_CACHE.get();
RBucket<Object> bucket = redissonClient.getBucket(key);
bucket.delete();
KEY_CACHE.remove();
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
try {
params.append(JSONUtil.toJsonStr(o)).append(" ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return params.toString().trim();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
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;
}
}
测试
新增下面测试接口,模拟表单提交操作。
RepeatSubmitController.java
@RestController
@Slf4j
@RequestMapping("/repeat")
public class RepeatSubmitController {
@PostMapping("submit")
@RepeatSubmit(message = "不可重复提交,请稍后再试!", interval = 6000)
public Result<Object> testSubmit(@RequestBody UserEntity userEntity) {
log.error("开始处理业务:userEntity = {}", userEntity);
try {
// 模拟业务处理3s
Thread.sleep(3000);
} catch (InterruptedException ex) {
}
log.error("业务处理完成......");
return Result.success("提交成功", userEntity);
}
}
下面使用ApiPost接口测试工具,新增两个接口测试,模拟重复提交过程。
启动项目,依次访问以上测试接口:测试接口(一)
、测试接口(二)
可以看到:
-
测试接口(二)访问失败,并抛出异常——
不可重复提交,请稍后再试!
-
接口一请求成功,返回数据。
{
"code": 200,
"msg": "提交成功",
"data": {
"userId": 1234,
"userName": "公众号:小小开发者"
}
}
源码获取: https://gitee.com/xxkfz/xxkfz-admin-redisson