AOP + 自定义注解实现防重提交

在Web应用中,防止用户重复提交表单是一项重要的功能。这不仅可以避免对数据库的不必要压力,还可以提升用户体验。本文将介绍如何使用AOP(面向切面编程)和自定义注解来实现防重提交的功能。

一、背景介绍

在Web应用中,用户可能会因为网络延迟、界面刷新等原因重复提交表单。为了防止这种情况,我们需要在后端实现防重提交的逻辑。本文将介绍一种基于AOP和自定义注解的方法来实现这一功能。

二、自定义注解(RepeatSubmit.java)

首先,我们定义一个自定义注解RepeatSubmit,用于标记需要防重提交的方法。

package net.xdclass.annotation;

import java.lang.annotation.*;

/**
 * 自定义防重提交
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 防重提交,支持两种,一个是参数,一个是令牌
     */
    enum Type { PARAM, TOKEN }

    /**
     * 默认防重提交,是方法参数
     * @return
     */
    Type limitType() default Type.PARAM;

    /**
     * 加锁过期时间,默认是10秒
     * @return
     */
    long lockTime() default 10;
}

三、切面类实现(RepeatSubmitAspect.java)

接下来,我们创建一个切面类RepeatSubmitAspect,用于拦截被RepeatSubmit注解标记的方法,并实现防重提交的逻辑。

package net.xdclass.aspect;

import lombok.extern.slf4j.Slf4j;
import net.xdclass.annotation.RepeatSubmit;
import net.xdclass.constant.RedisKey;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.exception.BizException;
import net.xdclass.interceptor.LoginInterceptor;
import net.xdclass.util.CommonUtil;
import org.apache.commons.lang3.StringUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
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.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 定义一个切面类
 **/

@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * @annotation:当执行的方法上拥有指定的注解时生效
     * execution:一般用于指定方法的执行
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }

    /**
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * 方式一:单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以
     * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        //用于记录成功或者失败
        boolean res = false;

        //防重提交类型
        String type = repeatSubmit.limitType().name();
        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            //方式一,参数形式防重提交

            long lockTime = repeatSubmit.lockTime();

            String ipAddr = CommonUtil.getIpAddr(request);

            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            Method method = methodSignature.getMethod();

            String className = method.getDeclaringClass().getName();

            String key = "order-server:repeat_submit:"+CommonUtil.MD5(String.format("%s-%s-%s",ipAddr,className,method,accountNo));

            //加锁
            //res  = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);

            RLock lock = redissonClient.getLock(key);
            // 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            res = lock.tryLock(0,lockTime, TimeUnit.SECONDS);

        } else {
            //方式二,令牌形式防重提交
            String requestToken = request.getHeader("request-token");
            if (StringUtils.isBlank(requestToken)) {
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
            }

            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);

            /**
             * 提交表的token key
             * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断
             * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
             */
            res = redisTemplate.delete(key);

        }
        if (!res) {
            log.error("请求重复提交");
            return null;
        }

        log.info("环绕通知执行前");

        Object obj = joinPoint.proceed();

        log.info("环绕通知执行后");

        return obj;

    }
}

四、使用示例

在需要防重提交的方法上添加@RepeatSubmit注解。

package net.xdclass.controller;

import net.xdclass.annotation.RepeatSubmit;
import net.xdclass.vo.PayInfoVO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PayController {

    @PostMapping("/pay")
    @RepeatSubmit(type = RepeatSubmit.Type.PARAM, lockTime = 10)
    public String pay(@RequestBody PayInfoVO payInfoVO) {
        // 支付逻辑
        return "支付成功";
    }
}

五、总结

通过自定义注解RepeatSubmit和切面类RepeatSubmitAspect,我们实现了一个简单而有效的防重提交功能。这种方法不仅代码简洁,而且易于维护和扩展。在实际应用中,你可以根据具体需求调整防重提交的策略,如更改锁的类型、过期时间等。

这种基于AOP和自定义注解的防重提交实现方式,不仅适用于支付场景,还可以应用于其他需要防重提交的场景,如表单提交、数据导入等。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值