接口重复提交拦截的优雅处理可以通过以下几种方式实现:
使用Token机制:在用户提交请求时,服务器生成一个Token并返回给客户端。客户端在后续的请求中需要携带这个Token。服务器会检查每个请求中的Token是否与之前生成的Token相同,如果不同则认为是重复提交。这种方式可以有效防止重复提交,并且可以方便地实现取消操作。
使用时间戳机制:在用户提交请求时,服务器记录下当前的时间戳。在后续的请求中,服务器会检查每个请求中的时间戳是否与之前记录的时间戳相同,如果不同则认为是重复提交。这种方式简单易实现,但可能会受到客户端和服务器时间不一致的影响。
无论采用哪种方式,都需要在服务器端进行拦截和处理,以避免重复提交对系统性能和数据一致性造成影响。同时,还需要在前端进行相应的提示和处理,以提升用户体验。
今天我们要介绍的是接口以redis 键值判断的形式直接做防抖:
@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 "请求过于频繁,请稍后再试";
}
@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()));
}
}
}
// 创建一个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的值)后 接口才再次可以请求。