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

        防重提交通常在需要防止用户重复提交表单或执行某些敏感操作时使用,以确保系统的数据一致性和安全性,本文章集结了通用场景下防重提交(参数形式&Token令牌),采用Java的特性(注解和AOP),配合Redis进行实现,使用方便有效。

注解介绍及使用

什么是注解

        自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接口,通过反射可以获取自定义注解

具体代码

import java.lang.annotation.*;

/**
 * 自定义防重提交
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSumbit {
    /**
     * 防重提交,支持两种,一种是方法参数,一个令牌
     */
    enum Type{PARAM,TOKEN}

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

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

}

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;

    /**
     * 发票内容
     */
    private String billContent;

    /**
     * 发票收票人电话
     */
    private String billReceiverPhone;

    /**
     * 发票收票人邮箱
     */
    private String billReceiverEmail;


}

参数形式

        均在通知中处理,根据方法名|ip|用户id生成摘要作为key,设置过期时间(防止重复提交时间)存储至redis,对业务无侵入。通过设置过期时间,防止在一定时间内重复提交。

 @PostMapping("confirm")
//    @RepeatSumbit(limitType=RepeatSumbit.Type.TOKEN)
    public void confirmOrder(@RequestBody ConfirmOrderRequest orderRequest, HttpServletResponse response) {
        JsonData jsonData = productOrderService.confirmOrder(orderRequest);
        if (jsonData.getCode() == 0)    {
            //端类型
            String clientType = orderRequest.getClientType();
            //支付类型
                String payType = orderRequest.getPayType();
            //如果是支付宝支付,跳转网页,sdk除外
            if (payType.equalsIgnoreCase(ProductOrderPayTypeEnum.ALI_PAY.name())) {

                if (clientType.equalsIgnoreCase(ClientTypeEnum.PC.name())) {
                    CommonUtil.sendHtmlMessage(response, jsonData);
                } else if (clientType.equalsIgnoreCase(ClientTypeEnum.APP.name())) {

                } else if (clientType.equalsIgnoreCase(ClientTypeEnum.H5.name())) {

                }
            } else if (payType.equalsIgnoreCase(ProductOrderPayTypeEnum.WECHAT_PAY.name())) {
                //微信支付
                CommonUtil.sendJsonMessage(response, jsonData);
            }

        } else {
            log.error("创建订单失败{}", jsonData.toString());
            CommonUtil.sendJsonMessage(response, jsonData);

        }

    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/485926.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

后端如何返回404地址

当我们网站输入不存在的地址&#xff0c;经常会出现404的页面&#xff0c;这是如何做到的 1.添加配置 spring:mvc:view:prefix: /templates/suffix: .html 2.resources下添加templates目录&#xff0c;下面放404的网站 3.添加依赖&#xff0c;版本在主pom里面配置好了&#x…

Memcached非关系型数据库介绍

使用背景 Memcached 不是一个数据库&#xff0c;而是一个高性能的分布式内存对象缓存系统。它主要用于减轻数据库负载&#xff0c;提高动态Web应用的速度、可扩展性和性能。Memcached 的工作原理是将数据存储在内存中&#xff0c;以提供快速的数据访问。当应用程序需要访问数据…

夸克、迅雷网盘项目拉新推广去哪对接?推荐几个一手项目渠道!

在进行夸克、迅雷网盘等项目的拉新推广时&#xff0c;对接合适的渠道和平台是至关重要的。本文将分享几个地推、网推一手渠道&#xff0c;帮助您轻松开展拉新推广项目。 1.任推邦 国内知名项目拉新平台&#xff0c;这个平台对接的项目大多是官方直签&#xff0c;截至目前已经…

Facebook账号防封的有效方法附解禁方法

Facebook作为跨境主要业务平台&#xff0c;一直以来封号率都非常高。相信点进来的各位或多或少地遇见了个人号被封&#xff0c;广告账户被禁&#xff0c;FB主页被封等情况。针对此类问题&#xff0c;今天就小编也来分享自己的Facebook防封经验。 一、Facebook被封原因 主要有以…

【电能管理】安科瑞AEM碳排放功能表/电碳表/碳结算/三相嵌入式电表/尖峰平谷峰谷分时/最大需量/二部制电价/节能降碳/CE认证

什么是电碳表!!! 电碳表是一种计量设备&#xff0c;可以帮助用户了解和控制电力使用中的碳排放。原理是根据实际电力系统的计量数据&#xff0c;动态计算并更新电碳因子&#xff08;平均每度电所蕴含的碳排放量&#xff09;&#xff0c;并且这个数据是实时更新的&#xff0c;真…

【云效测试管理】测试用例、测试计划(用例执行)、缺陷管理、测试报告全流程管理

背景 我们公司之前使用过很多测试管理软件&#xff0c;从最开始原始的Excel来管理缺陷&#xff0c;再到Worktitle管理缺陷&#xff0c;再到现在的云效&#xff1b;用例管理管理在本地。 再后来我们转用云效流水线来部署测试环境&#xff0c;开始尝试发掘云效中的“测试管理”…

泛型可空类型Nullable<T>

.Net Framework 4.8版本开始&#xff0c;引入了可空类型Nullable<T>. 对于引用类型的变量来说&#xff0c;如果未赋值&#xff0c;默认情况下是 Null 值&#xff0c; 对于值类型的变量&#xff0c;如果未赋值&#xff0c;整型变量的默认值为 0,Boolean默认为false&…

项目2-用户登录

1.创建项目 2.引入前端代码并检查是否有误 3.定义接口 需求分析 对于后端开发⼈员⽽⾔, 不涉及前端⻚⾯的展⽰, 只需要提供两个功能 1. 登录⻚⾯: 通过账号和密码, 校验输⼊的账号密码是否正确, 并告知前端 2. ⾸⻚: 告知前端当前登录⽤⼾. 如果当前已有⽤⼾登录, 返回登录的账…

【算法与数据结构】总结

目录 引言 一、线性数据结构 1. 1 数组&#xff08;Array&#xff09; 1.2 链表&#xff08;Linked List&#xff09; 1.3 栈&#xff08;Stack&#xff09; 1.4 队列&#xff08;Queue&#xff09; 二、图形数据结构 2.1 深度优先搜索&#xff08;DFS&#xff09;&…

机器学习之线性回归与逻辑回归【完整房价预测和鸢尾花分类代码解释】

目录 前言 一、什么是线性回归 二、什么是逻辑回归 三、基于Python 和 Scikit-learn 库实现线性回归 示例代码&#xff1a; 使用线性回归来预测房价: 四、基于Python 和 Scikit-learn 库实现逻辑回归 五、总结 线性回归的优缺点总结&#xff1a; 逻辑回归&#xff08;Logistic…

数字孪生技术在健康医疗的应用

数字孪生技术在健康医疗领域的应用前景广阔&#xff0c;它通过创建物理实体或工作过程的虚拟版本&#xff0c;为医疗健康领域带来了革命性的变化。以下是数字孪生在医疗健康领域的一些关键应用&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件…

[深度学习]yolov8+pyqt5搭建精美界面GUI设计源码实现一

【简单介绍】 基于YOLOv8与PyQt5的精美界面GUI设计&#xff0c;旨在为用户提供一个直观、易用且功能强大的目标检测平台。通过结合YOLOv8的先进目标检测能力与PyQt5的丰富界面设计元素&#xff0c;我们打造了一款高效、稳定的软件产品。 在界面设计上&#xff0c;我们注重用户…

R语言在气象、水文中数据处理及结果分析、绘图实践技术应用

R 语言是一门由统计学家开发的用于统计计算和作图的语言&#xff08;a Statistic Language developed for Statistic by Statistician&#xff09;&#xff0c;由 S 语言发展而来&#xff0c;以统计分析功能见长。R 软件是一款集成 了数据操作、统计和可视化功能的优秀的开源软…

Java中的代理模式(动态代理和静态代理)

代理模式 我们先了解一下代理模式&#xff1a; 在开发中&#xff0c;当我们要访问目标类时&#xff0c;不是直接访问目标类&#xff0c;而是访问器代理类。通过代理类调用目标类完成操作。简单来说就是&#xff1a;把直接访问变为间接访问。 这样做的最大好处就是&#xff1a…

C++第十一弹---类与对象(八)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、友元 1.1、友元函数 1.2、友元类 2、内部类 3、匿名对象 4、拷贝对象时的一些编译器优化 总结 1、友元 友元提供了一种突破封装的方式&a…

最新Java面试题5【2024初级】

互联网大厂面试题 1&#xff1a;阿里巴巴Java面试题 2&#xff1a;阿里云Java面试题-实习生岗 3&#xff1a;腾讯Java面试题-高级 4&#xff1a;字节跳动Java面试题 5&#xff1a;字节跳动Java面试题-大数据方向 6&#xff1a;百度Java面试题 7&#xff1a;蚂蚁金服Java…

由浅到深认识Java语言(26):阶段性练习

该文章Github地址&#xff1a;https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.c…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之六 简单图像倾斜校正处理效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之六 简单图像倾斜校正处理效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之六 简单图像倾斜校正处理效果 一、简单介绍 二、简单图像倾斜校正处理效果实现原理 三、简单图像倾斜校正…

数据结构——认识二叉树

这是一篇回顾二叉树概念的文章 前言&#xff1a;一、了解树形结构1.2 树的定义2.2 树的相关概念2.2 树的表示形式 二、了解二叉树结构和性质2.1 什么是二叉树&#xff1f;2.2 二叉树的性质2.3 二叉树的遍历2.3 二叉树的应用范围2.5 二叉树的优缺点 三、掌握二叉树的存储结构3.1…

NX二次开发常用函数:UF_MODL_ask_feat_......(二)

最近学习NX二次开发发现有一些函数经常使用&#xff0c;俗话说得好&#xff0c;好记性不如烂笔头&#xff0c;现在做一下笔记&#xff0c;帮助理解。 UF_MODL_ask_feat_......在头文件uf_modl.h中 1、UF_MODL_ask_feat_direction &#xff08;查询特征的方向&#xff09; 概…