⛰️个人主页: 蒾酒
🔥系列专栏:《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);}}//缓存keyString 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优雅实现防重复提交到这里就结束了,本文介绍了一种通用的防重复提交的实现方式,代码逻辑清晰。任何问题评论区或私信讨论,欢迎指正。