当前端重复点击按钮提交请求,造成后端接口重复处理。如果是添加操作就可能导致数据库中直接出现多份相同的数据。这种短时间重复提交是必须要避免的。本文将从后端的角度解决这个问题。
解决方案
利用每次请求的ip,uri,token,参数拼接成一个key,将key放入redis中,设置过期时间。根据ip和token判断是否是同一个用户,根据uri和参数判断是否是同一个请求,当4个都相同便是重复操作。
当请求来临时,先判断redis中是否存在这个key。
存在:判定为短时间重复提交,直接返回重复提交的提示信息。
不存在:将key存入redis中,正常执行代码逻辑。
对于上述的方案,我将利用Aop技术来实现,同时使用aop注解的形式开发。
代码实现
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<1--aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- json工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 生成token -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
代码实现
自定义注解
@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Documented // 用于生成文档
public @interface RepeatSubmit {
String value() default "";
long time() default 1; // 单位:s
}
切面类
/**
* @author 浩君狐
* @Description 使用aop技术防止重复请求操作,通过 ip+url+token+参数 拼接成key
*/
@Aspect // 切面注解
@Component // 注册成一个bean
public class RepeatSubmitAspectj {
@Resource
private RedisTemplate redisTemplate; // 注入redisTemplate
private static Logger log = LoggerFactory.getLogger(RepeatSubmitAspectj.class);
// 切点,通过注解来做切入点
@Pointcut("@annotation(org.example.aop.aspectj.RepeatSubmit)")
public void pt(){}
@Around("pt()") // ProceedingJoinPoint是继承JoinPoint,只能用于环绕通知.
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 生成key
// RequestContextUtil是我自己定义的工具类,底层是通过RequestContextHolder获得当前请求的request对象
HttpServletRequest request = RequestContextUtil.getRequest();
// IPUtil也是我自定义的工具类,专门用于获取请求的ip
String ip = IPUtil.getIpAddr(request);
String uri = request.getRequestURI();
// SecurityContextUtil是我自定义工具类,底层是SecurityContextHolder获取的token,我的项目是集成了SpringSecurity,所以有SecurityContextHolder对象
String token = SecurityContextUtil.getToken();
// 得到参数
Object[] args = joinPoint.getArgs();
// 因为我的项目全是json格式传输,所以只要得到数组的第一个就行
String params = JSON.toJSONString(args[0]);
// 拼接key
StringBuffer key = new StringBuffer();
key.append(ip);
key.append(uri);
key.append(token);
key.append(params);
// 判断redis中是否已经存在此请求的key
if (redisTemplate.hasKey(key.toString())){
// 返回错误信息
return Result.fail(HttpCode.REPEAT_ERROR);
}
// 得到注解上设置的超时时间
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
long time = repeatSubmit.time();
// 存入redis中
redisTemplate.opsForValue().set(key.toString(),uri+params,time, TimeUnit.SECONDS);
// 正常执行
Object result = joinPoint.proceed();
return result;
}
}
将注解加入到需要使用的接口方法上
@RestController
@RequestMapping("/article")
public class ArticleController {
@Resource
private ArticleService articleService;
/**
* 模糊查找文章
*/
@PostMapping("fuzzy")
@RepeatSubmit(time = 1) // 将注解加上
public Result fuzzy(@RequestBody ArticleFuzzyParam articleFuzzyParam){
return articleService.fuzzy(articleFuzzyParam);
}
}
这样注解便可以生效了,便可以实现防止重复提交的功能
其他代码
public class RequestContextUtil {
// 得到当前请求的request对象
public static HttpServletRequest getRequest(){
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
return requestAttributes.getRequest();
}
}
@Data
public class Result<T> implements Serializable{
private Integer code;
private String msg;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
public static Result success(){
Result result = new Result();
result.setCode(HttpCode.success.getCode());
result.setMsg(HttpCode.success.getMsg());
result.setData(null);
return result;
}
public static<T> Result success(T data){
Result result = new Result();
result.setCode(HttpCode.success.getCode());
result.setMsg(HttpCode.success.getMsg());
result.setData(data);
return result;
}
public static Result fail(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
return result;
}
public static Result fail(HttpCode httpCode){
return fail(httpCode.getCode(),httpCode.getMsg());
}
}
public class SecurityContextUtil {
public static String getToken(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) return null;
String token = (String) authentication.getCredentials();
if (!StringUtils.hasText(token)){
return null;
}
return token;
}
}
public enum HttpCode {
success(200,"success"),
fail(501,"fail"),
PARAM_ERROR(502, "参数错误"),
REPEAT_ERROR(520,"请勿重复提交" ),;
Integer code;
String msg;
HttpCode(Integer code,String msg){
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
由于本人能力有限,代码只能写到这一步,如有不足之处,还请大家不吝赐教。如果大家有哪里不懂的地方,也可以私信或者留言,大家一起交流一下。