springboot3使用自定义注解+AOP+redis优雅实现防重复提交

06-01 1102阅读

 springboot3使用自定义注解+AOP+redis优雅实现防重复提交

⛰️个人主页:     蒾酒

🔥系列专栏:《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博客springboot3使用自定义注解+AOP+redis优雅实现防重复提交https://blog.csdn.net/qq_62262918/article/details/136067550?spm=1001.2014.3001.5502

    全局异常捕获:

    Spring Boot3自定义异常及全局异常捕获_全局异常捕获 自定义异常-CSDN博客springboot3使用自定义注解+AOP+redis优雅实现防重复提交https://blog.csdn.net/qq_62262918/article/details/136110267?spm=1001.2014.3001.5502

    swagger3:

    Spring Boot3整合knife4j(swagger3)_springboot3 knife4j-CSDN博客springboot3使用自定义注解+AOP+redis优雅实现防重复提交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.接口测试

    第一次提交

    springboot3使用自定义注解+AOP+redis优雅实现防重复提交

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

    springboot3使用自定义注解+AOP+redis优雅实现防重复提交

    5s内第二次提交

    springboot3使用自定义注解+AOP+redis优雅实现防重复提交

    写在最后

    项目模板已开源。

    开源地址:
    springboot3x-template: 本项目为单体架构spring boot3x版本的web后端服务开发模板,整合常用依赖,以及起步功能,适合中作为小项目的工程初始化脚手架。大幅度提高开发效率。 (gitee.com)springboot3使用自定义注解+AOP+redis优雅实现防重复提交https://gitee.com/mi9688-wine/springboot-template

    欢迎star任何问题评论区或私信讨论,欢迎指正。

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码