SpringAop切面实践-禁止方法重复调用(表单重复提交)
SpringAop切面实践-禁止方法重复调用(表单重复提交)
需求:业务中遇到订单更新方法一个单号被重复调用多次可能是网络原因,也可能是重复提交(调用)遂采用切面的方法结合Redis达到一个锁的作用
创建自定义注解
/**
* 方法重复调用执行验证
*
* @author <a href="mailto:[email protected]">文刀草乙</a>
* @date 2021/11/30 10:10
* @project
* @Title: MethodRepeatInvoke.java
**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodRepeatInvoke {
/**
* Redis-Key前缀
*/
public String keyPreFix() default "";
/**
* Redis-Key后缀Class
*/
public Class<?> keySufFixClass() default Void.class;
/**
* Redis-Key后缀字段
*/
public String keySufFixField() default "";
/**
* Redis-Key的过期时间
*/
public int expireTime() default 3;
/**
* Redis-Key的时间单位
*/
public TimeUnit expireTimeUnit() default TimeUnit.SECONDS;
}
编写切面类
/**
* 方法重复调用校验切面
*
* @author <a href="mailto:[email protected]">文刀草乙</a>
* @date 2021/11/30 10:24
* @return
*/
@Aspect
@Component
@Slf4j
public class MethodRepeatInvokeAspect {
/**
* 注入Redis 放入缓存设置过期时间
*/
@Autowired
private RedisService redisService;
/**
* 以{@link MethodRepeatInvoke} 注解为切点
*
* @param
* @return void
* @author <a href="mailto:[email protected]">刘艺</a>
* @date 2021/11/30 10:41
*/
@Pointcut("@annotation(com.qdd.common.security.annotation.MethodRepeatInvoke)")
public void pointCut() {
}
@Around("pointCut()") // 环绕通知 也可采用前置通知
public Object around(ProceedingJoinPoint point)
throws Throwable
{
// 获取切点注解上的值
MethodRepeatInvoke annotation = this.getAnnotation(point);
String keyPreFix = "", keySufFix = "", redisKey = "";
Object cacheObject = null;
int expireTime = annotation.expireTime();
TimeUnit timeUnit = annotation.expireTimeUnit();
Class<?> keySufFixClass = annotation.keySufFixClass();
keyPreFix = (StringUtils.isBlank(annotation.keyPreFix()) ? getTargetMethod(point).getName() : annotation.keyPreFix());
// 如果注解上没有给出Redis-Key的前缀,选择目标方法的参数Hash吗为RedisKey 反之用给定的key前缀
if (StringUtils.isBlank(annotation.keyPreFix())) {
// 获取方法上的参数 获取后缀值
Object[] args = point.getArgs();
for (Object arg : args) {
Method keySuffix = ReflectUtil.getMethodIgnoreCase(arg.getClass(), "hashCode");
Object invoke = keySuffix.invoke(arg);
redisKey += invoke.toString();
}
cacheObject = redisService.getCacheObject(redisKey);
}
else {
boolean resultEquals = keySufFixClass.getName().equals(Void.class.getName());
if (!resultEquals && StringUtils.isNotBlank(annotation.keySufFixField())) {
// 获取方法上的参数 获取后缀值
Object[] args = point.getArgs();
for (Object arg : args) {
if (arg.getClass().getName().equals(keySufFixClass.getName())) {
Method keySuffix = ReflectUtil.getMethod(arg.getClass(), true, "get" + annotation.keySufFixField());
Object invoke = keySuffix.invoke(arg);
keySufFix = invoke.toString();
break;
}
}
//keySufFix = (String)getGetMethod(keySufFixClass, annotation.keySufFixField());
}
cacheObject = redisService.getCacheObject(keyPreFix + keySufFix);
redisKey = keyPreFix + keySufFix;
}
if (Objects.nonNull(cacheObject)) {
throw new RuntimeException("禁止方法重复调用!");
}
redisService.setCacheObject(redisKey, 1L, Long.parseLong(String.valueOf(expireTime)), timeUnit);
point.proceed();
return null;
}
/**
* 获取切点注解元数据
*
* @param
* @return com.qdd.common.security.annotation.MethodRepeatInvoke
* @author <a href="mailto:[email protected]">文刀草乙</a>
* @date 2021/11/30 10:52
*/
private MethodRepeatInvoke getAnnotation(ProceedingJoinPoint point)
throws NoSuchMethodException
{
Method targetMethod = this.getTargetMethod(point);
//1.3获取目标方法对象上注解中的属性值
//1.2.3 获取方法上的自定义ProcData注解
return targetMethod.getAnnotation(MethodRepeatInvoke.class);
}
/**
* 获取切点上的目标方法
*
* @param point
* @return java.lang.reflect.Method
* @author <a href="mailto:[email protected]">文刀草乙</a>
* @date 2021/11/30 11:20
*/
private Method getTargetMethod(ProceedingJoinPoint point)
throws NoSuchMethodException
{
//目的:获取切入点方法上自定义ProcData注解中procKey属性值
//1.1获取目标对象对应的字节码对象
Class<?> targetCls = point.getTarget().getClass();
//1.2获取目标方法对象
//1.2.1 获取方法签名信息从而获取方法名和参数类型
Signature signature = point.getSignature();
//1.2.1.1将方法签名强转成MethodSignature类型,方便调用
MethodSignature ms = (MethodSignature)signature;
//1.2.2通过字节码对象以及方法签名获取目标方法对象
return targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
}
/**
* 根据属性,获取get方法
*
* @param ob
* @param name
* @return java.lang.Object
* @author <a href="mailto:[email protected]">刘艺</a>
* @date 2021/11/4 14:12
*/
public static Object getGetMethod(Object ob, String name)
throws Exception
{
Method[] m = ob.getClass().getMethods();
for (int i = 0; i < m.length; i++) {
if (("get" + name).equalsIgnoreCase(m[i].getName())) {
return m[i].invoke(ob);
}
}
return null;
}
}
问题总结
- 在定义注解的时候有一个注解元素是
Class<?>默认值可以是Void.Class
public Class<?> keySufFixClass() default Void.class;
判断是不是传入的Class类型可以使用比较Class获取名称判断是否是默认的Class名称
boolean resultEquals = keySufFixClass.getName().equals(Void.class.getName());
- 其他类型声明
public @interface AnnotationElementDemo {
//枚举类型
enum Status {FIXED,NORMAL};
//声明枚举
Status status() default Status.FIXED;
//布尔类型
boolean showSupport() default false;
//String类型
String name()default "";
//class类型
Class<?> testCase() default Void.class;
//注解嵌套
Reference reference() default @Reference(next=true);
//数组类型
long[] value();
}
参考文章
注解声明