1. 什么是重复提交
重复提交一般是指, 在前端向后端发出请求时, 两次相同的请求在相隔较短的时间内发出了两次.
比如在登录时, 输入完账户密码后需要点击登录按钮,
假如这时有个手速很快的人, 在一瞬间连续点击了100次, 那么我们的后端就要处理100次登录请求,
很显然, 这100次登录请求中, 后面99个都是无意义的.
上面那个例子可能还不够贴切, 连续的100次登录好像也不会对我们的系统造成什么破坏,
而且, 现实中如果真有这种情况也不至于有100次.
但是,如果是新增数据的操作, 那就有大问题了.
假如我们在执行新增用户的操作时, 一不小心手抖了一下, 按了两次
那么就会有两个新增的请求发到后端,
由于是新增操作, 一般并不会区判断是否重复, 所以这两个请求都会被后台执行
虽然我们在发现问题之后可以删除, 但是很显然, 这样的系统缺陷会给我们带来不必要的麻烦,
所有我们必须要在后台识别到冲突的提交, 并且不去执行它
2. 如何防止重复提交
一般来说防止重复提交有三种方式
- 前端页面限制
Interceptor
拦截器- AOP
本文主要叙述的是第三种方法AOP
3. 主要原理
3.1 自定义注解
为了能够方便的标识出哪些方法需要用到"防止重复提交", 我们需要用到一个自定义注解.
/**
* 用于标记防止重复提交的方法
* @author Johnson
*/
@Inherited // 如果一个类继承了 @Inherited 注解修饰的注解, 那么其子类也会继承这个注解
@Documented
@Retention(RetentionPolicy.RUNTIME) // 使得该注解可以使用反射得到
@Target({ElementType.TYPE, ElementType.METHOD}) // 该注解可以对 类 和 方法 起作用
public @interface PreventDuplicateSubmit {
/**
* 判断重复提交到时间间隔(ms)
* @return
*/
int interval() default 10;
}
3.2 AOP切面类
首先定义切点,
我们可以很方便的使用@Pointcut注解将切点设置为所有带有@PreventDuplicateSubmit
注解的方法
然后我们使用Around
环绕通知来增强我们的Handler
(即Controller类中的方法),
因为环绕通知可以返回我们的结果,
如果是重复提交, 就返回错误的提示信息,
如果不是, 可以返回原本的结果
/**
* @author Johnson
*/
@Component
@Aspect
public class PreventDuplicateSubmitAspect {
@Value("${prevent-duplicate-submit-cache}")
private String CACHE_NAME;
@Autowired
private RedisCache redisCache;
/**
* 开启日志
*/
private static final Logger logger = LoggerFactory.getLogger(PreventDuplicateSubmitAspect.class);
/**
* 定义切入点
*/
@Pointcut("@annotation(com.ethan.annotation.PreventDuplicateSubmit)")
public void pointcut(){};
@Around("pointcut()")
public Object checkRequest(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取请求参数
String args = JSON.toJSONString(proceedingJoinPoint.getArgs());
// 获取请求 URI
String uri = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getRequestURI();
// 指定缓存的 key
String cacheKey = CACHE_NAME + "_" + uri + args;
// 获取方法
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
// 获取注解
PreventDuplicateSubmit methodAnnotation = method.getAnnotation(PreventDuplicateSubmit.class);
// 判断是否存在缓存, 如果存在则为重复提交, 否则添加到缓存
Boolean isDuplicateSubmit = redisCache.isCaught(cacheKey, methodAnnotation.interval(), TimeUnit.SECONDS);
// isDuplicateSubmit 为true则证明没有重复提交
if(isDuplicateSubmit){
throw new RuntimeException("请勿重复提交");
}
return proceedingJoinPoint.proceed();
}
}
3.3 Redis缓存
在AOP的代码中, 我们使用了一个Redis缓存的工具类RedisCatch
, 具体代码如下
我们直接使用RedisTemplate
这个API, 帮助我查询缓存中是否包含本次的请求, 如果包含,那么重置一下缓存的有效期
如果没有就将请求添加到缓存.
/**
* spring redis 工具类
*
**/
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
public Boolean isCaught(String key, int interval, TimeUnit timeUnit) {
Object object = redisTemplate.opsForValue().get(key);
if (object != null) {
redisTemplate.expire(key, interval, timeUnit);
return true;
}
redisTemplate.opsForValue().set(key, "", interval, timeUnit);
return false;
}
}
至此, 我们代码的核心内容就结束了,
我们使用切面类代理我们的Controller中的方法, 当方法执行时, 我们先在redis中检查一下是否已经有过这个请求
有 -> 是重复提交 -> 重置缓存有效期 -> 返回错误信息
没有 -> 不是重复信息 -> 添加到缓存 -> 返回正确内容
4. 源代码
gitee仓库地址
prevent_duplicate_submit_demo: SpringBoot 自定义注解 防止重复提交 AOP方式 (gitee.com)
最后附上我的源代码连接, 欢迎各位网友阅读使用,
如果您有改进意见, 欢迎评论