SpringBoot表单防止重复提交

哪些因素会引起重复提交?
开发的项目中可能会出现下面这些情况:

前端下单按钮重复点击导致订单创建多次

网速等原因造成页面卡顿,用户重复刷新提交请求

黑客或恶意用户使用postman等http工具重复恶意提交表单

重复提交会带来哪些问题?

重复提交带来的问题

会导致表单重复提交,造成数据重复或者错乱

核心接口的请求增加,消耗服务器负载,严重甚至会造成服务器宕机

订单的防重复提交你能想到几种方案?
核心接口需要做防重提交,你应该可以想到以下几种方案:

方式一:前端JS控制点击次数,屏蔽点击按钮无法点击 前端可以被绕过,前端有限制,后端也需要有限制

方式二:数据库或者其他存储增加唯一索引约束 需要想出满足业务需求的唯一索引约束,比如注册的手机号唯一。但是有些业务是没有唯一性限制的,且重复提交也会导致数据错乱,比如你在电商平台可以买一部手机,也可以买两部手机

方式三:服务端token令牌方式 下单前先获取令牌-存储redis,下单时一并把token提交并检验和删除-lua脚本

分布式情况下,采用Lua脚本进行操作(保障原子性)

其中方式三 是大家采用的最多的,那有没更加优雅的方式呢?

假如系统中不止一个地方,需要用到这种防重复提交,每一次都要写这种lua脚本,代码耦合性太强,这种又不属于业务逻辑,所以不推荐耦合进service中,可读性较低。

本文采用自定义注解+AOP的方式,优雅的实现防止重复提交功能。

1、注解
java 复制代码
/**
 * 表单防止重复提交自定义注解
 */
@Documented
@Target(ElementType.METHOD) //可以用在方法上
@Retention(RetentionPolicy.RUNTIME) //保留到虚拟机上,可通过反射获取
public @interface RepeatSubmit {
    /**
     * 加锁过期时间,默认是5秒
     * @return
     */
    int lockTime() default 5;

}
注解解析:
@Documented 将此注解包含在 javadoc 中

@Inherited 是否允许子类继承父类中的注解

@interface 用来声明一个注解,可以通过default来声明参数的默认值

自定义注解时,自动继承了java.lang.annotation.Annotation接口,可以通过反射可以获取自定义注解

@Retention 表示在什么级别保存该注解信息
  • RetentionPolicy.SOURCE 保留到源码上
  • RetentionPolicy.CLASS 保留到字节码上
  • RetentionPolicy.RUNTIME 保留到虚拟机运行时(最多,可通过反射获取)
@Target 表示该注解用于什么地方

ElementType.CONSTRUCTOR 用在构造器

ElementType.FIELD 用于描述域-属性上

ElementType.METHOD 用在方法上

ElementType.TYPE 用在类或接口上

ElementType.PACKAGE 用于描述包

2、aop
java 复制代码
/**
 * 表单防止重复提交切面
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    private Lock lock = new ReentrantLock();

    @Resource
    private RedisCache redisCache;

    /**
     * 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
     * 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
     * 方式一:@annotation:当执行的方法上拥有指定的注解时生效
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }


    /**
     * 环绕通知, 围绕着方法执行
     * @param joinPoint
     * @param repeatSubmit
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * 用@Pointcut和@Around联合注解也可以
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        // 获取用户的token验证,这里项目用的是 header 里的  Authorization 参数
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String requestToken = request.getHeader("Authorization");

        log.info("请求的token->{}", requestToken);

        boolean res = true;
        //从redis中获取token
        Object token = redisCache.getCacheObject("submit:token"+requestToken);
        if(token == null){
            //设置token并且设置有效期
            redisCache.setCacheObject("submit:token"+requestToken,requestToken,repeatSubmit.lockTime(), TimeUnit.SECONDS);
            res = false;
        }

        //用于记录成功或者失败
        log.info("环绕通知中");
        log.info("默认加锁时间->{}秒", repeatSubmit.lockTime());

        Object obj  = "";
        try {
            if (lock.tryLock(repeatSubmit.lockTime(), TimeUnit.SECONDS)) { //设置尝试获取锁得时间
                //防重提交
                if (res) {
                    log.error("请求重复提交");
                    log.info("环绕通知中");
                    throw new ServiceException("请勿重复提交");
                }
                log.info("环绕通知执行前");
                obj = joinPoint.proceed();
                log.info("环绕通知执行后");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock(); //释放锁
        }
        return obj;
    }
}
3、使用
java 复制代码
    /**
     * 同步菜谱
     * @param data
     * @return
     */
    @RepeatSubmit
    @PostMapping("/addRecipe")
    public AjaxResult addRecipe(@RequestBody String data){
        recipeService.addRecipe(data);
        return AjaxResult.success();
    }
相关推荐
颜淡慕潇13 分钟前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
ProtonBase19 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
乐之者v25 分钟前
leetCode43.字符串相乘
java·数据结构·算法
suweijie7684 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿4 小时前
List深拷贝后,数据还是被串改
java
向前看-8 小时前
验证码机制
前端·后端
xlsw_8 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹8 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭9 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫9 小时前
泛型(2)
java