⛰️个人主页: 蒾酒
🔥系列专栏:《spring boot实战》
目录
写在前面
实现思路
实现步骤
1.定义防重复提交注解
2.编写一个切面去发现该注解然后执行防重复提交逻辑
3.测试
依赖条件
1.接口上标记防重复提交注解
2.接口测试
写在最后
本文介绍了springboot开发后端服务中,防重复提交功能的设计与实现,坚持看完相信对你有帮助。
同时欢迎订阅springboot系列专栏,持续分享spring boot的使用经验。
通过定义一个防重复提交的自定义注解,再通过AOP的前置通知拦截带有该注解的方法,执行防重复提交逻辑,需要拼接一个唯一的key,如果redis中不存在则代表第一次请求,将这个key存入redis,设置注解类中指定的过期时间,遇到下次重复提交请求,直接抛出对应异常,全局异常处理返回对应信息即可。
需要注意
这个key的生成需要考虑有token和无token情况,同时满足唯一性。
- 有 token;可以用 token+请求参数,做为唯一值!
- 无 token:可以用请求路径+请求参数,做为唯一值!
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 "不允许重复提交,请稍后再试!"; }
因为缓存的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; } }
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-all5.8.25
随便写个测试接口添加防重复提交注解设置间隔5000毫秒
@PostMapping("/add") @RepeatSubmit(interval= 5000) public void test(@RequestBody User user){ //添加用户的操作逻辑。。。 }
第一次提交
可以看到对应缓存已经存入redis了
5s内第二次提交
springboot使用自定义注解+AOP+redis优雅实现防重复提交到这里就结束了,本文介绍了一种通用的防重复提交的实现方式,代码逻辑清晰。任何问题评论区或私信讨论,欢迎指正。