在这里插入图片描述

在Web应用中防止用户重复提交表单是一个重要的功能,它不仅可以提升用户体验,还能避免由于重复操作造成的数据冗余或业务逻辑错误。本文将介绍如何通过自定义注解 @RepeatSubmit,结合 Spring AOP 和 Redisson 分布式锁来实现防重复提交功能

一、实现原理

当用户提交请求时,系统会通过一个自定义注解 @RepeatSubmit 来标记需要防重复的方法。然后AOP 切面类 RepeatSubmitAspect 会拦截这些方法的调用。在方法执行之前,它会根据请求信息生成一个唯一的锁键,使用 Redisson 分布式锁来尝试获取这个锁。如果获取成功,说明当前请求是唯一的,方法可以正常执行,如果获取失败,则说明在指定的时间间隔内已经存在相同的请求,系统会阻止重复提交,并返回提示信息给用户。最后,无论方法执行成功与否,都会释放锁,确保后续请求可以正常进行。整个过程利用了 Redisson 的分布式锁特性,确保在分布式环境下也能有效防止重复提交。

二、添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.14.1</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>4.1.19</version>
</dependency>

三、防重复提交注解

package com.luck.cloud.redis.test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 间隔时间,默认3秒
     */
    int interval() default 3000;

    /**
     * 提示消息
     */
    String message() default "请勿重复提交";

    /**
     * 是否根据请求参数生成分布式锁
     * @return
     */
    boolean injectParam() default false;
}

四、防重复提交切面

package com.luck.cloud.redis.test;

import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSONArray;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
import com.luck.cloud.redis.test.RepeatSubmit;

@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    private Redisson redisson;

    @Pointcut("@annotation(repeatSubmit)")
    public void pointCut(RepeatSubmit repeatSubmit) {}

    @Around("pointCut(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String url = request.getRequestURI();
        String message = repeatSubmit.message();
        long interval = repeatSubmit.interval();

        String userId = "自定义";
        String bizKey = url + ":" + userId;
        if(repeatSubmit.injectParam()){
            String params = JSONArray.toJSONString(joinPoint.getArgs());
            bizKey = bizKey + ":" + params;
        }
        String lockKey = SecureUtil.md5(bizKey);
        RLock lock = redisson.getLock(lockKey);
        log.info("获取锁: {}", lockKey);
        try {
            // 尝试获取锁,最多等待指定秒,持有锁10秒钟
            boolean isLocked = lock.tryLock(interval, 10 * 1000, TimeUnit.MILLISECONDS);
            if (isLocked) {
                throw new RuntimeException(message);
            }
            return joinPoint.proceed();
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("锁已释放。");
            }
        }
    }
}

五、使用案例

/**
 * 用户信息Controller
 *
 * @author luck
 * @create 2024-4-10
 **/
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {

    @Autowired
    private IUserService userService;


    /**
     * 新增
     * 添加防重复提交注解
     * @param userVO
     * @return
     * @throws Exception
     */ 
    @PostMapping("/add")
	@RepeatSubmit()
    public ResultVO add(@RequestBody UserVO userVO) {
        User user = ConvertUtils.convert(userVO,User.class);
        userService.insert(user);
        return ResultVO.success(user.getId());
    }
}

六、学习交流

链接: Java-AI学习交流
一个人的精力是有限的,没有那么多的时间去探索新的事物,加入一个交流群就相当于增加了一个接触新事物、新想法、新机会的渠道,这就是交流群存在的意义,期待你的加入!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐