1.防重复提交 在前端用户使用中,用户点击过快在前后端无处理的情况下如果进行新增操作会发生数据重复
2.后端希望能够限制指定暴漏接口请求的访问频率比如,比如某接口指定用户5分钟内只可访问一次
基于通过注解基于切面在请求到接口内部前织入接口请求限制代码来实现此功能
maven引入 aop包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
/**
* 请求接收控制
*
* @author chenyanpeng
* @Date: 2021/11/27 16:30
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
//在函数方法中调用
@Target({ElementType.METHOD})
public @interface AcceptControl {
/**
* 控制时间
* 单位 豪秒
* 默认1000毫秒=>1秒
*/
long controlTime() default 3000L;
/**
* 唯一标记枚举=》枚举来区分标识定义模式 此处默认请求人主键区分
*/
AcceptControlFlagEnum mark() default AcceptControlFlagEnum.CURRENT_USER_ID;
/**
* 重复点击提示语
*
* @return
*/
String hint() default "您点的太快了,稍微歇歇吧";
}
/**
* 接口访问限制控制标识符
*
* @author chenyanpeng
* @Date: 2021/11/30 9:48
*/
public enum AcceptControlFlagEnum {
CURRENT_USER_ID(1, "当前登录人ID"),
CURRENT_USER_ID_AND_PARAM(2, "当前登录人和请求参数"),
CURRENT_USER_ID_CURRENT_TIME(2, "当前时间(yyyy-MM-dd HH:mm:ss)");
private Integer flag;
private String message;
AcceptControlFlagEnum(Integer flag, String message) {
this.flag = flag;
this.message = message;
}
}
这里采用spring-boot-aop包 aspectj,也可以使用其他方式进行处理,由于需要在时间内控制,并且需要唯一标识,并且需要灵活控制,这里采用数据库缓存redis处理,
/**
* 注解切面功能
*
* @author chenyanpeng
* @Date: 2021/12/3 17:18
*/
@Slf4j
//声明切面
@Aspect
//级别
@Order(AopSortConstant.ACCEPT)
public class AcceptControlAop {
public static final String ACCEPT_CONTROL = ConstantRedisKey.ACCEPT_CONTROL;
@Autowired
private DingNotification dingNotification;
@Autowired
private RedisUtil redisUtil;
/**
* 日志切入点
*/
@Pointcut("@annotation(com.--.annotation.annotion.AcceptControl)")
private void geLogPointcut() {
}
@Before("geLogPointcut()")
public void before(JoinPoint point) {
String splicing = ":";
try {
//获取切面注解
MethodSignature methodSignature = (MethodSignature) point.getSignature();
AcceptControl annotation = methodSignature.getMethod().getAnnotation(AcceptControl.class);
//得到请求接口地址url,url为统一基础标识
String flag = HttpServeletUtil.getRequestUrl();
//选取标识枚举,对应策略采用标识
AcceptControlFlagEnum mark = annotation.mark();
switch (mark) {
//附加当前登录人主键做为标识
case CURRENT_USER_ID:
flag += String.valueOf(CurrentUser.getUserID());
break;
//主键加上当前时间为标识 时间为指定格式 转换指定时间范围 比如说1小时内 2021-11-11 08:00
case CURRENT_USER_ID_CURRENT_TIME:
flag += CurrentUser.getUserID() + splicing + DateUtil.format(new Date(), DatePattern.NORM_DATETIME_MINUTE_PATTERN);
break;
//请求参数同时作为标识条件 此处理确定重复点击,重复点击参数条件一致
case CURRENT_USER_ID_AND_PARAM:
//当如果get请求参数在url则拼接url
flag = HttpServeletUtil.getUrlAndParam() + splicing + CurrentUser.getUserID();
Object[] args = point.getArgs();
if (ArrayUtil.isNotEmpty(args) && args[0] != null) {
//非如json则转成字符串拼接参数,
flag += splicing + JsonUtil.toJson(args);
}
break;
default:
break;
}
//作为标识存储缓存数据库中
String redisKey = ACCEPT_CONTROL + splicing + flag;
if (redisUtil.hasKey(redisKey)) {
//如果存在则抛出异常,代表此刻无法访问,还在控制期间中
BllErrorException.throwBll(annotation.hint());
} else {
//已该标识存入换粗库中,设置自定义的控制时间,指定为秒
redisUtil.set(redisKey, point.getArgs(), annotation.controlTime(), TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
//容错
if (e instanceof BllErrorException) {
//直接抛出业务异常
throw e;
} else {
log.error("重复注解异常捕获", e);
try {
//告知注解出现异常,便于分析原因
dingNotification.sendNotification(new NotificationObj(e.getMessage(), GlobalExceptionHandler.printStackTraceToString(e)), null);
} catch (Exception exception) {
log.error("重复注解异常捕获+1", exception);
}
}
}
}
}
//采用默认配置
@AcceptControl
@GetMapping("/demo1")
public ResultUtil test(@RequestBody BaseModel baseModel){
return ResultUtil.ok();
}
/**
* 自定义配置
*
* @ controlTime: 控制时间
* @ mark: 标识
* @ hint: 自定义配置
*/
@AcceptControl(controlTime = 1000L,mark = AcceptControlFlagEnum.CURRENT_USER_ID_AND_PARAM,hint = "我是自定义提示")
@GetMapping("/demo2")
public ResultUtil test(@RequestBody BaseModel baseModel){
return ResultUtil.ok();
}
1.统一处理,方便维护
2.调用简单,只需根据业务正确配置处理方式即可