【工作笔记】防止表单重复提交

作用:防止因网络、多次点击等问题造成的表单重复提交等问题,保持幂等性
实现方式

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);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值