自定义防抖注解

问题场景

在开发中由于可能存在的网络波动问题导致用户重复提交,所以自定义一个防抖注解。设计思路:自定义注解加在接口的方法上,注解中设置了SPEL表达式,可以通过SPEL表达式从接口参数中提取Redis的Key,以这个Key作为判断是否重复提交的依据。如果没有设置SPEL表达式的话就以当前登录用户的ID作为Key。同时在将数据设置到缓存的时候使用Lua脚本执行保证Redis命令的原子性。

代码实现

自定义注解
package com.creatar.common.annotation;

import java.lang.annotation.*;

/**
 * 防抖注解
 *
 * @author: 张定辉
 * @date: 2024/6/13 上午9:43
 * @description: 防抖注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatLock {
    /**
     * SPEL表达式,根据该表达式解析出的值作为key
     *
     * @return SPEL表达式解析得到的值
     */
    String value();

    /**
     * redis前缀
     */
    String prefix() default "repeat_lock::";

    /**
     * 错误提示信息
     */
    String message() default "请勿重复提交!";

    /**
     * 设置单位时间内禁止重复提交,以秒为单位
     */
    int unitTime() default 3;
}
AOP注解处理器
package com.creatar.common.annotation.handler;

import com.creatar.common.annotation.RepeatLock;
import com.creatar.exception.CustomException;
import com.creatar.util.SecurityUtil;
import com.creatar.util.SpelUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.expression.EvaluationContext;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Objects;

/**
 * 防抖注解处理器
 *
 * @author: 张定辉
 * @date: 2024/6/13 上午9:49
 * @description: 防抖注解处理器
 */
@Aspect
@RequiredArgsConstructor
@Slf4j
@Component
public class RepeatLockAspect {
    private final RedisTemplate<String, String> redisTemplate;

    @Before("@annotation(repeatLock)")
    public void before(JoinPoint joinPoint, RepeatLock repeatLock) {
        String redisPrefix = repeatLock.prefix();
        String errorMessage = repeatLock.message();
        String value = repeatLock.value();
        int unitTime = repeatLock.unitTime();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        EvaluationContext context = SpelUtil.getContext(joinPoint.getArgs(), signature.getMethod());
        String key = SecurityUtil.getCurrentUserId();
        try {
            key = key + SpelUtil.getValue(context, value, String.class);
        } catch (Exception e) {
            log.error("防抖注解获取SPEL表达式失败,类名称:{},方法名称{}\n", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), e);
        }
        String redisKey = redisPrefix + key;
        //如果是重复提交则抛出异常
        if (isRepeat(redisKey,unitTime)) {
            throw new CustomException(errorMessage);
        }

    }

    /**
     * 使用Lua脚本执行原子性的Redis操作,避免由于并发过大从而导致的key永久有效
     * 如果key不存在则设置value为1并且设置过期时间,
     *
     * @return 如果没有key则false,如果有key则返回true表示重复提交
     */
    private boolean isRepeat(String key,int unitTime) {
        String scriptStr = """
                if redis.call('exists', KEYS[1]) == 0 then
                    redis.call('set', KEYS[1], 1, 'ex',%s)
                    return false
                else
                    return true
                end
                """.formatted(unitTime);

        RedisScript<Boolean> script = new DefaultRedisScript<>(scriptStr, Boolean.class);
        Boolean result = redisTemplate.execute(script, Collections.singletonList(key));
        if (Objects.isNull(result)) {
            return true;
        }
        return result;
    }
}
应用
package com.creatar.controller;

import com.creatar.common.Res;
import com.creatar.common.annotation.RepeatLock;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.security.PermitAll;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试接口
 *
 * @author: 张定辉
 * @date: 2024/6/15 下午4:36
 * @description: 测试接口
 */
@RestController
@RequestMapping("/test")
@Tag(name = "测试接口",description = "测试接口")
@PermitAll
public class TestController {

    @GetMapping("/testLimit")
    @RepeatLock(value = "#param",unitTime = 5)
    public Res<String> testLimit(@RequestParam(value = "param")String param) {
        return Res.success(param);
    }
}

相关推荐

  1. 定义注解

    2024-06-16 10:08:02       6 阅读
  2. 【VUE3】定义指令

    2024-06-16 10:08:02       6 阅读
  3. Spring定义注解+AOP实现接口重复提交

    2024-06-16 10:08:02       11 阅读
  4. Spring定义注解

    2024-06-16 10:08:02       15 阅读
  5. 定义注解【项目篇】

    2024-06-16 10:08:02       18 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-16 10:08:02       10 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-16 10:08:02       12 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-16 10:08:02       11 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-16 10:08:02       13 阅读

热门阅读

  1. 如何把自己卖个好价钱:实战面试谈薪水

    2024-06-16 10:08:02       7 阅读
  2. 游戏缓存与异步持久化的完美邂逅

    2024-06-16 10:08:02       5 阅读
  3. C++语法10 变量连续赋值、自增自减

    2024-06-16 10:08:02       5 阅读
  4. Android 的整体架构

    2024-06-16 10:08:02       6 阅读
  5. Android基础-RecyclerView的优点

    2024-06-16 10:08:02       7 阅读
  6. AWS无服务器 应用程序开发—第十一章API Gateway

    2024-06-16 10:08:02       4 阅读
  7. Eclipse 重构菜单

    2024-06-16 10:08:02       6 阅读