背景
前后端分离的项目会经常产生重复提交的数据,前端做了各种防重提交的方法,比如提交后置灰,但偶尔还是会有重复的数据。这里提供一个后端防重复提交的方案。
主要思路
对接口入参进行校验,如果单位时间内提交的参数一样,就表示是重复提交,后面的请求直接返回
具体实现
1、新建一个注解
作用的标识接口需要防重复提交。
参数second,禁止时长,表示该时间内禁止再提交,默认是1秒,可以针对接口自定义。
参数value是对防重接口起名,需要至少在类中是唯一的。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Resubmit {
/**
* 全局唯一的名称
* @return 名称
*/
String value() default "";
/**
* 禁止提交时长 单位秒
* @return 时长
*/
int second() default 1;
}
2、防重主要实现
主要使用了spring aop和分布式锁来实现防重校验,分布式锁不在本次的讨论范围内,具体实现可以根据各自的方式,本人是使用redis来实现的。
private static final String PRIVATE_KEY = "resubmit";
@Autowired
private LockUtil lockUtil;
@Pointcut("@annotation(com.ydj.annotation.Resubmit)")
public void cutService(){
}
@Around("cutService()")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable{
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
String className = joinPoint.getTarget().getClass().getName();
Resubmit resubmit = method.getAnnotation(Resubmit.class);
//获取接口入参
Object requestBodyArg = getRequestBodyParam(method.getParameterAnnotations(), joinPoint.getArgs());
if (null == requestBodyArg) {
//如果没有入参有 requestBody注解的, 暂不支持重复提交校验
return joinPoint.proceed(joinPoint.getArgs());
}
//禁止重复提交时间
int second = resubmit.second();
//获取分布式锁key
String key = getKey(className, methodName, resubmit.value(), requestBodyArg);
if (!lockUtil.tryGetDistributedLock(key, UUID.randomUUID().toString(), second)) {
throw new BizException("禁止重复提交");
}
return joinPoint.proceed(joinPoint.getArgs());
}
3、获取接口参数
我们系统中的提交接口一般使用的是post application/json方式,如果有其它方式这边需要进行修改
/**
* 找到RequestBody的参数
* @param annotations 方法参数注解
* @param arguments 参数
* @return 带有RequestBody的参数
*/
private Object getRequestBodyParam(Annotation[][] annotations , Object[] arguments){
for (int i = 0; i < annotations.length; i++) {
Annotation[] argAnnotations = annotations[i];
for (Annotation argAnnotation : argAnnotations) {
if (argAnnotation instanceof RequestBody) {
return arguments[i];
}
}
}
return null;
}
4、获取分布式锁的key
入参按key1=value1&key2=value2这样的形式,并且key是自然排序的。
urlParams 拼上resubmitValue是为了防止不同的接口参数是一样的,再在前面拼上className和methodName是做进一步保证不会串到其它接口
最后再对urlParams进行base64编码用作分布式锁的key。
/**
* 获取分布式锁的key
* @param className 类名
* @param methodName 方法名
* @param resubmitValue 防重提交value
* @param requestBodyArg 参数
* @return key
*/
private String getKey(String className, String methodName, String resubmitValue, Object requestBodyArg){
Map<String, Object> paramMap = JSON.parseObject(JSON.toJSONString(requestBodyArg), new TypeReference<Map<String, Object>>(){});
List<String> paramList=new ArrayList<>(paramMap.size());
for (Map.Entry<String, Object> entry:paramMap.entrySet()){
if (null == entry.getValue() || StringUtils.isBlank(String.valueOf(entry.getValue()))){
continue;
}
paramList.add(entry.getKey()+"="+entry.getValue());
}
Collections.sort(paramList);
String urlParams = className + ":" + methodName + ":" + resubmitValue + ":" + String.join("&", paramList);
try {
return RSAUtils.sign(urlParams, PRIVATE_KEY, "UTF_8");
} catch (CodecSignException e) {
return urlParams;
}
}
5、用法
用法很简单了,只需要在防重提交的接口上加上@Resubmit注解就行
@Resubmit("testResubmit")
@RequestMapping(value = "/testResubmit", method = RequestMethod.POST)
public String testResubmit(@RequestBody ResubmitReq resubmitReq){
return JSONObject.toJSONString(resubmitReq);
}
总结
以上代码已经经过本人测试过,如有问题可以联系我改正,谢谢。