SpringBoot利用注解+AOP实现防止重复提交功能

文章介绍了如何从后端角度解决前端重复点击导致的接口重复处理问题。通过利用AOP和Redis,创建自定义注解,结合IP、URI、Token和参数生成唯一Key,存储在Redis中并设置过期时间,以判断并阻止短时间内重复的请求。此外,文章提供了具体的代码实现,包括注解定义、切面处理和相关工具类的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当前端重复点击按钮提交请求,造成后端接口重复处理。如果是添加操作就可能导致数据库中直接出现多份相同的数据。这种短时间重复提交是必须要避免的。本文将从后端的角度解决这个问题。

解决方案

	利用每次请求的ip,uri,token,参数拼接成一个key,将key放入redis中,设置过期时间。根据ip和token判断是否是同一个用户,根据uri和参数判断是否是同一个请求,当4个都相同便是重复操作。
	当请求来临时,先判断redis中是否存在这个key。
			存在:判定为短时间重复提交,直接返回重复提交的提示信息。
			不存在:将key存入redis中,正常执行代码逻辑。
	对于上述的方案,我将利用Aop技术来实现,同时使用aop注解的形式开发。

代码实现

依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
		<1--aop依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
		<!-- json工具 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
		<!-- 生成token -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

代码实现

自定义注解
@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Documented // 用于生成文档
public @interface RepeatSubmit {
    String value() default "";
    long time() default 1; // 单位:s

}
切面类
/**
 * @author 浩君狐
 * @Description 使用aop技术防止重复请求操作,通过 ip+url+token+参数 拼接成key
 */
@Aspect // 切面注解
@Component // 注册成一个bean
public class RepeatSubmitAspectj {
    @Resource
    private RedisTemplate redisTemplate; // 注入redisTemplate

    private static Logger log = LoggerFactory.getLogger(RepeatSubmitAspectj.class);

    // 切点,通过注解来做切入点
    @Pointcut("@annotation(org.example.aop.aspectj.RepeatSubmit)")
    public void pt(){}

    @Around("pt()") // ProceedingJoinPoint是继承JoinPoint,只能用于环绕通知.
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        // 生成key
        // RequestContextUtil是我自己定义的工具类,底层是通过RequestContextHolder获得当前请求的request对象
        HttpServletRequest request = RequestContextUtil.getRequest();
        // IPUtil也是我自定义的工具类,专门用于获取请求的ip
        String ip = IPUtil.getIpAddr(request);
        String uri = request.getRequestURI();
        // SecurityContextUtil是我自定义工具类,底层是SecurityContextHolder获取的token,我的项目是集成了SpringSecurity,所以有SecurityContextHolder对象
        String token = SecurityContextUtil.getToken();
        // 得到参数
        Object[] args = joinPoint.getArgs();
        // 因为我的项目全是json格式传输,所以只要得到数组的第一个就行
        String params = JSON.toJSONString(args[0]);
        // 拼接key
        StringBuffer key = new StringBuffer();
        key.append(ip);
        key.append(uri);
        key.append(token);
        key.append(params);
		// 判断redis中是否已经存在此请求的key
        if (redisTemplate.hasKey(key.toString())){
	        // 返回错误信息
            return Result.fail(HttpCode.REPEAT_ERROR);
        }

        // 得到注解上设置的超时时间
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
        long time = repeatSubmit.time();
		// 存入redis中
        redisTemplate.opsForValue().set(key.toString(),uri+params,time, TimeUnit.SECONDS);
		// 正常执行
        Object result = joinPoint.proceed();
        return result;
    }
}

将注解加入到需要使用的接口方法上
@RestController
@RequestMapping("/article")
public class ArticleController {
    @Resource
    private ArticleService articleService;

    /**
     * 模糊查找文章
     */
    @PostMapping("fuzzy")
    @RepeatSubmit(time = 1) // 将注解加上
    public Result fuzzy(@RequestBody ArticleFuzzyParam articleFuzzyParam){
        return articleService.fuzzy(articleFuzzyParam);
    }
}

这样注解便可以生效了,便可以实现防止重复提交的功能

其他代码

public class RequestContextUtil {

    // 得到当前请求的request对象
    public static HttpServletRequest getRequest(){
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
        return requestAttributes.getRequest();
    }
}
@Data
public class Result<T> implements Serializable{
    private Integer code;

    private String msg;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;

    public static Result success(){
        Result result = new Result();
        result.setCode(HttpCode.success.getCode());
        result.setMsg(HttpCode.success.getMsg());
        result.setData(null);
        return result;
    }

    public static<T> Result success(T data){
        Result result = new Result();
        result.setCode(HttpCode.success.getCode());
        result.setMsg(HttpCode.success.getMsg());
        result.setData(data);
        return result;
    }

    public static Result fail(Integer code,String msg){
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
    public static Result fail(HttpCode httpCode){
        return fail(httpCode.getCode(),httpCode.getMsg());
    }

}

public class SecurityContextUtil {


    public static String getToken(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) return null;
        String token = (String) authentication.getCredentials();
        if (!StringUtils.hasText(token)){
            return null;
        }
        return token;
    }
}
public enum HttpCode {
    success(200,"success"),
    fail(501,"fail"),
    PARAM_ERROR(502, "参数错误"),
    REPEAT_ERROR(520,"请勿重复提交" ),;

    Integer code;
    String msg;

    HttpCode(Integer code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

由于本人能力有限,代码只能写到这一步,如有不足之处,还请大家不吝赐教。如果大家有哪里不懂的地方,也可以私信或者留言,大家一起交流一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值