⛰️个人主页:     蒾酒

系列专栏:《spring boot实战》

目录

写在前面

实现思路

实现步骤

1.定义防重复提交注解

2.编写一个切面去发现该注解然后执行防重复提交逻辑

3.测试

依赖条件

1.接口上标记防重复提交注解

2.接口测试

写在最后

写在前面

本文介绍了springboot开发后端服务中,防重复提交功能的设计与实现,坚持看完相信对你有帮助。

同时欢迎订阅springboot系列专栏,持续分享spring boot的使用经验。

实现思路

通过定义一个防重复提交的自定义注解,再通过AOP的前置通知拦截带有该注解的方法,执行防重复提交逻辑,需要拼接一个唯一的key,如果redis中不存在则代表第一次请求,将这个key存入redis,设置注解类中指定的过期时间,遇到下次重复提交请求,直接抛出对应异常,全局异常处理返回对应信息即可。

需要注意

这个key的生成需要考虑有token和无token情况,同时满足唯一性。

有 token;可以用 token+请求参数,做为唯一值!无 token:可以用请求路径+请求参数,做为唯一值!

实现步骤

1.定义防重复提交注解

import java.lang.annotation.*;

import java.util.concurrent.TimeUnit;

/**

* @author mijiupro

*/

@Inherited

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface RepeatSubmit {

/**

* 锁定时间,默认5000毫秒

*/

int interval() default 5000;

/**

* 锁定时间单位,默认毫秒

*/

TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

/**

* 提示信息

*/

String message() default "不允许重复提交,请稍后再试!";

}

2.编写一个切面去发现该注解然后执行防重复提交逻辑

因为缓存的key有拼接请求参数,所以遇到文件类型的参数需要进行过滤,拼接逻辑以及参数过滤方法都在下面代码中。

import cn.hutool.crypto.SecureUtil;

import cn.hutool.json.JSONUtil;

import com.mijiu.commom.aop.annotation.RepeatSubmit;

import com.mijiu.commom.exception.GeneralBusinessException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.stereotype.Component;

import org.springframework.validation.BindingResult;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import org.springframework.web.multipart.MultipartFile;

import java.util.Collection;

import java.util.Map;

import java.util.Objects;

/**

* @author mijiupro

*/

@Aspect

@Component

@Slf4j

public class RepeatSubmitAspect {

private final StringRedisTemplate redisTemplate;

public RepeatSubmitAspect(StringRedisTemplate redisTemplate) {

this.redisTemplate = redisTemplate;

}

@Before("@annotation(repeatSubmit)")

public void before(JoinPoint joinPoint, RepeatSubmit repeatSubmit) {

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = null;

if (attributes != null) {

request = attributes.getRequest();

}

//请求参数拼接

String requestParams = argsArrayToString(joinPoint.getArgs());

String authorizationHeader = null;

if (request != null) {

authorizationHeader = request.getHeader("Authorization");

}

String submitKey = null;

if (authorizationHeader != null) {

//如果存在token则通过token+请求参数生成唯一标识

String token = StringUtils.removeStart(authorizationHeader, "Bearer ");

submitKey= SecureUtil.md5(token+":"+requestParams);

} else{

//不存在token则通过请求url+参数生成唯一标识

if (request != null) {

submitKey = SecureUtil.md5(request.getRequestURL().toString()+":"+requestParams);

}

}

//缓存key

String cacheKey = "repeat_submit:"+submitKey;

if (Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey))) {

throw new GeneralBusinessException(repeatSubmit.message());

}

redisTemplate.opsForValue().set(cacheKey, "1", repeatSubmit.interval(), repeatSubmit.timeUnit());

}

/**

* 参数拼接

* @param args 参数数组

* @return 拼接后的字符串

*/

private String argsArrayToString(Object[] args){

StringBuilder params = new StringBuilder();

if(args!= null && args.length > 0){

for(Object o:args){

if(Objects.nonNull(o)&&!isFilterObject(o)){

try {

params.append(JSONUtil.toJsonStr(o)).append(" ");

}catch (Exception e){

log.error("参数拼接异常:{}",e.getMessage());

}

}

}

}

return params.toString().trim();

}

/**

* 判断是否需要过滤的对象。

* @param o 对象

* @return true:需要过滤;false:不需要过滤

*/

private boolean isFilterObject(final Object o) {

Class c = o.getClass();

//如果是数组且类型为文件类型的需要过滤

if(c.isArray()){

return c.getComponentType().isAssignableFrom(MultipartFile.class);

}

//如果是集合且类型为文件类型的需要过滤

else if(Collection.class.isAssignableFrom(c)){

Collection collection = (Collection) o;

for(Object value:collection){

return value instanceof MultipartFile;

}

}

//如果是Map且类型为文件类型的需要过滤

else if(Map.class.isAssignableFrom(c)){

Map map = (Map) o;

for(Object value:map.entrySet()){

Map.Entry entry = (Map.Entry) value;

return entry.getValue() instanceof MultipartFile;

}

}

//如果是文件类型的需要过滤

return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse

|| o instanceof BindingResult;

}

}

3.测试

依赖条件

redis:

Spring Boot3整合Redis_springboot3整合redis-CSDN博客https://blog.csdn.net/qq_62262918/article/details/136067550?spm=1001.2014.3001.5502

全局异常捕获:

Spring Boot3自定义异常及全局异常捕获_全局异常捕获 自定义异常-CSDN博客https://blog.csdn.net/qq_62262918/article/details/136110267?spm=1001.2014.3001.5502

swagger3:

Spring Boot3整合knife4j(swagger3)_springboot3 knife4j-CSDN博客https://blog.csdn.net/qq_62262918/article/details/135761392?spm=1001.2014.3001.5502

hutool工具包:

cn.hutool

hutool-all

5.8.25

1.接口上标记防重复提交注解

随便写个测试接口添加防重复提交注解设置间隔5000毫秒

@PostMapping("/add")

@RepeatSubmit(interval= 5000)

public void test(@RequestBody User user){

//添加用户的操作逻辑。。。

}

2.接口测试

第一次提交

可以看到对应缓存已经存入redis了

5s内第二次提交

写在最后

springboot使用自定义注解+AOP+redis优雅实现防重复提交到这里就结束了,本文介绍了一种通用的防重复提交的实现方式,代码逻辑清晰。任何问题评论区或私信讨论,欢迎指正。

相关文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。