总体思路:AOP拦截特定的注解,根据参数以及请求方法生成特定的md5字符串,在固定时间内存在即判断为重复请求,返回前台错误信息
AOP代码
获取参数注解要注意下
package com.marry.simplespringboot.aspect;
import com.marry.simplespringboot.annotation.ExcludeRepeat;
import com.marry.simplespringboot.annotation.IncludeRepeat;
import com.marry.simplespringboot.annotation.RepeatSubmit;
import com.marry.simplespringboot.exception.RepeatSubmitException;
import com.marry.simplespringboot.util.JacksonUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author mal
* @date 2022-07-26 15:17
*/
@Aspect
@Component
public class RepeatSubmitAspect {
private static final Logger logger = LoggerFactory.getLogger(RepeatSubmitAspect.class);
@Autowired
StringRedisTemplate stringRedisTemplate;
@Before("@annotation(rs)")
public void checkReSubmit(JoinPoint joinPoint, RepeatSubmit rs) {
try {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
int interval = annotation.interval();
String message = annotation.message();
boolean onlyInclude = annotation.onlyInclude();
Object[] args = joinPoint.getArgs();
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
Parameter[] parameters = method.getParameters();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
List<Object> candidateArgList = new ArrayList<>();
candidateArgList.add(getMethodUrl(method));
if (args != null) {
for (int i = 0; i < parameters.length; i++) {
String parameterName = parameterNames[i];
Object arg = args[i];
Annotation[] annotations = parameterAnnotations[i];
if (onlyInclude) {
final IncludeRepeat includeRepeat =
getAnnotationByType(annotations, IncludeRepeat.class);
if (includeRepeat == null) {
continue;
}
} else {
final ExcludeRepeat excludeRepeat =
getAnnotationByType(annotations, ExcludeRepeat.class);
if (excludeRepeat != null || ignoreArg(arg, parameterName)) {
continue;
}
}
candidateArgList.add(arg);
}
}
String lockKey = DigestUtils.md5Hex(JacksonUtil.writeObj2Json(candidateArgList));
//boolean flag = memcachedClient.add("REPEAT_SUBMIT/" + lockKey, interval, System.currentTimeMillis());
//if (!flag) {
// throw new RepeatSubmitException(message);
//}
boolean lock = RepeatSubmitLock.getInstance().lock(lockKey, System.currentTimeMillis(), interval);
if (!lock) {
throw new RepeatSubmitException(message);
}
//boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("repeat_submit:" + lockKey, String.valueOf(System.currentTimeMillis()), interval, TimeUnit.MILLISECONDS);
//if (!lock) {
// throw new RepeatSubmitException(message);
//}
} catch (RepeatSubmitException e) {
throw e;
} catch (Exception e) {
logger.error("checkReSubmit,error", e);
}
}
private static <T extends Annotation> T getAnnotationByType(final Annotation[] annotations,
final Class<T> clazz) {
T result = null;
for (final Annotation annotation : annotations) {
if (clazz.isAssignableFrom(annotation.getClass())) {
result = (T) annotation;
break;
}
}
return result;
}
private String getMethodUrl(Method method) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
if (request != null) {
return request.getRequestURI();
}
}
return method.getDeclaringClass().getName() + "_" + method.getName();
}
private boolean ignoreArg(Object param, String parameterName) {
if (param instanceof HttpServletRequest
|| param instanceof HttpServletResponse
|| param instanceof MultipartFile
|| param instanceof MultipartFile[]
|| param instanceof ModelMap
|| param instanceof Model
|| param instanceof byte[]) {
return true;
}
if (parameterName.contains("base64") || parameterName.contains("Base64") || parameterName.contains("agreement")) {
return true;
}
return false;
}
}
本来系统中是用的
memcache,它的add方法也类似redis.setnx方法了,但是大家现在用redis的比较多,可以把缓存放到redis中,测试时就直接放map中。
记录请求的MAP
用
concurrentHashMap存储生成的key, 用延时任务线程池来指定的时间内删除存储,达到过期效果
package com.marry.simplespringboot.aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author mal
* @date 2022-07-26 16:19
*/
public class RepeatSubmitLock {
private static final Logger logger = LoggerFactory.getLogger(RepeatSubmitLock.class);
private final ConcurrentHashMap<String, Object> LOCK_CACHEMAP = new ConcurrentHashMap<>(100);
private final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.CallerRunsPolicy());
private RepeatSubmitLock() {
}
private static class SingletonInstance {
private static final RepeatSubmitLock INSTANCE = new RepeatSubmitLock();
}
public static RepeatSubmitLock getInstance() {
return SingletonInstance.INSTANCE;
}
public boolean lock(final String key, Object value, int intervals) {
Object absent = LOCK_CACHEMAP.putIfAbsent(key, value);
if (Objects.isNull(absent)) {
logger.info("lock,set value for key 【{}】",key);
EXECUTOR.schedule(() -> {
logger.info("lock,clear value for key 【{}】", key);
LOCK_CACHEMAP.remove(key);
}
, intervals, TimeUnit.MILLISECONDS);
return true;
} else {
if ((((Long) value) - ((Long) absent))> intervals) {
LOCK_CACHEMAP.put(key, value);
EXECUTOR.schedule(() -> LOCK_CACHEMAP.remove(key), intervals, TimeUnit.MILLISECONDS);
return true;
}
}
return false;
}
}
注解内容
三个注解的内容很简单
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 判断是重复请求的间隔时间
* 单位 秒
* @return
*/
int interval() default 10000;
String message() default "请勿重复提交!";
/**
* 是否只判断包含@InCludeRepeat注解的数据
* @return
*/
boolean onlyInclude() default false;
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IncludeRepeat {
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcludeRepeat {
}
简单的请求
@RestController
@RequestMapping("/repeat")
public class RepeatController {
@RepeatSubmit
@RequestMapping("/sayHello")
public String sayHello(String name) {
return "hello" + name;
}
日志
可以看出来时间还是听准确的
2022-07-28 16:36:03.509 :lock,set value for key 【3832e882eb227e3c5de35a569304287a】
2022-07-28 16:36:13.525 : lock,clear value for key 【3832e882eb227e3c5de35a569304287a】
2022-07-28 16:36:13.558 : lock,set value for key 【3832e882eb227e3c5de35a569304287a】
2022-07-28 16:36:23.561 : lock,clear value for key 【3832e882eb227e3c5de35a569304287a】
总结
有条件的话还是建议使用redis来做,性能好,也省得占用应用服务器的资源