Spring自定义注解+AOP实现接口防重复提交

1.防重幂等概念

有时因为网络问题导致用户多次提交表单,后端会出现重复脏数据,所以做表单防重复提交很有必要。
接口幂等性跟并发请求是两个概念,接口幂等性是针对自身,而并发请求是代表不同人。
目标:前端通过防抖, 后端通过Spring AOP + Redis实现防重幂等功能

2.接口幂等设计

目标:在指定窗口时间内,限制同一个用户对同一种业务提交相同数据。
实现思路:

  1. 通过AOP环绕通知,在进入方法前记录当前请求唯一性标识(方法+参数+用户唯一性标识)存入Redis。
  2. 判断当前请求标识能否在redis中找到,如果找到则代表还在窗口内,不允许提交。
  3. 当前方法执行完从Redis移除请求标识。

代码实现

  1. 防重复注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AvoidRepeatSubmit {
    /**
     * 有效期内,相同请求将会拒绝掉, 默认为1s
     * @return
     */
    long explainTime() default 1;
}

2.实现切面

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
@Order(1)
public class RequestSubmitAspect {

    private final RedisUtil redisUtil;

    @Around("@annotation(validate)")
    public Object around(ProceedingJoinPoint joinPoint, AvoidRepeatSubmit validate) throws Throwable {
        String userName = UserContext.get() == null ? null : UserContext.get().getUsername();
        // 1.提取请求参数
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getMethod().getName();
        String url = getRequestUrl();
        String paramStr = getRequestParams();
        log.debug("请求地址:{}", url);
        log.debug("请求方法:{}", methodName);
        log.debug("当前用户:{}", userName);

        // 2.拼接业务重复请求Key
        String repeatSubmitKey = RedisKeyUtil.repeatSubmitKey(userName, url, requestParamMD5(paramStr));

        // 3. 设置时间内,不可再次请求
        long expireTime = validate.explainTime();
        long expireAt = System.currentTimeMillis() + expireTime * 1000;;
        boolean isRepeatRequest = redisUtil.setnx(repeatSubmitKey, expireAt, expireTime);
        if (!isRepeatRequest) {
            throw new BusinessException("请求频繁,稍后重试");
        }

        // 4.执行目标方法
        return joinPoint.proceed();
    }

    /**
     * 方法执行完移除key
     * @param joinPoint
     * @param validate
     */
    @After("@annotation(validate)")
    public void after(JoinPoint joinPoint, AvoidRepeatSubmit validate) {
        String userName = UserContext.get() == null ? null : UserContext.get().getUsername();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String url = getRequestUrl();
        String paramStr;
        try {
            paramStr = getRequestParams();
        } catch (IOException e) {
            log.error("获取请求操作异常, {}", e.getMessage());
            paramStr = "";
        }

        String repeatSubmitKey = RedisKeyUtil.repeatSubmitKey(userName, url, requestParamMD5(paramStr));
        redisUtil.del(repeatSubmitKey);
    }


    /**
     * 获取请求Url
     * @return
     */
    private String getRequestUrl() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String url = "";
        if (attributes != null) {
            url = attributes.getRequest().getRequestURI();
        }
        return url;
    }

    /**
     * 获取请求参数
     * @return
     * @throws IOException
     */
    private String getRequestParams() throws IOException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 1. init请求参数,默认空字符串
        String paramStr = "";
        // 2. 获取请求参数
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            Map params = request.getParameterMap();
            if (!MyUtils.isEmpty(params)) {
                paramStr = JacksonUtils.toJson(request.getParameterMap());
            } else {
                paramStr = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8).replaceAll("\\s+", " ");
            }
        }
        log.info("请求参数:{}", paramStr);
        return paramStr;
    }

    /**
     * @param reqJson 请求的参数,这里通常是JSON
     * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
     * @return 去除参数的MD5摘要
     */
    private String requestParamMD5(final String reqJson, String...  excludeKeys) {
        if (StrUtil.isEmpty(reqJson)) {
            return "";
        }

        TreeMap paramTreeMap = JSONUtil.toBean(reqJson, TreeMap.class);
        if (excludeKeys != null) {
            List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
            if (!dedupExcludeKeys.isEmpty()) {
                for (String dedupExcludeKey : dedupExcludeKeys) {
                    paramTreeMap.remove(dedupExcludeKey);
                }
            }
        }
        String requestJson = JSON.toJSONString(paramTreeMap);
        String md5Str = DigestUtil.md5Hex(requestJson);
        log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5Str, Arrays.deepToString(excludeKeys), requestJson);
        return md5Str;
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值