• 防止重复提交解决方案-(基于JAVA注解+AOP切面)


    1、前言

      近期在构建项目脚手架时,关于接口幂等性问题,考虑做成独立模块工具放进脚手架中进行通用。
      如何保证接口幂等性,换句话说就是如何防止接口重复提交。通常,前后端都需要考虑如何实现相关控制。

    • 前端常用的解决方案是“表单提交完成,按钮置灰、按钮不可用或者关闭相关页面”。
    • 常见的后端解决方案有“基于JAVA注解+AOP切面实现防止重复提交“。

    2、方案

      基于JAVA注解+AOP切面方式实现防止重复提交,一般需要自定义JAVA注解,采用AOP切面解析注解,实现接口首次请求提交时,将接口请求标记(由接口签名、请求token、请求客户端ip等组成)存储至redis,并设置超时时间T(T时间之后redis清除接口请求标记),接口每次请求都先检查redis中接口标记,若存在接口请求标记,则判定为接口重复提交,进行拦截返回处理。

    3、实现

         本次采用的基础框架为SpringBoot,涉及的组件模块有AOP、WEB、Redis、Lombok、Fastjson。详细代码与配置如下文。

    • pom依赖

    复制代码
    <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.28</version>
            </dependency>
    
        </dependencies>
    复制代码
    • 配置文件

    复制代码
    server.port=8888
    
    # Redis数据库索引(默认为0)
    spring.redis.database=0
    # Redis服务器地址
    spring.redis.host=127.0.0.1
    # Redis服务器连接端口
    spring.redis.port=6379
    # Redis服务器连接密码(默认为空)
    spring.redis.password=
    # 连接池最大连接数(使用负值表示没有限制)
    spring.redis.pool.max-active=8
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.pool.max-wait=-1
    # 连接池中的最大空闲连接
    spring.redis.pool.max-idle=8
    # 连接池中的最小空闲连接
    spring.redis.pool.min-idle=0
    # 连接超时时间(毫秒)
    spring.redis.timeout=5000
    复制代码
    • 自定义注解

    复制代码
    /**
     * @author :Gavin
     * @see :防止重复操作注解
     */
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface PreventDuplication {
        /**
         * 防重复操作限时标记数值(存储redis限时标记数值)
         */
        String value() default "value" ;
    
        /**
         * 防重复操作过期时间(借助redis实现限时控制)
         */
        long expireSeconds() default 10;
    }
    复制代码
    • 自定义切面(解析注解)

        切面用于处理防重复提交注解,通过redis中接口请求限时标记控制接口的提交请求。

    复制代码
    /**
     * @author :Gavin
     * @see :防止重复操作切面(处理切面注解)
     */
    
    @Aspect
    @Component
    public class PreventDuplicationAspect {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 定义切点
         */
        @Pointcut("@annotation(com.example.idempotent.idempotent.annotation.PreventDuplication)")
        public void preventDuplication() {
        }
    
        /**
         * 环绕通知 (可以控制目标方法前中后期执行操作,目标方法执行前后分别执行一些代码)
         *
         * @param joinPoint
         * @return
         */
        @Around("preventDuplication()")
        public Object before(ProceedingJoinPoint joinPoint) throws Exception {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            Assert.notNull(request, "request cannot be null.");
    
            //获取执行方法
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            //获取防重复提交注解
            PreventDuplication annotation = method.getAnnotation(PreventDuplication.class);
    
            // 获取token以及方法标记,生成redisKey和redisValue
            String token = request.getHeader(IdempotentConstant.TOKEN);
            String redisKey = IdempotentConstant.PREVENT_DUPLICATION_PREFIX
                    .concat(token)
                    .concat(getMethodSign(method, joinPoint.getArgs()));
            String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");
    
            if (!redisTemplate.hasKey(redisKey)) {
                //设置防重复操作限时标记(前置通知)
                redisTemplate.opsForValue()
                        .set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
                try {
                    //正常执行方法并返回
                    //ProceedingJoinPoint类型参数可以决定是否执行目标方法,且环绕通知必须要有返回值,返回值即为目标方法的返回值
                    return joinPoint.proceed();
                } catch (Throwable throwable) {
                    //确保方法执行异常实时释放限时标记(异常后置通知)
                    redisTemplate.delete(redisKey);
                    throw new RuntimeException(throwable);
                }
            } else {
                throw new RuntimeException("请勿重复提交");
            }
        }
    
        /**
         * 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
         *
         * @param method
         * @param args
         * @return
         */
        private String getMethodSign(Method method, Object... args) {
            StringBuilder sb = new StringBuilder(method.toString());
            for (Object arg : args) {
                sb.append(toString(arg));
            }
            return DigestUtils.sha1DigestAsHex(sb.toString());
        }
    
        private String toString(Object arg) {
            if (Objects.isNull(arg)) {
                return "null";
            }
            if (arg instanceof Number) {
                return arg.toString();
            }
            return JSONObject.toJSONString(arg);
        }
    }
    复制代码
    复制代码
    public interface IdempotentConstant {
    
        String TOKEN = "token";
    
        String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
    }
    复制代码
    • controller实现(使用注解)

    复制代码
    @Slf4j
    @RestController
    @RequestMapping("/web")
    public class IdempotentController {
    
        @PostMapping("/sayNoDuplication")
        @PreventDuplication(expireSeconds = 8)
        public String sayNoDuplication(@RequestParam("requestNum") String requestNum) {
            log.info("sayNoDuplicatin requestNum:{}", requestNum);
            return "sayNoDuplicatin".concat(requestNum);
        }
    
    }
    复制代码

    4、测试

    • 正常请求(首次)

         首次请求,接口正常返回处理结果。

    •  限定时间内重复请求(上文设置8s)

       在限定时间内重复请求,AOP切面拦截处理抛出异常,终止接口处理逻辑,异常返回。

     控制台报错:

     

    5、源代码

      本文代码已经上传托管至GitHub以及Gitee,有需要的读者请自行下载。

    • GitHub:https://github.com/gavincoder/idempotent.git
    • Gitee:https://gitee.com/gavincoderspace/idempotent.git

    Java后端接口防止重复提交

    最近在开发的过程中遇到前端没有对提交按钮做点击后变灰处理,必须在后端添加防止重复提交的校验。网上有很多中方案,我这边采用的是aop+自定义注解方式实现。
      刚开始采用利用自定义注解+aop+redis防止重复提交这篇博客的逻辑去实现,但是后来在测试多线程访问的时候会出现问题,然后参考网上Redis分布式锁的逻辑,多线程情况下测试只有一个可以通过。参考了LockManager中关于加锁的逻辑。具体的代码逻辑就不占了,只是在上面介绍的资料基础上做了稍微的改造。

    参考资料
    https://blog.csdn.net/weixin_37505014/article/details/103461741
    https://gitee.com/billion/redisLock/

    自定义注解解决API接口幂等设计防止表单重复提交(生成token存放到redis中)

    写在后面

      本文重点在于讲解如何采用基于JAVA注解+AOP切面快速实现防重复提交功能,该方案实现可以完全胜任非高并发场景下实施应用。但是在高并发场景下仍然有不足之处,存在线程安全问题(可以采用Jemeter复现问题)。那么,如何实现支持高并发场景防重复提交功能?请读者查看我的博文《基于Redis实现分布式锁》,这篇博客对本文基于JAVA注解+AOP切面实现进行了优化改造,以便应用于高并发场景。

  • 相关阅读:
    数据库
    用hosts管理IP和域名
    软件测试周期
    jdk安装、环境配置
    IntelliJ IDEA 下载、安装、破解及卸载
    Servlet线程
    servlet什么时候被实例化?【转】
    JSP转译成Servlet详细过程【转】
    电脑使用--快捷键等【转】
    api大全
  • 原文地址:https://www.cnblogs.com/zgq123456/p/15242281.html
Copyright © 2020-2023  润新知