⛰️个人主页: 蒾酒
🔥系列专栏:《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工具包:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
1.接口上标记防重复提交注解
随便写个测试接口添加防重复提交注解设置间隔5000毫秒
@PostMapping("/add")
@RepeatSubmit(interval= 5000)
public void test(@RequestBody User user){
//添加用户的操作逻辑。。。
}
2.接口测试
第一次提交
可以看到对应缓存已经存入redis了
5s内第二次提交
写在最后
springboot使用自定义注解+AOP+redis优雅实现防重复提交到这里就结束了,本文介绍了一种通用的防重复提交的实现方式,代码逻辑清晰。任何问题评论区或私信讨论,欢迎指正。