在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和自定义注解的防重提交实现方式,不仅适用于支付场景,还可以应用于其他需要防重提交的场景,如表单提交、数据导入等。
761

被折叠的 条评论
为什么被折叠?



