java接口防抖的优雅处理

接口重复提交拦截的优雅处理可以通过以下几种方式实现:

使用Token机制:在用户提交请求时,服务器生成一个Token并返回给客户端。客户端在后续的请求中需要携带这个Token。服务器会检查每个请求中的Token是否与之前生成的Token相同,如果不同则认为是重复提交。这种方式可以有效防止重复提交,并且可以方便地实现取消操作。

使用时间戳机制:在用户提交请求时,服务器记录下当前的时间戳。在后续的请求中,服务器会检查每个请求中的时间戳是否与之前记录的时间戳相同,如果不同则认为是重复提交。这种方式简单易实现,但可能会受到客户端和服务器时间不一致的影响。

无论采用哪种方式,都需要在服务器端进行拦截和处理,以避免重复提交对系统性能和数据一致性造成影响。同时,还需要在前端进行相应的提示和处理,以提升用户体验。

今天我们要介绍的是接口以redis 键值判断的形式直接做防抖:

接口增加注解类ApiResubmit

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

    /**
     * 限流前缀,用来区分不同的限流场景
     */
    String prefix() default "";

    /**
     * 限流key,用来辨别是否是一次重复的请求,支持SpEL,可以从方法的入参中获取
     */
    String key() default "";

    /**
     * 请求禁止秒数,即在多少秒内禁止重复请求
     */
    int forbidSeconds() default 10;

    /**
     * 限流提示信息
     */
    String message() default "请求过于频繁,请稍后再试";

}

aop注解切面逻辑

@Slf4j
@Aspect
@Component
public class ResubmitAspect {

    @Autowired
    private RedissonClient redissonClient;

    private static final String DEFAULT_BLOCKING_MESSAGE = "提交的频率过快,请稍后再试";

    //切点
    @Pointcut("@annotation(ApiResubmit)")
    public void resubmitPointcut() {
    }

    @Around(value = "resubmitPointcut()")
    public Object checkSubmit(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        ApiResubmit annotation = AnnotationUtils.findAnnotation(method, ApiResubmit.class);
        if (annotation != null) {  
            //以类名+方法名作为key的默认前缀
            String defaultPrefix = joinPoint.getSignature().getDeclaringTypeName() + "#" + method.getName();

            //获取此方法所传入的参数 map<参数名, 参数值>
            Map<String, Object> methodParam = getMethodParam(joinPoint);

            validate(annotation, defaultPrefix, methodParam);
        }
        return joinPoint.proceed();
    }

    private Map<String, Object> getMethodParam(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> paramMap = new HashMap<>(args.length);
        for (int i = 0; i < args.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }
        return paramMap;
    }


    private void validate(ApiResubmit annotation, String defaultPrefix, Map<String, Object> methodParam) {
        String prefix = StringUtils.isBlank(annotation.prefix()) ? defaultPrefix : annotation.prefix();
        // 创建一个ExpressionParser实例
        ExpressionParser parser = new SpelExpressionParser();
        // 创建一个StandardEvaluationContext实例,并将Person对象添加到其中
        StandardEvaluationContext context = new StandardEvaluationContext();
          for (Map.Entry<String, Object> entry : methodParam.entrySet()) {
            String key = entry.getKey();
            context.setVariable(key, entry.getValue());
        }
        // 使用EL表达式获取对象中的属性值
        Object key = (String) parser.parseExpression(annotation.key()).getValue(context);
        String msg = annotation.message();
        msg = StringUtils.isBlank(msg) ? DEFAULT_BLOCKING_MESSAGE : msg;
        //redis 键值判断
        RBucket<String> bucket = redissonClient.getBucket(prefix + key.toString());
        if (bucket.get() != null) {
            throw new SysException(msg);
        } else {
            bucket.set(msg, Duration.ofSeconds(annotation.forbidSeconds()));
        }
    }
}

方法解析

  1. el表达式解析接口实际数据
// 创建一个ExpressionParser实例
ExpressionParser parser = new SpelExpressionParser();
// 创建一个StandardEvaluationContext实例,并将Person对象添加到其中
StandardEvaluationContext context = new StandardEvaluationContext();
  for (Map.Entry<String, Object> entry : methodParam.entrySet()) {
            String key = entry.getKey();
            context.setVariable(key, entry.getValue());
        }
// 使用EL表达式获取对象中的属性值
String key = (String) parser.parseExpression(annotation.key()).getValue(context);

这里我们使用Spring Expression Language (SpEL)来解析和执行表达式的。SpEL是一种强大的表达式语言,它允许在运行时动态地访问和操作对象的属性和方法。

首先,创建一个ExpressionParser实例。这是一个用于解析和执行SpEL表达式的工具类。

然后,创建一个StandardEvaluationContext实例。这个上下文对象用于存储变量的值,这些值可以在SpEL表达式中使用。

接下来,遍历methodParam这个Map对象。这个Map对象的键是字符串,值是对象。对于Map中的每一个条目,将键和值添加到context中。这样,当SpEL表达式被解析时,就可以通过键来获取对应的值。

最后,使用parser.parseExpression(annotation.key()).getValue(context)来解析和执行SpEL表达式。这个表达式是通过annotation.key()获取的,然后使用context中的变量值进行替换。执行结果是一个对象,这个对象是通过SpEL表达式获取的。
2. redission 分布式锁控制防抖
先引入redisson工具:

   <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.23.5</version>
        </dependency>

此时我们可以用redissonClient,比如redis 键值判断:

 RBucket<String> bucket = redissonClient.getBucket(prefix + key.toString());

redisson有其他很强大的功能:分布式锁、分布式对象、分布式集合等此处不再过多介绍。

测试

此时有post接口如下:

 @PostMapping("/commit/product")
    @ApiResubmit(prefix = "commitProduct", key = "#product.id", message = "正在提交中,请稍后再试")
    public void commitProduct(@RequestBody Product product) {
    }

假设现在product的ID=1调用此接口,那么此时他第一次调用是可以成功的。
第二次接口将抛出异常,过10秒(forbidSeconds的值)后 接口才再次可以请求。

你可能感兴趣的:(java,github,git)