导入依赖
<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
1、定义一个防止提交的注解
import java.lang.annotation.*;
/**
* 防止重复提交的注解:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {
/**
* 延时时间 在延时多久后可以再次提交,默认为20
*
* @return Time unit is one second
*/
int delaySeconds() default 20;
}
2、新建一个“锁”,通过对参数进行加锁和放锁来防止重复提交;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 防止重复提交的锁
*/
@Slf4j
public final class ResubmitLock {
/**
* 新建一个用于存放key的容器,容量为20;
*/
private static final ConcurrentHashMap<String, Object> LOCK_CACHEMAP = new ConcurrentHashMap<>(200);
/**
* 新建一个可定时线程池:
* - 核心线程数为5;
* - 设置任务数超过线程池容量以及任务队列的容量时的处理程序,这里是默默丢弃掉新来的任务,并抛出一个RejectedExecutionHandler拒绝处理异常
*/
private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());
private ResubmitLock() {
}
/**
* 单例模式,保证同一时间内只生成一个锁的实例;
* ——这里采用了静态内部类的方式;
* @return
*/
private static class SingletonInstance {
private static final ResubmitLock INSTANCE = new ResubmitLock();
}
public static ResubmitLock getInstance() {
return SingletonInstance.INSTANCE;
}
// 对参数进行md5加密:
public static String handleKey(String param) {
return DigestUtils.md5Hex(param == null ? "" : param);
}
/**
* 加锁:
* putIfAbsent 是原子操作,保证线程安全
* putIfAbsent在放入数据时,如果存在重复的key,那么putIfAbsent不会放入值,会返回存在的value,不进行替换
* @param key 对应的key
* @param value
* @return
*/
public boolean lock(final String key, Object value) {
//如果之前不存在该key,才会将该key和value存储起来,并返回true,
// 如果之前存在该key,这里会返回false;
return Objects.isNull(LOCK_CACHEMAP.putIfAbsent(key, value));
}
/**
* 延时释放锁, 用以控制指定时间内的重复提交
*
* @param lock 是否需要解锁
* @param key 对应的key
* @param delaySeconds 延时时间
*/
public void unLock(final boolean lock, final String key, final int delaySeconds) {
if (lock) {
//EXECUTOR.schedule(),执行定时任务;
EXECUTOR.schedule(() -> {
LOCK_CACHEMAP.remove(key);
}, delaySeconds, TimeUnit.SECONDS);
}
}
}
3、定义切面,完成环绕增强的逻辑,在里边进行加锁和放锁来实现防止重复提交:
import com.alibaba.fastjson.JSONObject;
import com.yscz.bs.bean.RespBean;
import lombok.extern.log4j.Log4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 数据重复提交校验
**/
@Log4j
@Aspect
@Component
public class ResubmitDataAspect {
private final static String DATA = "data";
//因为key容器是一个Map类型,所以PRESENT是作为一个僵尸value、用于存储key的;
private final static Object PRESENT = new Object();
/**
* 处理重复提交的方法:
* @param joinPoint 连接点对象
* Proceedingjoinpoint 继承了 JoinPoint,是在JoinPoint的基础上暴露出 proceed 这个方法。
* proceed很重要,这个是aop代理链执行的方法。
* 环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的;
* 暴露出proceed这个方法,就能支持 aop:around 这种切面,就能走代理链中的增强方法;
* (而其他的几种切面只需要用到JoinPoint,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关),
* 建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。
* @return
* @throws Throwable
*/
@Around("@annotation(Resubmit)") //环绕增强
public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
//获取此连接点对象上的加@Resubmit防止重复提交注解的方法:
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取注解信息,比如延迟时间:
Resubmit annotation = method.getAnnotation(Resubmit.class);
int delaySeconds = annotation.delaySeconds();
Object[] pointArgs = joinPoint.getArgs();
String key = "";
//获取用户传进来的(连接点对象的)第一个参数
Object firstParam = pointArgs[0];
if (firstParam instanceof RespBean) {
//解析参数
JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
//获取到该参数的数据值:
JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));
if (data != null) {
StringBuffer sb = new StringBuffer();
data.forEach((k, v) -> {
sb.append(v);
});
//对该参数的数据值进行加密,使用了content_MD5的加密方式
key = ResubmitLock.handleKey(sb.toString());
}
}
//对该参数值执行加锁
boolean isLock = false;
try {
//如果是第一次提交,那么key容器内还没有该key参数,则返回true,加锁成功;
isLock = ResubmitLock.getInstance().lock(key, PRESENT);
if (isLock) {
//放行,进行AOP代理链中的下一个增强方法的调用
return joinPoint.proceed();
}
//如果是第二次第三次等重复提交,此时key容器内已有该key参数,则返回false,加锁失败;
else {
//抛出重复提交异常
return RespBean.error("重复提交。。");
}
} finally {
//设置解锁key和解锁时间,到时间了自动从key容器内移除该key;
ResubmitLock.getInstance().unLock(isLock, key, delaySeconds);
}
}
}
4、使用@Resubmit注解,防止重复提交:
@RestController
public class TestController {
@PostMapping("/test")
@Resubmit(delaySeconds = 10) //加了防止重复提交的注解
public ResponBean saveOrder(Map mapper) {
// TODO
return new ResponBean(); //模拟返回结果
}
}