Spring自定义注解防重提交方案(参数形式&Token令牌)

AOP的介绍和使用

切面作用

利用AOP(面向切面编程),我们可以在不改变原有逻辑的情况下,增加额外的功能。AOP思想将系统的功能分为两个部分,从而分离各种关注点,降低了代码的耦合性,减少了代码侵入性。通过AOP,我们能够统一处理横切逻辑,这使得添加和删除横切逻辑变得更加方便。

AOP里面常见的概念

横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些就叫横切关注点,比如 权限认证、日志、事物。

通知 Advice

在特定的切入点上执行的增强处理做什么? 比如你需要记录日志,控制事务 ,提前编写好通用的模块,需要的地方直接调用,比如重复提交判断逻辑
    @Before前置通知,在执行目标方法之前运行
    @After后置通知,在目标方法运行结束之后
    @AfterReturning返回通知,在目标方法正常返回值后运行
    @AfterThrowing异常通知,在目标方法出现异常后运行
    @Around环绕通知,在目标方法完成前、后做增强处理 ,环绕通知是最重要的通知类型 ,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint,需要手动执joinPoint.procced()

连接点 JointPoint

要用通知的地方,业务流程在运行过程中需要插入切面的具体位置,一般是方法的调用前后,全部方法都可以是连接点。只是概念,没啥特殊

切入点 Pointcut

不能全部方法都是连接点,通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法,在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点,过滤出相应的 Advice 将要发生的joinpoint地方

切面 Aspect

通常是一个类,里面定义切入点+通知, 定义在什么地方; 什么时间点、做什么事情,通知 advice指明了时间和做的事情(前置、后置等),切入点 pointcut 指定在什么地方干这个事情,web接口设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面,对象和对象,方法和方法之间都是一个个切面

目标 target

目标类,真正的业务逻辑,可以在目标类不知情的条件下,增加新的功能到目标类的链路上

织入 Weaving

把切面(某个类)应用到目标函数的过程称为织入

// 目标类
BookOrderService{
    //新增订单;
    addOrder(){};
    //查询订单;
    findOrderById();
    //删除订单;
    deleteOrderById();
    //更新订单
    updateOrder(){};
    
    
}

JoinPoint连接点:addOrder、findOrderById、deleteOrderById、updateOrder;
PointCut切入点:过滤出哪些JoinPoint连接点中哪些函数进行切入;
Advice通知:在切入点的函数上执行的动作,如权限校验,日志记录等等;
Aspect切面:由PointCut切入点和Advice通知组合而成,定义通知应用到哪些切入点;
Weaving织入:把切面的代码,应用到目标函数的过程;

具体代码


/**
 * 定义一个切面类
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 要在哪里执行该方法
     * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
     * 方式二:execution:一般用于指定方法的执行
     */
    @Pointcut("@annotation(repeatSumbit)")
    public void pointCutNoRepeatSubmit(RepeatSumbit repeatSumbit) {

    }

    /**
     * 环绕通知, 围绕着方法执行
     *
     * @param joinPoint
     * @param noRepeatSubmit
     * @return
     * @throws Throwable
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * <p>
     * 方式一:单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以
     * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
     * <p>
     * <p>
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * <p>
     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSumbit noRepeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        //用于记录成功或者失败
        boolean res = false;
        /**
         * 防重提交类型
         */
        String type = noRepeatSubmit.limitType().name();
        if (type.equalsIgnoreCase(RepeatSumbit.Type.PARAM.name())) {
            //方式1,参数形式防重提交 TODO
            long lockTime = noRepeatSubmit.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);
            // 尝试加锁,最多等待2秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            res = lock.tryLock(0, lockTime, TimeUnit.SECONDS);
        } else {
            //方式2,令牌形式防重提交 TODO
            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.error("请求重复提交");
            return null;
        }
        log.info("环绕通知执行前");
        Object obj = joinPoint.proceed();
        log.info("环绕通知执行后");
        return obj;
    }

}

防重提交业务流程

Token令牌校验

下单前获取一个token,使用一次后失效,不可重复使用,对业务有一定侵入性,需在下单业务获取token,并将token存储到页面中,提交订单时,连同token一并提交;

 /**
     * 下单前获取令牌用于防重提交
     * @return
     */
    @GetMapping("token")
    public JsonData getOrderToken() {
        long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        String token = CommonUtil.getStringNumRandom(32);

        String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, token);

        //令牌有效时间是30分钟
        redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()), 30, TimeUnit.MINUTES);

        return JsonData.buildSuccess(token);
    }
@Data
public class ConfirmOrderRequest {


    /**
     * 订单类型
     */
    private Long productId;


    /**
     * 购买数量
     */
    private Integer buyNum;


    /**
     * 终端类型
     */
    private String clientType;
    /**
     * 支付类型,微信-银行-支付宝
     */
    private String payType;

    /**
     * 订单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 订单实际支付价格
     */
    private BigDecimal payAmount;

    /**
     * 防重令牌
     */
    private String token;

    /**
     * 发票类型:0->不开发票;1->电子发票;2->纸质发票
     */
    private String billType;

    /**
     * 发票抬头
     */
    private String billHeader;

    /**
**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数Python工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Python开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**

![img](https://img-blog.csdnimg.cn/img_convert/86622fd41d04d17eab5378dadb7ea102.png)

 

![img](https://img-blog.csdnimg.cn/img_convert/5f066761ba44aedeb6b5f0a0c0b99910.png)

![img](https://img-blog.csdnimg.cn/img_convert/46506ae54be168b93cf63939786134ca.png)

![img](https://img-blog.csdnimg.cn/img_convert/252731a671c1fb70aad5355a2c5eeff0.png)

![img](https://img-blog.csdnimg.cn/img_convert/6c361282296f86381401c05e862fe4e9.png)

![img](https://img-blog.csdnimg.cn/img_convert/9f49b566129f47b8a67243c1008edf79.png)

 

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以扫码获取!!!(备注Python)**

2fe4e9.png)

![img](https://img-blog.csdnimg.cn/img_convert/9f49b566129f47b8a67243c1008edf79.png)

 

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以扫码获取!!!(备注Python)**

<img src="https://img-community.csdnimg.cn/images/fd6ebf0d450a4dbea7428752dc7ffd34.jpg" alt="img" style="zoom:50%;" />
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自定义注解AOP防重提交是一种通过在代码中添加自定义注解的方式来实现防止重复提交的功能。这种方法可以有效地避免代码耦合性过强,提高代码的可读性和可维护性。 具体实现方案可以有多种,以下是几种常见的方案: 1. Token验证:在每次请求中添加一个唯一的Token标识,服务端接收到请求后将Token保存在缓存中,然后进行重复提交的验证。如果同一个Token已经存在于缓存中,则表示该请求已经被处理过,可以拒绝重复提交。 2. 请求参数验证:通过对请求参数进行校验,判断是否已经存在相同的请求参数,如果存在则表示重复提交。可以使用缓存或者数据库来存储已经处理过的请求参数,通过查询来进行重复提交的验证。 3. 时间窗口验证:通过设置一个时间窗口,限制在该时间窗口内只接受一次请求。可以使用缓存或者数据库记录请求的时间戳,每次接收到请求时与最近一次的时间戳进行比对,如果在时间窗口内已经存在过请求,则拒绝重复提交。 以上方案都可以使用Redis作为缓存来进行存储和验证操作。可以通过引入相关的依赖来使用Spring Boot集成的Redis组件和Jedis依赖。 通过使用自定义注解AOP来实现防重提交可以有效地提高代码的可读性和可维护性,同时也能够减轻服务器的负载,避免因为重复提交而导致的服务器宕机等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值