一、定义注解 Idempotent
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @Author:
* @Description: 幂等注解 主要作用于方法和类上 作用在类上表示这个类里所有的方法都做限制
* 如果要使用nacos配置文件,不要这里使用@ConfigurationProperties,而是应该再写一个类
* 使用注解获取ncos的配置后,在切面里覆盖这个元数据
* @Date: 2024/4/25
**/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等的超时时间,默认为 1 秒
*
* 注意,如果执行时间超过它,请求还是会进来
*/
int timeout() default 3;
/**
* 时间单位,默认为 SECONDS 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* redis锁前缀
* @return
*/
String keyPrefix() default "submit:";
/**
* key分隔符
* @return
*/
String delimiter() default ":";
/**
* 提示信息,正在执行中的提示
*/
String message() default "重复请求,请稍后重试";
}
二、定义切面
import lombok.SneakyThrows;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
@Aspect
@Configuration
public class IdempotentAspect {
@Autowired
private RedissonClient redissonClient;
@SneakyThrows
@Around(value = "@within(Idempotent) || @annotation(Idempotent)")
public Object interceptor(ProceedingJoinPoint joinPoint) {
// 先获取类上的,如果没有再获取方法上的
Class<?> clazz = joinPoint.getTarget().getClass();
Idempotent idempotent = AnnotationUtils.findAnnotation(clazz, Idempotent.class);
if (ObjectUtil.isNull(idempotent)){
//获取连接点的方法签名对象
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
//Method对象
Method method = methodSignature.getMethod();
String name = methodSignature.getReturnType().getName();
//获取Method对象上的注解对象
idempotent = method.getAnnotation(Idempotent.class);
}
assert idempotent != null;
if (StringUtils.isEmpty(idempotent.keyPrefix())) {
throw new RuntimeException("重复提交前缀不能为空");
}
String userId = "1132";
//获取自定义key
final String lockKey = getLockKey(joinPoint,userId,idempotent );
// 使用Redisson分布式锁的方式判断是否重复提交
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;
try {
//尝试抢占锁 在多个服务之间共享状态,所以适用于分布式环境
// 本身在获得锁和设置锁时就是原子操作,所以不需要额外使用同步方法
isLocked = lock.tryLock();
//没有拿到锁说明已经有了请求了
if (!isLocked) {
throw new RuntimeException(idempotent.message());
}
//拿到锁后设置过期时间
lock.lock(idempotent.timeout(), idempotent.timeUnit());
return joinPoint.proceed();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
} finally {
//释放锁
if (isLocked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@SneakyThrows
@Before(value = "@within(Idempotent) || @annotation(Idempotent)")
public void interceptorBefpre(JoinPoint joinPoint) {
System.out.println("前置处理,注意参数JoinPoint和环绕ProceedingJoinPoint不一样");
}
/**
* 获取LockKey
*
* @param joinPoint 切入点
* @return
*/
public static String getLockKey(ProceedingJoinPoint joinPoint, String userId,Idempotent idempotent) {
// signature 是签名的意思,可以获取到当前请求所对应的方法名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
Object[] args = joinPoint.getArgs();
StringBuilder sb = new StringBuilder(userId);
sb.append(idempotent.delimiter()).append(name);
if (args.length > 0) {
for (Object arg : args) {
Class<?> argClass = arg.getClass();
Field[] fields = argClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(RequestKeyParam.class)) {
field.setAccessible(true); // 允许访问私有字段
// 字段上有@RequestKeyParam注解,获取注解的内部属性 暂时没用到
RequestKeyParam requestKeyParam = field.getAnnotation(RequestKeyParam.class);
String description = requestKeyParam.description();
// 获取字段值
Object fieldValue = field.get(arg);
System.out.println("Field name: " + field.getName() + ", Value: " + fieldValue);
// 这里你可以根据需要对fieldValue进行处理
sb.append(idempotent.delimiter()).append(fieldValue);
}
}
}
}
//返回指定前缀的key "submit:1132:query:123" 如果是get请求则会是submit:1132:query 没有参数作为key
return idempotent.keyPrefix() + sb;
}
三、使用注解
@RestController
@RequestMapping
@Idempotent
public class testController {
/**
* 测试 防重复提交
* @param reqVO
* @return
*/
@PostMapping("/add")
public Result queryScanCodeSwitch(@RequestBody ProjectReqVO reqVO) {
return Result.ok(reqVO);
}
/**
* 测试 防重复提交
* @param reqVO
* @return
*/
@PostMapping("/delete")
public Result delete(@RequestBody ProjectReqVO reqVO) {
return Result.ok(reqVO);
}
}
四、测试类
import lombok.Data;
/**
* 项目管理 新增 VO
*
*/
@Data
public class ProjectReqVO {
/**
* 合同编号
*/
private String contractNo;
/**
* 项目名字
*/
private String name;
/**
* 项目状态
*/
private Integer status;
}
参考的这位大哥写的,并做了一点简化SpringBoot接口防抖(防重复提交),接口幂等性,轻松搞定_springboot 防抖重复提交-CSDN博客