多场景自定义注解防重提交实战

DBC 195 0

一、java核心知识-自定义注解

  • Annotation(注解)
    • 从JDK 1.5开始, Java增加了对元数据(MetaData)的支持,也就是 Annotation(注解)。
    • 注解其实就是代码里的特殊标记,它用于替代配置文件
    • 常见的很多 @Override@Deprecated
  • 什么是元注解
    • 注解的注解,比如当我们需要自定义注解时
    • 会需要一些元注解(meta-annotation),如@Target@Retention
  • java内置4种元注解
    • @Target 表示该注解用于什么地方
      • ElementType.CONSTRUCTOR 用在构造器
      • ElementType.FIELD 用于描述域-属性上
      • ElementType.METHOD 用在方法上
      • ElementType.TYPE 用在类或接口上
      • ElementType.PACKAGE 用于描述包
    • @Retention 表示在什么级别保存该注解信息
      • RetentionPolicy.SOURCE 保留到源码上
      • RetentionPolicy.CLASS 保留到字节码上
      • RetentionPolicy.RUNTIME 保留到虚拟机运行时(最多,可通过反射获取)
    • @Documented 将此注解包含在 javadoc 中
    • @Inherited 是否允许子类继承父类中的注解
  • @interface
    • 用来声明一个注解,可以通过default来声明参数的默认值
    • 自定义注解时,自动继承了java.lang.annotation.Annotation接口
    • 通过反射可以获取自定义注解

二、AOP+自定义注解接口防重提交多场景设计

  • 防重提交方式
    • token令牌方式
    • ip+类+方法方式
  • 利用AOP
    • Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能
    • AOP思想把功能分两个部分,分离系统中的各种关注点
    • 好处
      • 减少代码侵入,解耦
      • 可以统一处理横切逻辑
      • 方便添加和删除横切逻辑
  • 业务流程
  • 多场景自定义注解防重提交实战插图

三、完整代码

RepeatSubmit

package net.dbc.annotation;
import java.lang.annotation.*;
/**
 * 自定义防重提交
 * @author DBC
 * @version 1.0.0
 * @date 2023年01月19日 14:20:13
 * @website dbc655.top
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {


    /**
     * 防重提交,支持两种,一个是方法参数,一个是令牌
     */
    enum Type { PARAM, TOKEN }

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


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

}

RepeatSubmitAspect

package net.dbc.aspect;
import lombok.extern.slf4j.Slf4j;
import net.dbc.annotation.RepeatSubmit;
import net.dbc.constant.RedisKey;
import net.dbc.enums.BizCodeEnum;
import net.dbc.exception.BizException;
import net.dbc.interceptor.LoginInterceptor;
import net.dbc.utils.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;

/**
 * 定义一个切面类
 * @author DBC
 * @version 1.0.0
 * @date 2023年01月19日 14:34:11
 * @website dbc655.top
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {


    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 定义 @Pointcut注解表达式,
     * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
     * 方式二:execution:一般用于指定方法的执行
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }


    /**
     * 环绕通知, 围绕着方法执行
     *
     * @param joinPoint
     * @param repeatSubmit
     * @return
     * @throws Throwable
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * <p>
     * 方式一:单用 @Around("execution(* net.dbc.controller.*.*(..))")可以
     * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
     * <p>
     * <p>
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * <p>
     * 方式二:先请求获取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-%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){
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT);
        }


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

        Object obj = joinPoint.proceed();

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

        return obj;

    }


}
温馨提示

好了,到这里就可以实现了,使用方法例子如下,代码中注释十分的详细[aru_17]

多场景自定义注解防重提交实战插图2

发表评论 取消回复
表情 图片 链接 代码

分享