SpringAop切面实践-禁止方法重复调用(表单重复提交)

724 15~19 min

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();
}

参考文章
注解声明