java一个注解实现接口幂等性

314 阅读1分钟

不多说直接上代码,复制粘贴可用

@Aspect
@Component
@Slf4j
public class IdempotencyAspect {
    @Resource
    StringRedisTemplate stringRedisTemplate;


    // 定义切点Pointcut
    @Pointcut("@annotation(com.sunjinke.ncmis.service.aop.Idempotency)")
    public void executeService() {
    }

    @Around("executeService()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        if (args.length <= 0) {
            return ResponseModel.fail("数据错误");
        }
        Object arg = args[0];
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Idempotency annotation = signature.getMethod().getAnnotation(Idempotency.class);
        String[] value = annotation.value();
        Long time = annotation.expireTime();
        log.info("请求参数:{}", JSON.toJSONString(arg));
        String s = ReqDedupHelper.dedupParamMD5(JSON.toJSONString(arg), value);
        String KEY = s;
        long expireTime = time;// 1000毫秒过期,1000ms内的重复请求会认为重复
        long expireAt = System.currentTimeMillis() + expireTime;
        String val = "ex@" + expireAt;
        // NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作
        Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime),
                RedisStringCommands.SetOption.SET_IF_ABSENT));
        log.info("请求去重在redis中的结果={}", firstSet);
        if (firstSet != null && firstSet) {

        } else {
            return ResponseModel.fail("请求过去频繁,稍后再试");
        }
        Object result = pjp.proceed();
        return result;
    }


}
/**
 * 对参数进行MD5加密 相同参数加密出来的是相同的,利用此特性对请求并发去重
 * GG
 */
@Slf4j
public class ReqDedupHelper {

    /**
     * @param reqJSON     请求的参数,这里通常是JSON
     * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
     * @return 去除参数的MD5摘要
     */
    public static String dedupParamMD5(final String reqJSON, String... excludeKeys) {
        String decreptParam = reqJSON;

        TreeMap paramTreeMap = JSON.parseObject(decreptParam, TreeMap.class);
        if (excludeKeys != null) {
            List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
            if (!dedupExcludeKeys.isEmpty()) {
                for (String dedupExcludeKey : dedupExcludeKeys) {
                    paramTreeMap.remove(dedupExcludeKey);
                }
            }
        }

        String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
        String md5deDupParam = jdkMD5(paramTreeMapJSON);
        log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
        return md5deDupParam;
    }

    private static String jdkMD5(String src) {
        String res = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] mdBytes = messageDigest.digest(src.getBytes());
            res = DatatypeConverter.printHexBinary(mdBytes);
        } catch (Exception e) {
            log.error("", e);
        }
        return res;
    }
}
/**
 * 幂等性注解(相同参数在一定时间内不能重复请求)
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotency {
    /**
     * 排除不需要加加密的字段
     *
     * @return
     */
    String[] value() default {};

    /**
     * 指定过期时间
     *
     * @return
     */

    long expireTime() default 2000;
}

基本原理: 创建一个注解,注解可以设置排除加密的字段和过期时间,利用aop切面编程,对参数进行MD5加密得到唯一值,放入redis中 返回true通过,否者在规定时间重复请求