0


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

⛰️个人主页: 蒾酒

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

🌊山高路远,行路漫漫,终有归途


写在前面

本文介绍了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优雅实现防重复提交到这里就结束了,本文介绍了一种通用的防重复提交的实现方式,代码逻辑清晰。任何问题评论区或私信讨论,欢迎指正。

标签: spring boot redis java

本文转载自: https://blog.csdn.net/qq_62262918/article/details/137483835
版权归原作者 蒾酒 所有, 如有侵权,请联系我们删除。

“springboot3使用自定义注解+AOP+redis优雅实现防重复提交”的评论:

还没有评论