问题场景
在开发中由于可能存在的网络波动问题导致用户重复提交,所以自定义一个防抖注解。设计思路:自定义注解加在接口的方法上,注解中设置了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);
}
}