一、先看场景:

  1. 填写完页面表单数据,手抖或者恶意在极短的时间内连续多次调用保存操作,表中出现了业务数据完全重复的数据,只有ID不一样。
  2. 老生常谈的付款操作,正常操作,我们只触发一次扣款操作,即使遇到其他的情况发生了多次扣款,但是也只应该扣款一次。

不同的场景,需要不同的幂等操作方式实现。
今天主要针对,上述第一种场景,通过注解+Redis+aop切面的形式处理。

二、撸码

废话不多说,直接撸码。

定义注解

package com.aida.annotation.common.annotation;import com.aida.annotation.common.aspect.em.VariableProvider;import java.lang.annotation.*;/*** 防止重复提交** @author Mr.SoftRock* @Date 2021/7/13 17:14**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {/*** 参数的提供方式* @return*/VariableProvider variableProvider() default VariableProvider.PATH_VARIABLE;/*** 待校验 属性或者变量名称* @return*/String variableName();/*** 参数变量位置* @return*/int variablePosition() default 0;/*** 要切的资源名称,用于描述接口功能* @return*/String name()  default "";/*** key 前缀* @return*/String prefix() default "";/*** 时间显示* 这个参数,我们可以随便设定,默认单位是 秒* 可以根据不通的业务要求去设定* @return*/int period();}

变量提供方式枚举VariableProvider
这个地方,可以去根据自己的实际业务去扩展。

package com.aida.annotation.common.aspect.em;/*** 变量提供方式** @author Mr.SoftRock* @Date 2021/7/13 17:23**/
public enum VariableProvider {/*** 通过PATH路径提供* url/{p}*/PATH_VARIABLE,/*** 通过请求参数提供* url?p=1*/REQUEST_PARAMETER,/*** 通过请求体提供*/REQUEST_BODY,
}

定义切面类:

package com.aida.annotation.common.aspect;import com.aida.annotation.common.annotation.RepeatSubmit;
import com.aida.annotation.common.aspect.em.VariableProvider;
import com.aida.annotation.common.redis.CommonRedisCache;
import com.aida.annotation.common.utils.ServletUtils;
import com.aida.annotation.support.security.AccountPrincipalUtils;
import com.aida.annotation.support.security.userdetails.AccountPrincipal;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;/*** 1、自定义业务防止重复提交切面 在controller层注入,标记key变量获取的方式和变量名称* 2、本切面主要是用来识别解析得到的key* 3、将获取到的key,根据业务规则去执行相应的处理* 4、如果判断重复操作,直接断言出异常** @author Mr.SoftRock* @Date 2021/7/13 17:28**/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {public final String CACHE_REPEAT_KEY = "repeatSubmitData:";public final String REPEAT_TIME = "repeatTime";public final String REPEAT_PARAMS = "repeatParams";@AutowiredCommonRedisCache redisCache;@Before("@annotation(repeatSubmit)")public void repeatSubmitCheck(JoinPoint joinPoint, RepeatSubmit repeatSubmit) {AccountPrincipal handler = AccountPrincipalUtils.getCurrentHandler();String aopTarget = this.getAopTarget(joinPoint);HttpServletRequest request = ServletUtils.getRequest();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method signatureMethod = signature.getMethod();RepeatSubmit limit = signatureMethod.getAnnotation(RepeatSubmit.class);int period = limit.period();//ImmutableList是一个不可变、线程安全的列表集合,它只会获取传入对象的一个副本。ImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(),"_", limit.name(), "_", handler.getUserId(), request.getRequestURI().replaceAll("/", "_")));//redis key//我们使用 (前缀)+ 用户标识 + 调用url 做redis的key,将防重幂等的粒度缩小。String redisKey = keys.toString();//这个地方,我只使用了其中一种,可以根据自己的实际需求去调整。if (Objects.equals(VariableProvider.REQUEST_BODY, repeatSubmit.variableProvider())) {//如果参数提供者是REQUEST_BODY,则直接按照参数位置获取Object arg = this.getArg(joinPoint.getArgs(), repeatSubmit.variablePosition());if (Objects.isNull(arg)) {log.error(String.format("无法执行重复提交的判断:请求类[%s]切片参数配置错误,无法获取指定位置的参数对象", aopTarget));}Assert.notNull(arg, String.format("无法执行重复提交的判断:请求[%s]切片参数配置有误,无法获取指定位置的参数对象", aopTarget));Assert.isTrue(arg.getClass().getName().equals(repeatSubmit.variableName()), String.format("无法执行重复提交的判断:请求类[%s]切片参数配置错误,所配置的参数类与指定位置的参数类实际不一致", aopTarget));//拿到接口传参String strArg = arg.toString();log.info("切面传参:-->{},redisKey==>{}", strArg, redisKey);Map<String, Object> nowDataMap = new HashMap<>();nowDataMap.put(REPEAT_PARAMS, strArg);nowDataMap.put(REPEAT_TIME, SystemClock.now());Object cacheObject = redisCache.getCacheObject(CACHE_REPEAT_KEY);if (Objects.nonNull(cacheObject)) {Map<String, Object> cacheObjMap = (Map<String, Object>) cacheObject;if (cacheObjMap.containsKey(redisKey)) {Map<String, Object> preDataMap = (Map<String, Object>) cacheObjMap.get(redisKey);boolean result = compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, period);Assert.isTrue(!result, "您提交过快,稍后再试");}}Map<String, Object> cacheMap = new HashMap<>();cacheMap.put(redisKey, nowDataMap);redisCache.setCacheObject(CACHE_REPEAT_KEY, cacheMap, period, TimeUnit.SECONDS);}}/*** 获取切片目标信息** @param joinPoint* @return*/private String getAopTarget(JoinPoint joinPoint) {String method = joinPoint.getSignature().getName();String clazz = joinPoint.getTarget().getClass().getName();return String.join("#", clazz, method);}/*** 根据位置序号获取请求参数** @param args* @param position* @return*/private Object getArg(Object[] args, int position) {if (args == null || position + 1 > args.length) {return null;}return args[position];}/*** 判断参数是否相同*/private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {String nowParams = (String) nowMap.get(REPEAT_PARAMS);String preParams = (String) preMap.get(REPEAT_PARAMS);return nowParams.equals(preParams);}/*** 判断两次间隔时间*/private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int period) {long time1 = (Long) nowMap.get(REPEAT_TIME);long time2 = (Long) preMap.get(REPEAT_TIME);return (time1 - time2) < (period * 1000);}
}

三、验证

1、新建一个controller 调用方法

    @RepeatSubmit(variableProvider = VariableProvider.REQUEST_BODY, variableName = "com.aida.annotation.common.controller.dto.Test", period = 5,name = "testRepeatSubmit", prefix = "repeat")@PostMapping("/repeat")public int testRepeatSubmit(@RequestBody Test test) {return ATOMIC_INTEGER.incrementAndGet();}

2、用到的测试class类对象

package com.aida.annotation.common.controller.dto;import lombok.Data;import java.io.Serializable;
import java.util.List;/*** @author Mr.SoftRock* @Date 2021/7/13 19:28**/
@Data
public class Test {String name;Integer age;List<String> list;Test1 test1;@Datapublic static class Test1 implements Serializable {private static final long serialVersionUID = -4262288319285897072L;String name;Integer age;List<String> list;}
}

3、启动项目,通过postman调用看下效果

在设定的5秒内调用一次,可以正常返回,如下图:

Redis中也存在了对应的key值,如下图:

如果在设定的时间内多次操作,则触发幂等校验,如下图:

总结

幂等性的问题确实是在很多种场景都会需要,实现的方式有很多种,找一种最合适自己的。

JAVA 通过Redis、注解和切面的形式实现接口幂等相关推荐

  1. 使用自定义注解和切面AOP实现Java程序增强

    1.注解介绍 1.1注解的本质 Oracle官方对注解的定义为: Annotations, a form of metadata, provide data about a program that ...

  2. Java基于自定义注解的面向切面的实现

    目的:实现在任何想要切的地方添加一个注解就能实现面向切面编程 自定义注解类 @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retentio ...

  3. Spring中利用java注解声明切面

    Spring中利用java注解声明切面 第一步:确定在Spring的XML文件中包含AOP的命名空间: 第二步:在Spring的XML文件中输入<aop:aspectj-autoproxy/&g ...

  4. java 切面 注解_十、使用注解定义切面

    一.本课目标 掌握使用注解实现AOP的方法 二.使用注解定义切面 2.1简介 AspectJ 面向切面的框架,它扩展了Java语言,定义了AOP语法,能够在编译期提供代码的织入. @AspectJ A ...

  5. Java:annotation注解的简单理解和总结

    Java annotation 注解Annotation 1.Annotation的概述 1.1.定义 1.2.Annotation作用分类 1.3.Annotation 架构 2.Annotatio ...

  6. java annotation list_Java 注解 (Annotation)你可以这样学

    注解语法 因为平常开发少见,相信有不少的人员会认为注解的地位不高.其实同 classs 和 interface 一样,注解也属于一种类型.它是在 Java SE 5.0 版本中开始引入的概念. 注解的 ...

  7. Java连接Redis及操作(一)

    Redis简介 Redis是一个开源的使用ANSI c语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API.它是一种非关系性的数据库.它是以key-val ...

  8. java redis remove_最全的Java操作Redis的工具类

    RedisUtil 当前版本:1.1 增加更全的方法,对以前的部分方法进行了规范命名,请放心替换成新版本. 介绍 最全的Java操作Redis的工具类,使用StringRedisTemplate实现, ...

  9. 彻底搞懂 Java 中的注解 Annotation

    Java注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分.注解对于代码的运行效果没有直接影响. 网络上对注解的解释过于严肃.刻板,这并不是我喜欢的风格.尽管这样的 ...

最新文章

  1. python打包工具哪个好用_python打包工具比较
  2. 制作镜像包时遇到的模块加载错误的问题
  3. mysql -- 死锁
  4. 【JAVA SE】第十五章 ArrayList、LinkedList、HashMap和HashSet
  5. CSS挂马及相应防范方法
  6. python登录跳转_Python模拟登录和登录跳转的参考示例
  7. Facebook 开源 Instagram 的Python 代码静态安全分析工具 Pysa
  8. mysql变红_数据库变成红色紧急
  9. IIS出现 分析器错误消息: 在应用程序级别之外使用注册为 allowDefinition='MachineToApplication' 的节是错误的...
  10. Android中Activity出现与退出的自定义动画
  11. onnx-tensorrt:builtin_op_importers.cpp:628:5: error: ‘IIdentityLayer’ is not a member of ‘nvinfer1’
  12. python协程,asyncIO
  13. BIM族库下载——Revit家用电器族库
  14. mt管理器主题修改教程_微信也可以设置皮肤了!超详细教程和方法!
  15. 斐讯K2 刷breed 再刷 固件 。
  16. Java多线程导出Excel表格, 100w数据量
  17. 2022 基于SpringBoot的API文档管理系统 接口文档管理系统
  18. 神经网络与深度学习:回归问题
  19. 嵌入式设备的容器化App
  20. 主从模式的数据库搭建(主从复制)

热门文章

  1. 阿里“看”AI + “ET大脑”战略启动
  2. html文件转换Excel2016文件,万能文件转换工具(word,excel,powerpiont,PDF,TXT,JPG,HTML互转)...
  3. 婴幼儿体重在线计算机,【婴儿体重计算器在线计算_婴儿体重计算器在线计算专题】- 天鹅到家...
  4. typescript-pdf教程 下载
  5. 微慕-专业WordPress微信小程序
  6. 计算机辅助教学是人工智能应用,人工智能教学论文,关于人工智能存计算机辅助教学中应用相关参考文献资料-免费论文范文...
  7. 一个人的高度,就看闲暇时间做什么
  8. 关于文件的扩展名和区别源文件、目标程序文件、可执行程序文件
  9. foorbar关于flac和WAV文件的蛇皮走位(播放不了flac而可以用酷狗之类的播放)
  10. Python68个内置函数大总结,内置函数的骚操作来了!!