核心逻辑阐述:
1.定义注解 NoRepeatSubmit,包含可重复提交时间间隔;使用时标记在controller需要防重复提交的类或者方法上;
2.定义AOP切面NoRepeatSubmitAspect切NoRepeatSubmit注解;
3.对请求方法query参数和body参数分别做MD5,作为redis中的key;
4.如果在redis中找到了就算重复,抛出异常;
5.如果未重复,放到redis中并设置下次可提交的时间;
注意:
为了防止并发出现问题,判断redis是否重复时需要加锁;
改进:
参加计算重复的参数可选;
将query和body做到一起;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface NoRepeatSubmit {
/**
* 默认限制重复提交时间单位
*/
TimeUnit lockTimeUnit() default TimeUnit.SECONDS;
/**
* 默认限制重复提交时间
*/
long lockTime() default 30;
}
@Aspect
@Component
@Order
public class NoRepeatSubmitAspect {
@Resource
private RedisComponent redisComponent;
@Pointcut("@annotation(com.guazi.opl.dealer.tool.application.aop.NoRepeatSubmit)")
public void noRepeatSubmit() {
}
@Around(value = "noRepeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) attributes;
if (Objects.isNull(sra)) {
return joinPoint.proceed();
}
NoRepeatSubmit noRepeatSubmit = this.getAnnotation(joinPoint);
if (Objects.isNull(noRepeatSubmit)) {
return joinPoint.proceed();
}
HttpServletRequest request = sra.getRequest();
String queryString = request.getQueryString();
//计算query参数的cache key
String queryKey = this.calcQueryKey(queryString);
String noRepeatQueryKey = this.getNoRepeatQueryKey(queryKey, request);
//此args中不止含有body,如果还有requestParam、multipartfile指定的参数
Object[] args = joinPoint.getArgs();
String paramKey = this.calcParamKey(args);
String noRepeatParamKey = this.getNoRepeatParamKey(paramKey, request);
synchronized (this) {
//判断queryKey是否在redis中存在,如果存在说明重复
boolean queryRepeatFlag = this.judgeAndSetRepeat(noRepeatQueryKey, noRepeatSubmit);
//先判断paramKey是否在redis中存在,如果存在说明已经重复
boolean paramRepeatFlag = this.judgeAndSetRepeat(noRepeatParamKey, noRepeatSubmit);
if (queryRepeatFlag && paramRepeatFlag) {
throw new BusinessException(-1, "正在处理,请勿重复操作");
}
}
return joinPoint.proceed();
}
/**
* 获取防重复提交注解
*/
private NoRepeatSubmit getAnnotation(ProceedingJoinPoint joinPoint) {
//先从方法上获取
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
//获取不到再到类上找
if (Objects.isNull(annotation)) {
annotation = joinPoint.getTarget().getClass().getAnnotation(NoRepeatSubmit.class);
}
return annotation;
}
/**
* 计算queryString对应的Md5值为value
*/
private String calcQueryKey(String queryString) throws Exception {
if (StringUtils.isBlank(queryString)) {
return StringUtils.EMPTY;
}
return SignatureUtils.md5(queryString).substring(5, 15);
}
/**
* 计算queryString对应的Md5最为value
*/
private String calcParamKey(Object[] args) throws Exception {
TreeSet<String> paramTree = new TreeSet<>();
for (Object arg : args) {
if (arg instanceof BindingResult || arg instanceof HttpServletResponse || arg instanceof HttpServletRequest) {
//requestURL参数,这直接返回
continue;
}
//转成json字符串
String jsonArg = JsonUtils.toJsonString(arg);
paramTree.add(jsonArg);
}
if (paramTree.isEmpty()) {
return StringUtils.EMPTY;
}
//获取有序body json后的参数
List<String> paramList = new ArrayList<>(paramTree);
//连在一起
String bodyJson = String.join("", paramList);
return SignatureUtils.md5(bodyJson).substring(5, 15);
}
/**
* 获取防重复提交query的key
*/
private String getNoRepeatQueryKey(String queryKey, HttpServletRequest request) {
if (StringUtils.isBlank(queryKey)) {
return StringUtils.EMPTY;
}
String noRepeatSubmitPrefix = RedisKeyConstant.NO_REPEAT_QUERY_SUBMIT_PREFIX + request.getRequestURL();
return noRepeatSubmitPrefix + queryKey;
}
/**
* 获取防重复提交body的key
*/
private String getNoRepeatParamKey(String paramKey, HttpServletRequest request) {
if (StringUtils.isBlank(paramKey)) {
return StringUtils.EMPTY;
}
String noRepeatSubmitPrefix = RedisKeyConstant.NO_REPEAT_QUERY_SUBMIT_PREFIX + request.getRequestURL();
return noRepeatSubmitPrefix + paramKey;
}
/**
* 如果不重复 则设置下次可提交时间 返回false
* 否则返回true
*/
private boolean judgeAndSetRepeat(String cacheKey, NoRepeatSubmit noRepeatSubmit) {
if (StringUtils.isBlank(cacheKey)) {
return true;
}
String value = redisComponent.getString(cacheKey);
if (Objects.isNull(value)) {
redisComponent.setString(cacheKey, cacheKey, noRepeatSubmit.lockTime(), noRepeatSubmit.lockTimeUnit());
return false;
}
return true;
}
}