一、背景

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

二、解决的主要思想

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

    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进行处理分布式问题。

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

请注意方法调用捕获异常时的返回值。