使用切面实现前端重复提交(防抖)
- 代码结构
- 定义注解
- 请求锁切面处理器
- 入参对象
- 使用注解
代码结构
原理:
1、前端提交保存操作;
2、后端通过注解指定重复提交的关键字段进行识别,可以有多个;
3、拼接关键字段,缓存到redis中,设置到期时间(默认3秒);
4、命中缓存则进行防抖处理,否则进行正常业务。
定义注解
/*** @description 请求防抖锁,用于防止前端重复提交导致的错误*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLock {/**redis 锁前缀 */String prefix() default "";/**redis 锁过期时间, 默认3秒 */int expire() default 3;/** redis 锁过期时间单位,默认单位为秒 */TimeUnit timeUnit() default TimeUnit.SECONDS;/** redis key分隔符, 分隔符 */String delimiter() default ":";/** 防抖关键标识,使用spring el 表达式<br>* 例子:<br>* 第一个参数(对象)中的一个属性(电话号码) : params[0].phone<br>* 第一个参数(Map)中的键值对(电话号码) : params[0]['phone']<br>*/String[] keys();
}
请求锁切面处理器
/*** @description 请求锁切面处理器*/
@Aspect
@Configuration
public class RequestLockAspect {@AutowiredStringRedisTemplate stringRedisTemplate;/** 切入点声明 */@Pointcut("execution(public * * (..)) && @annotation(zzc.learn.springboot.demo.repeatsubmit.annotation.RequestLock)")public void pointcut() {// do nothing}/** 定义切面:环绕通知 */@Around("pointcut()")public Object interceptor(ProceedingJoinPoint joinPoint) {MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method method = methodSignature.getMethod();RequestLock requestLock = method.getAnnotation(RequestLock.class);if (StringUtils.isEmpty(requestLock.prefix())) {// throw new RuntimeException("重复提交前缀不能为空");return "重复提交前缀不能为空";}//获取自定义keyfinal String lockKey = _getLockKey(joinPoint);final Boolean success = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(requestLock.expire(), requestLock.timeUnit()), RedisStringCommands.SetOption.SET_IF_ABSENT));if (!success) {// throw new RuntimeException("您的操作太快了,请稍后重试");return "您的操作太快了,请稍后重试";}try {return joinPoint.proceed();} catch (Throwable throwable) {// throw new RuntimeException("系统异常");return "系统异常";}}/** 获取防抖提交对应的rediss的key */private String _getLockKey(ProceedingJoinPoint joinPoint) {//获取连接点的方法签名对象MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();//Method对象Method method = methodSignature.getMethod();//获取Method对象上的注解对象RequestLock requestLock = method.getAnnotation(RequestLock.class);//获取方法参数List<Object> spelValueist = _getSpelValueList(requestLock, joinPoint.getArgs());StringBuilder sb = new StringBuilder();for (Object object : spelValueist) {sb.append(requestLock.delimiter()).append(object);}//返回指定前缀的keyreturn requestLock.prefix() + sb;}/** 根据spel表达式读取对应参数的值 */private List<Object> _getSpelValueList(RequestLock requestLock, final Object[] args) {List<Object> spelValueist = new ArrayList<>();ExpressionParser parser = new SpelExpressionParser();EvaluationContext context = new StandardEvaluationContext();context.setVariable("params", args);String[] spelArr = requestLock.keys();for (int i = 0; i < spelArr.length; i++) {String expressionString = spelArr[i];if(StringUtils.isBlank(expressionString)) {continue;}Expression expression = parser.parseExpression(expressionString);spelValueist.add(expression.getValue(context));}return spelValueist;}
}
入参对象
@Data
public class User {private String name;private Integer age;private String phone;
}
使用注解
@Api(value = "重复提交测试", tags = {"重复提交测试"})
@RestController
@RequestMapping("/repeatsubmit/user")
public class UserController {@ApiOperation(value = "增加用户(不限制重复提交)", notes = "增加用户1")@PostMapping("/addUser1")public String addUser1(@RequestBody User user) {System.out.println("不做任何处理" + user);return "添加成功";}@ApiOperation(value = "增加对象类型用户(限制重复提交)", notes = "增加用户2")@PostMapping("/addUser2")@RequestLock(prefix = "addUser", keys= {"#params[0].name","#params[0].phone"})public String addUser2(@RequestBody User user, @RequestParam String name) {System.out.println("防重提交" + user);return "添加成功";}@ApiOperation(value = "增加Map类型用户(限制重复提交)", notes = "增加用户3")@PostMapping("/addUser3")@RequestLock(prefix = "addUser", keys = {"#params[0]['phone']"})public String addUser3(@RequestBody Map<String, Object> paramMap, @RequestParam String name) {System.out.println("防重提交" + paramMap);return "添加成功";}
}