当在网速不好,或者用户有意快速点击时,会出现这种情况:
这样导致一条数据在同一时间添加多条!需要防止这种行为!
防重复提交的方法有很多种,例如:
本文通过自定义注解与Spring aop实现防重复提交流程做一个说明。
点击查看:自定义注解浅析
1.首先自定义注解:
import java.lang.annotation.*;
/**
* 避免重复提交
* @author
* @version
* @since
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {
/**
* 指定时间内不可重复提交,单位毫秒,默认10000毫秒
*/
long timeout() default 10000 ;
}
2.创建切面类:
/**
* 重复提交aop
*/
@Aspect//
@Component
public class AvoidRepeatableCommitAspect {
private static final Logger logger = Logger.getLogger(AvoidRepeatableCommitAspect.class);
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate;
/**
* @param point 连接点
*/
@SuppressWarnings("unchecked")
@Around("@annotation(com.***.annotation.AvoidRepeatableCommit)")//切面拦截
public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = IPUtil.getIP(request);
//此处method获取的是代理对象(由代理模式生成的)的方法
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//此处realMethod是目标对象(原始的)的方法
// Method realMethod = point.getTarget().getClass().getDeclaredMethod(signature.getName(),method.getParameterTypes());
//目标类、方法
String className = method.getDeclaringClass().getName();
String name = method.getName();
String ipKey = String.format("%s#%s",className,name);
int hashCode = Math.abs(ipKey.hashCode());
String key = String.format("%s_%d",ip,hashCode);
//logger.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
logger.info(String.format("ipKey={},hashCode={},key={}",ipKey,hashCode,key));
//通过反射技术来获取注解对象
AvoidRepeatableCommit avoidRepeatableCommit = method.getAnnotation(AvoidRepeatableCommit.class);
long timeout = avoidRepeatableCommit.timeout();
if (timeout < 0){
//过期时间10秒
timeout = 10000;
}
//获取key键对应的值
String value = (String) redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)){
return new Message(1,"请勿重复提交!");
}
//新增一个字符串类型的值,key是键,value是值。
redisTemplate.opsForValue().set(key, UUIDUtil.uuid(),timeout,TimeUnit.MILLISECONDS);
//返回继续执行被拦截到的方法
return point.proceed();
}
}
让我们来仔细解读一下这个类
类有两个注释,分别是@Component
和@Aspect
,
@Component
是使得AvoidRepeatableCommitAspect 受Spring托管并实例化。
@Aspect
就是使得这个类具有AOP功能(你可以这样理解)两个注解缺一不可
类里面只有一个方法,名字叫做around,其实就是为了防重复提交的!
3.在我需要防重复提交的方法上添加 自定义注解:
// 新增
@AvoidRepeatableCommit //自定义注解
@RequestMapping(method = RequestMethod.POST)
public @ResponseBody Message create(SourceEntity sourceEntity) {
//设置创建时间
sourceEntity.setGmt_create(new Date());
//保存数据库
sourceEntity.save(sourceEntity);
return MessageUtil.message("sourceEntity.create.success");
}
试验效果:
可以看到,无论手速多快,一次只会提交一条数据了;
注意:
这里引入了ProceedingJoinPoint,在使用了@Around之后可以带入这个参数,代表的其实就是我的create这个函数,不过做了一些封装。
point.proceed();就是执行这个方法。
//返回继续执行被拦截到的方法
return point.proceed();
那么她是怎么找到在保存方法前拦截的呢?
@Around("@annotation(com.***.annotation.AvoidRepeatableCommit)")//切面拦截
@Around表示包围一个函数,也就是可以在函数执行前做一些事情,也可以在函数执行后做一些事情
具体各种通知可以参考大佬文章,这里就不叙述了。
点击查看:Spring中的AOP以及切入点表达式和各种通知
好了到这里就完成了,主要步骤就是通过每次提交表单时,Aspect都会保存当前key到reids(先设置过期时间)。重复提交时Aspect会判断当前redis是否有该key,若有则拦截。没有就放行。