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

本文主要是介绍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 {@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;@Autowiredprivate 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,参数形式防重提交 TODOlong 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,令牌形式防重提交 TODOString 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);}}

这篇关于Spring自定义注解防重提交方案(参数形式Token令牌)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/844912

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

电脑找不到mfc90u.dll文件怎么办? 系统报错mfc90u.dll丢失修复的5种方案

《电脑找不到mfc90u.dll文件怎么办?系统报错mfc90u.dll丢失修复的5种方案》在我们日常使用电脑的过程中,可能会遇到一些软件或系统错误,其中之一就是mfc90u.dll丢失,那么,mf... 在大部分情况下出现我们运行或安装软件,游戏出现提示丢失某些DLL文件或OCX文件的原因可能是原始安装包

电脑显示mfc100u.dll丢失怎么办?系统报错mfc90u.dll丢失5种修复方案

《电脑显示mfc100u.dll丢失怎么办?系统报错mfc90u.dll丢失5种修复方案》最近有不少兄弟反映,电脑突然弹出“mfc100u.dll已加载,但找不到入口点”的错误提示,导致一些程序无法正... 在计算机使用过程中,我们经常会遇到一些错误提示,其中最常见的就是“找不到指定的模块”或“缺少某个DL

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.