java防重复提交AOP

该博客详细介绍了如何利用Spring AOP和Redis实现防止接口重复提交的功能。通过定义`NoRepeatSubmit`注解,设置重复提交间隔,并在切面中计算请求参数的MD5值作为Redis键,判断是否存在来避免并发重复操作。同时,针对query和body参数进行单独处理,确保防重复机制的准确性。此外,还提供了参数可选和合并query与body的优化建议。
摘要由CSDN通过智能技术生成

核心逻辑阐述:
1.定义注解 NoRepeatSubmit,包含可重复提交时间间隔;使用时标记在controller需要防重复提交的类或者方法上;
2.定义AOP切面NoRepeatSubmitAspect切NoRepeatSubmit注解;
3.对请求方法query参数和body参数分别做MD5,作为redis中的key;
4.如果在redis中找到了就算重复,抛出异常;
5.如果未重复,放到redis中并设置下次可提交的时间;

注意:
为了防止并发出现问题,判断redis是否重复时需要加锁;
改进:
参加计算重复的参数可选;
将query和body做到一起;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface NoRepeatSubmit {

    /**
     * 默认限制重复提交时间单位
     */
    TimeUnit lockTimeUnit() default TimeUnit.SECONDS;

    /**
     * 默认限制重复提交时间
     */
    long lockTime()  default 30;
}

@Aspect
@Component
@Order
public class NoRepeatSubmitAspect {

    @Resource
    private RedisComponent redisComponent;

    @Pointcut("@annotation(com.guazi.opl.dealer.tool.application.aop.NoRepeatSubmit)")
    public void noRepeatSubmit() {
    }

    @Around(value = "noRepeatSubmit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) attributes;
        if (Objects.isNull(sra)) {
            return joinPoint.proceed();
        }
        NoRepeatSubmit noRepeatSubmit = this.getAnnotation(joinPoint);
        if (Objects.isNull(noRepeatSubmit)) {
            return joinPoint.proceed();
        }
        HttpServletRequest request = sra.getRequest();
        String queryString = request.getQueryString();
        //计算query参数的cache key
        String queryKey = this.calcQueryKey(queryString);
        String noRepeatQueryKey = this.getNoRepeatQueryKey(queryKey, request);
        //此args中不止含有body,如果还有requestParam、multipartfile指定的参数
        Object[] args = joinPoint.getArgs();
        String paramKey = this.calcParamKey(args);
        String noRepeatParamKey = this.getNoRepeatParamKey(paramKey, request);
        synchronized (this) {
            //判断queryKey是否在redis中存在,如果存在说明重复
            boolean queryRepeatFlag = this.judgeAndSetRepeat(noRepeatQueryKey, noRepeatSubmit);
            //先判断paramKey是否在redis中存在,如果存在说明已经重复
            boolean paramRepeatFlag = this.judgeAndSetRepeat(noRepeatParamKey, noRepeatSubmit);
            if (queryRepeatFlag && paramRepeatFlag) {
                throw new BusinessException(-1, "正在处理,请勿重复操作");
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 获取防重复提交注解
     */
    private NoRepeatSubmit getAnnotation(ProceedingJoinPoint joinPoint) {
        //先从方法上获取
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
        //获取不到再到类上找
        if (Objects.isNull(annotation)) {
            annotation = joinPoint.getTarget().getClass().getAnnotation(NoRepeatSubmit.class);
        }
        return annotation;
    }


    /**
     * 计算queryString对应的Md5值为value
     */
    private String calcQueryKey(String queryString) throws Exception {
        if (StringUtils.isBlank(queryString)) {
            return StringUtils.EMPTY;
        }
        return SignatureUtils.md5(queryString).substring(5, 15);
    }


    /**
     * 计算queryString对应的Md5最为value
     */
    private String calcParamKey(Object[] args) throws Exception {
        TreeSet<String> paramTree = new TreeSet<>();
        for (Object arg : args) {
            if (arg instanceof BindingResult || arg instanceof HttpServletResponse || arg instanceof HttpServletRequest) {
                //requestURL参数,这直接返回
                continue;
            }
            //转成json字符串
            String jsonArg = JsonUtils.toJsonString(arg);
            paramTree.add(jsonArg);
        }

        if (paramTree.isEmpty()) {
            return StringUtils.EMPTY;
        }
        //获取有序body json后的参数
        List<String> paramList = new ArrayList<>(paramTree);
        //连在一起
        String bodyJson = String.join("", paramList);
        return SignatureUtils.md5(bodyJson).substring(5, 15);
    }


    /**
     * 获取防重复提交query的key
     */
    private String getNoRepeatQueryKey(String queryKey, HttpServletRequest request) {
        if (StringUtils.isBlank(queryKey)) {
            return StringUtils.EMPTY;
        }
        String noRepeatSubmitPrefix = RedisKeyConstant.NO_REPEAT_QUERY_SUBMIT_PREFIX + request.getRequestURL();
        return noRepeatSubmitPrefix + queryKey;
    }

    /**
     * 获取防重复提交body的key
     */
    private String getNoRepeatParamKey(String paramKey, HttpServletRequest request) {
        if (StringUtils.isBlank(paramKey)) {
            return StringUtils.EMPTY;
        }
        String noRepeatSubmitPrefix = RedisKeyConstant.NO_REPEAT_QUERY_SUBMIT_PREFIX + request.getRequestURL();
        return noRepeatSubmitPrefix + paramKey;
    }

    /**
     * 如果不重复 则设置下次可提交时间 返回false
     * 否则返回true
     */
    private boolean judgeAndSetRepeat(String cacheKey, NoRepeatSubmit noRepeatSubmit) {
        if (StringUtils.isBlank(cacheKey)) {
            return true;
        }
        String value = redisComponent.getString(cacheKey);
        if (Objects.isNull(value)) {
            redisComponent.setString(cacheKey, cacheKey, noRepeatSubmit.lockTime(), noRepeatSubmit.lockTimeUnit());
            return false;
        }
        return true;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值