ajax同步方式可以防重复提交吗 SpringBoot 项目中如何有效防止重复提交

在数字化业务领域,数据准确性与一致性面临重大挑战。特别是表单数据在短时间内频繁重复提交,这一问题尤为突出,有时甚至会导致数据混乱。那么,如何有效解决这一问题?以下将为大家提供一些实用方法。

了解重复提交的危害

在业务操作中,编辑表单数据尚可应对,可一旦涉及新增数据ajax同步方式可以防重复提交吗,短时间内频繁提交就会导致混乱。以电商平台用户注册为例,重复提交可能导致服务器端出现多个账号信息冗余。从技术层面来看,大量重复数据会导致数据管理和数据分析等多个环节出现问题。此外,用户体验也会受到影响,因为一条数据存在多个冗余记录,可能导致数据混乱,进而影响用户查询订单状态等功能的正常使用。

客户端限制操作的方法

package com.cube.share.resubmit.check.aspect;
import com.cube.share.resubmit.check.constants.Constant;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
 * @author cube.li
 * @date 2021/7/9 20:45
 * @description 防重复提交注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ResubmitCheck {
    /**
     * 参数Spring EL表达式例如 #{param.name},表达式的值作为防重复校验key的一部分
     */
    String[] argExpressions();
    /**
     * 重复提交报错信息
     */
    String message() default Constant.RESUBMIT_MSG;
    /**
     * Spring EL表达式,决定是否进行重复提交校验,多个条件之间为且的关系,默认是进行校验
     */
    String[] conditionExpressions() default {"true"};
    /**
     * 是否选用当前操作用户的信息作为防重复提交校验key的一部分
     */
    boolean withUserInfoInKey() default true;
    /**
     * 是否仅在当前session内进行防重复提交校验
     */
    boolean onlyInCurrentSession() default false;
    /**
     * 防重复提交校验的时间间隔
     */
    long interval() default 1;
    /**
     * 防重复提交校验的时间间隔的单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

我们常常会考虑在客户端设置限制,比如对提交按钮进行一些操作。具体来说,就是在点击请求未得到反馈时,将提交按钮设置为不能重复点击。然而,这种方法有缺陷。比如,开发者在调试接口时,可能会用Postman或Curl这样的工具来测试,这时候客户端的限制就失去了作用,仍然可能发生重复提交的问题。

图片[1]-ajax同步方式可以防重复提交吗 SpringBoot 项目中如何有效防止重复提交-唐朝资源网

服务端加锁接口串行化

服务端存在一种做法,就是通过加锁来让接口实现串行化,以此避免重复提交。这种方式能够有效地识别重复提交的数据。曾有一个项目采用了这种方法,但后来发现,虽然它能防止数据问题,但由于所有请求都必须排队依次处理,即使是少量并发请求也会导致接口的吞吐量降低。因此,在使用时必须合理控制锁的粒度。否则,整个系统的性能可能会大幅下降。特别是对于一些流量较大的新闻资讯平台,如果直接这样应用,可能会让用户面临加载缓慢等不良体验。

基于AOP实现防重复提交校验思路

AOP实现防重复提交校验的方法较为实用。我们可以通过拼接请求参数的指定方法,将其作为key存储到Redis中ajax同步方式可以防重复提交吗,并设置相应的过期时间。在请求接口时,我们会在Redis中根据这个key进行查找。如果找到,说明在短时间内已经存在相同请求参数的请求,这就意味着本次请求为重复提交。随后,系统将抛出错误信息,终止本次请求。这种做法在众多金融系统中得到应用,例如用户在进行转账操作时,就会利用这种重复提交校验来防止同一笔转账被多次请求,从而避免资金问题。

package com.cube.share.resubmit.check.aspect;
import com.cube.share.base.templates.CustomException;
import com.cube.share.base.utils.ExpressionUtils;
import com.cube.share.resubmit.check.constants.Constant;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
 * @author cube.li
 * @date 2021/7/9 22:17
 * @description 防重复提交切面
 */
@Component
@Aspect
@Order(-1)
@ConditionalOnProperty(name = "enabled", prefix = "resubmit-check", havingValue = "true", matchIfMissing = true)
public class ResubmitCheckAspect {
    private static final String REDIS_SEPARATOR = "::";
    private static final String RESUBMIT_CHECK_KEY_PREFIX = "resubmitCheckKey" + REDIS_SEPARATOR;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private HttpServletRequest request;
    @Before("@annotation(annotation)")
    public void resubmitCheck(JoinPoint joinPoint, ResubmitCheck annotation) throws Throwable {
        final Object[] args = joinPoint.getArgs();
        final String[] conditionExpressions = annotation.conditionExpressions();
        //根据条件判断是否需要进行防重复提交检查
        if (!ExpressionUtils.getConditionValue(args, conditionExpressions) || ArrayUtils.isEmpty(args)) {
            //return ((ProceedingJoinPoint) joinPoint).proceed();
        }
        doCheck(annotation, args);
        //return ((ProceedingJoinPoint) joinPoint).proceed();
    }
    /**
     * key的组成为: prefix::userInfo::sessionId::uri::method::(根据spring EL表达式对参数进行拼接)
     *
     * @param annotation 注解
     * @param args       方法入参
     */
    private void doCheck(@NonNull ResubmitCheck annotation, Object[] args) {
        final String[] argExpressions = annotation.argExpressions();
        final String message = annotation.message();
        final boolean withUserInfoInKey = annotation.withUserInfoInKey();
        final boolean onlyInCurrentSession = annotation.onlyInCurrentSession();
        String methodDesc = request.getMethod();
        String uri = request.getRequestURI();
        StringBuilder stringBuilder = new StringBuilder(64);
        Object[] argsForKey = ExpressionUtils.getExpressionValue(args, argExpressions);
        for (Object obj : argsForKey) {
            stringBuilder.append(obj.toString());
        }
        StringBuilder keyBuilder = new StringBuilder();
        //userInfo一般从token中获取,可以使用当前登录的用户id作为标识
        keyBuilder.append(RESUBMIT_CHECK_KEY_PREFIX)
                //userInfo一般从token中获取,可以使用当前登录的用户id作为标识
                .append(withUserInfoInKey ? "userId" + REDIS_SEPARATOR : "")
                .append(onlyInCurrentSession ? request.getSession().getId() + REDIS_SEPARATOR : "")
                .append(uri)
                .append(REDIS_SEPARATOR)
                .append(methodDesc).append(REDIS_SEPARATOR)
                .append(stringBuilder.toString());
        if (redisTemplate.opsForValue().get(keyBuilder.toString()) != null) {
            throw new CustomException(StringUtils.isBlank(message) ? Constant.RESUBMIT_MSG : message);
        }
        //值为空
        redisTemplate.opsForValue().set(keyBuilder.toString(), "", annotation.interval(), annotation.timeUnit());
    }
}

灵活控制校验的粒度

图片[2]-ajax同步方式可以防重复提交吗 SpringBoot 项目中如何有效防止重复提交-唐朝资源网

有几个参数能够调整锁的精细程度。比如,argExpressions可以将特定参数纳入防重复校验key的构成。以在线办公为例,若某部门提交文档,部门名称可以成为该参数的一部分。withUserInfoInKey参数用于决定是否将操作者信息包含在key中。以在线学习平台学生提交作业为例,将学生信息加入key中,可以实现精确验证。另外,onlyInCurrentSession参数在withUserInfoInKey设置为false时,允许在session级别对重复数据进行提交校验。

注解进行校验操作

实际上,我们可以在需要执行防重复提交校验的方法上添加一个名为ResubmitCheck的注解,并设定相关参数以完成校验任务。在这个过程中,使用注解属性argExpressions和conditionExpressions来指定SpringEL表达式,操作起来非常便捷。以物流订单处理系统为例,我们可以设置conditionExpressions为”[0].address!=null”,以此来实现更为灵活和精确的控制。当这个表达式的求值结果为false时,系统将不会执行防重复提交校验。反之,若不包含这一条件,那么在使用Postman进行自测时,一秒内多次发送请求就会触发“请不要重复提交数据”的错误提示。

package com.cube.share.base.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @author cube.li
 * @date 2021/7/9 21:00
 * @description Spring EL表达式工具类
 */
@SuppressWarnings("unused")
public class ExpressionUtils {
    private static final Map EXPRESSION_CACHE = new ConcurrentHashMap(64);
    /**
     * 获取Expression对象
     *
     * @param expressionString Spring EL 表达式字符串 例如 #{param.id}
     * @return Expression
     */
    @Nullable
    public static Expression getExpression(@Nullable String expressionString) {
        if (StringUtils.isBlank(expressionString)) {
            return null;
        }
        if (EXPRESSION_CACHE.containsKey(expressionString)) {
            return EXPRESSION_CACHE.get(expressionString);
        }
        Expression expression = new SpelExpressionParser().parseExpression(expressionString);
        EXPRESSION_CACHE.put(expressionString, expression);
        return expression;
    }
    /**
     * 根据Spring EL表达式字符串从根对象中求值
     *
     * @param root             根对象
     * @param expressionString Spring EL表达式
     * @param clazz            值得类型
     * @param               泛型
     * @return 值
     */
    @Nullable
    public static  T getExpressionValue(@Nullable Object root, @Nullable String expressionString, @NonNull Class clazz) {
        if (root == null) {
            return null;
        }
        Expression expression = getExpression(expressionString);
        if (expression == null) {
            return null;
        }
        return expression.getValue(root, clazz);
    }
    @Nullable
    public static  T getExpressionValue(@Nullable Object root, @Nullable String expressionString) {
        if (root == null) {
            return null;
        }
        Expression expression = getExpression(expressionString);
        if (expression == null) {
            return null;
        }
        //noinspection unchecked
        return (T) expression.getValue(root);
    }
    /**
     * 求值
     *
     * @param root              根对象
     * @param expressionStrings Spring EL表达式
     * @param                泛型 这里的泛型要慎用,大多数情况下要使用Object接收避免出现转换异常
     * @return 结果集
     */
    public static  T[] getExpressionValue(@Nullable Object root, @Nullable String... expressionStrings) {
        if (root == null) {
            return null;
        }
        if (ArrayUtils.isEmpty(expressionStrings)) {
            return null;
        }
        IAssert.notNull(expressionStrings, "Expressions cannot be null!");
        //noinspection ConstantConditions
        Object[] values = new Object[expressionStrings.length];
        for (int i = 0; i  0;
        }
        return true;
    }
    /**
     * 表达式条件求值
     *
     * @param root              根对象
     * @param expressionStrings Spring EL表达式数组
     * @return 值
     */
    @Nullable
    public static boolean getConditionValue(@Nullable Object root, @Nullable String... expressionStrings) {
        if (root == null) {
            return false;
        }
        if (ArrayUtils.isEmpty(expressionStrings)) {
            return false;
        }
        IAssert.notNull(expressionStrings, "Expressions cannot be null!");
        //noinspection ConstantConditions
        for (String expressionString : expressionStrings) {
            if (!getConditionValue(root, expressionString)) {
                return false;
            }
        }
        return true;
    }
}

最后有个问题想请教大家,那就是在你们参与的项目中,通常是如何应对表单重复提交的问题的?希望各位能点赞并转发这篇文章,若在处理类似问题时,也欢迎在评论区留言交流。

package com.cube.share.resubmit.check.controller;
import com.cube.share.base.templates.ApiResult;
import com.cube.share.resubmit.check.aspect.ResubmitCheck;
import com.cube.share.resubmit.check.model.Person;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author cube.li
 * @date 2021/7/9 23:05
 * @description
 */
@RestController
public class ResubmitController {
    @PostMapping("/save")
    @ResubmitCheck(argExpressions = {"[0].id", "[0].name"}, conditionExpressions = "[0].address != null")
    public ApiResult save(@RequestBody Person person) {
        return ApiResult.success();
    }
}

© 版权声明
THE END
喜欢就支持一下吧
点赞225赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容