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();
    }
相关推荐
陈平安Java and C1 小时前
MyBatisPlus
java
秋野酱1 小时前
如何在 Spring Boot 中实现自定义属性
java·数据库·spring boot
安的列斯凯奇2 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
Bunny02122 小时前
SpringMVC笔记
java·redis·笔记
架构文摘JGWZ2 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC2 小时前
Swift语言的网络编程
开发语言·后端·golang
feng_blog66882 小时前
【docker-1】快速入门docker
java·docker·eureka
邓熙榆2 小时前
Haskell语言的正则表达式
开发语言·后端·golang
枫叶落雨2224 小时前
04JavaWeb——Maven-SpringBootWeb入门
java·maven
m0_748232394 小时前
SpringMVC新版本踩坑[已解决]
java