1. 主要使用场景:
数据比对,本系统数据库内部数据和第三方数据比对是否一致,差别。
因为需要时间比较长,避免用户短时间重复点击问题
aop 通过注解的方式,根据请求地址和参数(md5),使用redis单线程缓存机制,设定过期时间
2. 代码案例
AvoidRepeatSubmit:注解说明,默认5秒
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 防止重复提交注解
* @author fanchenbin
* @since 2022年06月01日18:04:29
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AvoidRepeatSubmit {
int lockTime() default 5;
}
RepeatSubmitAspect: 实现类 考虑到参数的各种传参方式,目前只取了传参值加密,没有实现过滤指定参数的方式
如果要实现过滤指定参数的方式,需要格式传参方式单独处理,无法兼容
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.tianque.collaboration.annotation.AvoidRepeatSubmit;
import com.tianque.collaboration.common.cache.CacheConstant;
import com.tianque.doraemon.core.tool.api.Result;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import java.io.BufferedReader;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Log4j2
public class RepeatSubmitAspect {
private final StringRedisTemplate cacheService;
public RepeatSubmitAspect(StringRedisTemplate cacheService) {
this.cacheService = cacheService;
}
@Pointcut("@annotation(avoidRepeatSubmit)")
public void pointCut(AvoidRepeatSubmit avoidRepeatSubmit) {
}
@Around(value = "pointCut(avoidRepeatSubmit)", argNames = "joinpoint,avoidRepeatSubmit")
public Object around(ProceedingJoinPoint joinpoint, AvoidRepeatSubmit avoidRepeatSubmit) throws Throwable {
//锁接口的时长
int lockSeconds = avoidRepeatSubmit.lockTime();
MethodSignature methodSignature = (MethodSignature) joinpoint.getSignature();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// Map<String, Object> dataMaps = commonRequestParamConvert(request);
// System.out.println(JSON.toJSONString(dataMaps));
// 获取参数名称
String methodsName = methodSignature.getMethod().getName();
// String[] params = methodSignature.getParameterNames();
//获取参数值
Object[] args = joinpoint.getArgs();
StringBuilder content = new StringBuilder();
if (args != null && args.length > 0) {
for (Object obj : args) {
content.append(obj.toString());
}
}
//组合成Redis的KEY
boolean flag = checkRequest(methodsName, lockSeconds, content.toString());
//如果缓存中有这个URL,视为重复提交
if (flag) {
Object o = joinpoint.proceed();
return o;
} else {
//存在重复请求,直接返回错误
return Result.fail("系统资源加载中,请勿重复点击!");
}
}
private Map<String, Object> commonRequestParamConvert(HttpServletRequest request) {
Map<String, Object> params = new HashMap<>();
try {
Map<String, String[]> requestParams = request.getParameterMap();
if (requestParams != null && !requestParams.isEmpty()) {
requestParams.forEach((key, value) -> params.put(key, value[0]));
} else {
StringBuilder paramSb = new StringBuilder();
try {
String str = "";
BufferedReader br = request.getReader();
while ((str = br.readLine()) != null) {
paramSb.append(str);
}
} catch (Exception e) {
System.out.println("httpServletRequest get requestbody error, cause : " + e);
}
if (paramSb.length() > 0) {
JSONObject paramJsonObject = JSON.parseObject(paramSb.toString());
if (paramJsonObject != null && !paramJsonObject.isEmpty()) {
paramJsonObject.forEach((key, value) -> params.put(key, value));
}
}
}
} catch (Exception e) {
System.out.println("commonRequestParamConvert error, cause : " + e);
}
return params;
}
public boolean checkRequest(String methodsName, int lockSeconds, String reqJsonParam) {
String dedupMD5 = jdkMD5(reqJsonParam);
String key = CacheConstant.KEY_METHOD + methodsName + ":" + dedupMD5;
// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了
if (!cacheService.hasKey(key)) {
cacheService.opsForValue().set(key, String.valueOf(0), lockSeconds, TimeUnit.SECONDS);
return true;
} else {
return false;
}
}
public boolean checkRequest(String methodsName, int lockSeconds, String reqJsonParam, String... excludeKeys) {
String dedupMD5 = dedupParamMD5(reqJsonParam, excludeKeys);
String key = CacheConstant.KEY_METHOD + methodsName + ":" + dedupMD5;
// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了
if (!cacheService.hasKey(key)) {
cacheService.opsForValue().set(key, String.valueOf(0), lockSeconds, TimeUnit.SECONDS);
return true;
} else {
return false;
}
}
public String dedupParamMD5(String reqJSON, String... excludeKeys) {
TreeMap paramTreeMap = JSON.parseObject(reqJSON, TreeMap.class);
if (excludeKeys != null) {
List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
if (!dedupExcludeKeys.isEmpty()) {
for (String dedupExcludeKey : dedupExcludeKeys) {
paramTreeMap.remove(dedupExcludeKey);
}
}
}
String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
String md5deDupParam = jdkMD5(paramTreeMapJSON);
log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
return md5deDupParam;
}
private static String jdkMD5(String src) {
String res = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] mdBytes = messageDigest.digest(src.getBytes());
res = DatatypeConverter.printHexBinary(mdBytes);
} catch (Exception e) {
log.error("", e);
}
return res;
}
}
TestRequest:测试类
import com.alibaba.fastjson.JSON;
import com.tianque.collaboration.annotation.AvoidRepeatSubmit;
import com.tianque.collaboration.domain.TaskFileLog;
import com.tianque.doraemon.core.tool.api.Result;
import com.tianque.doraemon.mybatis.support.PageParam;
import io.swagger.annotations.Api;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.*;
/**
* @Description
* @Author YuanWeiMin
* @Date 2022-10-19 10:24
*/
@Log4j2
@Api(tags = "验证接口重复调用【袁伟民】")
@RestController
@RequestMapping("/testRequest")
public class TestRequest {
@AvoidRepeatSubmit
@ResponseBody
@RequestMapping(value = "/testJson", method = RequestMethod.POST)
public Result taskPassToOtherCountry(@RequestBody TaskFileLog dataMap) throws Exception {
String result = JSON.toJSONString(dataMap);
return Result.data(result);
}
@AvoidRepeatSubmit
@ResponseBody
@RequestMapping(value = "/testParam", method = RequestMethod.POST)
public Result taskPassToOtherCountry(@RequestParam("result") String result) throws Exception {
return Result.data(result);
}
@AvoidRepeatSubmit
@ResponseBody
@RequestMapping(value = "/testBody", method = RequestMethod.POST)
public Result taskPassToOtherCountry(TaskFileLog taskFileLog, PageParam pageParam) throws Exception {
return Result.data("ok");
}
}