后端防止重复点击设计

一、背景

    想了解的都懂,不再描述。

二、解决的主要思想

    重复调用会存在在以下几种情况中:

    1、点击一次后无遮罩可进行二次点击。(可通过前端进行设置)

    2、在出现遮罩之前,可能由于屏幕的特殊性,而自行进行了多次点击。(主要是避免此种问题)

    在同一时刻,调用同一个方法,且入参一致则认定为是重复点击,此时不在执行后续方法。

三、思路

    1、为了方法的通用性以及和业务系统进行解耦,在此使用aop的环绕增强。

    2、在增强中判断当前的类名+方法名+入参转换为(json)组装成的key是否已经在redis中存在

    3、利用redis的setNx(此方法为原子性,不建议判断后再进行set,避免出现线程安全问题)

    4、返回为true,则说明未提交。调用pjp.proceed方法执行。

        4.1、方法执行后删除当前redis值

    5、返回为false,则说明为重复点击,则直接返回。

四、代码实现

    1、定义注解类

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ForbidRepeatClick { }

    2、定义切面

    @Component
    @Aspect
    @Order(1)
    public class ForbidRepeatClickInterceptor {
        private static final Logger LOGGER = LoggerFactory.getLogger(ForbidRepeatClickInterceptor.class);
        @Pointcut("@annotation(ForbidRepeatClick)")
       public void pointcut() {
       }
        @Around("pointcut()")
        public Object forbidRepeatClick(ProceedingJoinPoint pjp) throws Throwable {
            //1、根据入参方法名获取组装的redis的key值
            String redisKey = getRedisKey(pjp);
            LOGGER.info("ForbidRepeatClickInterceptor->forbidRepeatClick->redisKey:{}", redisKey);
           
            if(RedisUtil.setIfAbsent(redisKey, "exist")) {
                LOGGER.info("ForbidRepeatClickInterceptor->forbidRepeatClick->redisKey:notexist");
                //2、当前方法同一时间段无完全同参数调用,则继续往下执行
               Object res = pjp.proceed();
                //2.1 执行后将数据从redis删除
                RedisUtil.delete(redisKey);
                return res;
            }
        
           //3、当前方法同一时间段具有相同参数执行,则不再执行,直接返回错误标识
           LOGGER.info("ForbidRepeatClickInterceptor->forbidRepeatClick->redisKey:exist");
           CommonResponse commonResponse = new CommonResponse();
           commonResponse.setCode(ResultEnum.REPEAT_CLICK.getNo());
           return commonResponse;      
       }
    
        /**
         * 获取存储的redis的key值
         * @param pjp
         * @return
         */
        private String getRedisKey(ProceedingJoinPoint pjp) {
           
            // 1、获取被代理的对象类型
            String className = pjp.getTarget().getClass().getName();
            
            // 2、获取当前代理的方法名
           String methodName = pjp.getSignature().getName();
           
            // 3、获取入参并转换成jason串
            String convertJson = convertArgsToJson(pjp.getArgs());
           
            String redisKey = className + "->" + methodName + "->" + convertJson;
            
            return redisKey;
        }
    
       /**
         * 将传入的参数拼接成json类型的字符串
         * @param args
         * @return
         */
        private String convertArgsToJson(Object[] args) {
            StringBuilder convertJson = new StringBuilder();
            for (Object object : args) {
               if(!(object instanceof HttpServletRequest)) {  // 此处判断不能舍去
                    convertJson.append(JSON.toJSONString(object));
               }
            }
           return convertJson.toString();
        }

 

注:本次设计主要是利用到了redis是线程安全的以及redis进行处理分布式问题。

方法返回值的设计在此不再赘述,如果后端方法使用的是同类型的返回值,可直接返回该类型,如果不同类型,请参考策略模式进行设计。

请注意方法调用捕获异常时的返回值。(如果设计时有问题,可以私聊博主哦)

aop失效请参考链接:后续补充。~~

你可能感兴趣的:(实战系列,java)