作用:防止因网络、多次点击等问题造成的表单重复提交等问题,保持幂等性
实现方式
1使用map或者缓存,提交时首先去查,如果存在了,就说明提交过了,报异常,否则正常提交
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.common.base.Preconditions;
import java.util.HashMap;
import java.util.Map;
/**
* 普通 Map 版本
*/
@RestController
public class TestController {
// 缓存 ID 集合
private Map<String, Integer> cache = new HashMap<>();
@RequestMapping("/test")
public String test(String id) {
// 非空判断
Preconditions.checkNotNull(id,"id is null");
synchronized (this.getClass()) {
// 重复请求判断
if (cache.containsKey(id)) {
// 重复请求
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
// 存储请求 ID
//value其实没什么用,主要是判断是否存在key的,所以使用ArrayList理论上也可以
cache.put(id, 1);
}
// 业务代码...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
使用map的缺点是一直在往map里存值,没有回收机制,会占用越来越多的内存,查询速度也会降低
2 使用LRUMap 代替HashMap
LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的数据淘汰算法,选择最近最久未使用的数据予以淘汰。。LRUMap是基于这种算法的一种map结构,保证数据量不会越来越大
@RestController
public class TestController {
// 最大容量 100 个,根据 LRU 算法淘汰数据的 Map 集合
private LRUMap<String, Integer> reqCache = new LRUMap<>(100);
@RequestMapping("/test")
public String test(String id) {
// 非空判断
Preconditions.checkNotNull(id,"id is null");
synchronized (this.getClass()) {
// 重复请求判断
if (reqCache.containsKey(id)) {
// 重复请求
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
// 存储请求 ID
reqCache.put(id, 1);
}
// 业务代码...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
3 使用第三方缓存,如Caffeine,redis等
1.引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
2.具体使用
@RestController
public class TestController {
//10分钟后失效,对于表单来说足以
Cache<Integer, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
@RequestMapping("/test")
public String test(User user) {
//hashCode作为判重标识
Integer userHash = user.hashCode();
synchronized (this.getClass()) {
// 把参数的hash值作为键
Integer hashCode = user.hashCode();
Object obj = cache.getIfPresent(hashCode);
if (!ObjectUtils.isEmpty(obj)) {
return;
} else {
cache.get(hashCode, new Function<Integer, Object>() {
@Override
public Object apply(Integer integer) {
return hashCode;
}
});
}
}
// 业务代码...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
4.提取公共方法供项目使用
/**
* 防止重复提交工具类
*/
public class IdempotentUtils {
// 根据 LRU(Least Recently Used,最近最少使用)算法淘汰数据的 Map 集合,最大容量 100 个
private static LRUMap<String, Integer> reqCache = new LRUMap<>(100);
/**
* 幂等性判断
* @return
*/
public static boolean judge(String id, Object lockClass) {
synchronized (lockClass) {
// 重复请求判断
if (reqCache.containsKey(id)) {
// 重复请求
System.out.println("请勿重复提交!!!" + id);
return false;
}
// 非重复请求,存储请求 ID
reqCache.put(id, 1);
}
return true;
}
}
运用
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/add")
public String addUser(String id) {
// 非空判断(忽略)...
// -------------- 幂等性调用(开始) --------------
if (!IdempotentUtils.judge(id, this.getClass())) {
//或者抛异常
return "执行失败";
}
// -------------- 幂等性调用(结束) --------------
// 业务代码...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
5 自定义注解进行防重操作
5.1 首先自定义一个注解
NoRepeatSubmit .java
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 NoRepeatSubmit {
NoRepeatSubmitType value() default NoRepeatSubmitType.FORM;
}
5.2 枚举类型
NoRepeatSubmitType.java
/**
* @Desc:
* @Author: wangyafei
* @Date: 2020/8/21 17:38
* @Version 1.0
*/
public enum NoRepeatSubmitType {
/**
* 表单数据
*/
FORM,
/**
* sessionId + 请求URL
*/
URL
}
5.3 AOP解析这个注解
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
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 java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @Desc:
* @Author: wangyafei
* @Date: 2020/8/21 16:59
* @Version 1.0
*/
@Component
@Aspect
@Slf4j
public class NoRepeatSubmitAop {
private Cache<String, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(15, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
@Around("execution(* com..*Controller.*(..)) && @annotation(nrs)")
public Object aroundMethod(ProceedingJoinPoint pjp, NoRepeatSubmit nrs) {
try {
if(nrs.value().equals(NoRepeatSubmitType.URL)) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
HttpServletRequest request = attributes.getRequest();
String key = sessionId + "-" + request.getServletPath();
// 如果缓存中有这个url视为重复提交
return getObject(pjp, key);
}else if(nrs.value().equals(NoRepeatSubmitType.FORM)){
Object firstArg = pjp.getArgs()[0];
String hashCode = firstArg.hashCode() + "";
// 如果缓存中有这个url视为重复提交
return getObject(pjp, hashCode);
}else {
return pjp.proceed();
}
} catch (Throwable e) {
e.printStackTrace();
log.error("验证重复提交时出现未知异常!");
return "";
}
}
private Object getObject(ProceedingJoinPoint pjp, String key) throws Throwable {
if (cache.getIfPresent(key) == null) {
Object o = pjp.proceed();
cache.put(key, 0);
return o;
} else {
Object[] args = pjp.getArgs();
log.error("重复提交" + Arrays.toString(args));
return null;
}
}
}
5.4 应用
@RequestMapping("/public")
@RestController
public class TestController {
@RequestMapping("/test")
@NoRepeatSubmit
public String test(String s) {
return ("success" + s);
}
}