前言:
此刻咱们对“java防重复提交”大致比较重视,我们都需要分析一些“java防重复提交”的相关文章。那么小编同时在网络上搜集了一些有关“java防重复提交””的相关文章,希望看官们能喜欢,各位老铁们快快来学习一下吧!正如本文标题所言,今天我们来聊一聊在Java应用系统中如何防止接口重复提交;简单地讲,这其实就是“重复提交”的话题,本文将从以下几个部分展开介绍:
1.“重复提交”简介与造成的后果
2.“防止接口重复提交”的实现思路
3.“防止接口重复提交”的代码实战
1、“重复提交”简介与造成的后果
对于“重复提交”,想必各位小伙伴都知晓它的意思,简单的理解,它指的是前端用户在间隔很短的时间周期内对同一个请求URL发起请求,导致前端开发者在很短的时间周期内将同一份数据(请求体)提交到后端相同的接口 多次,最终数据库出现多条主键ID不一样而其他业务数据几乎一毛一样的记录;
仔细研究上述整个过程,会发现如果发起的多次请求的时间间隔足够短,即时间趋向于无穷小 时,其过程可以归为“多线程并发导致并发安全”的问题范畴;而对于“并发安全”的话题,debug早在此前自己录制的课程以及之前的文章中介绍过多次了,在此不再赘述;
上述在对“重复提交”的介绍中隐约也提及它所带来的的后果:
(1)数据库DB出现多条一毛一样的数据记录;
(2)如果重复发起的请求足够多、请求体容量足够大,很可能会给系统接口带来极大的压力,导致其出现“接口不稳定”、“DB负载过高”,严重点甚至可能会出现“系统宕机”的情况;
因此,我们需要在一些很可能会出现“重复提交”的后端接口中加入一些处理机制(附注:前端其实也需要配合一同处理的,其处理方式在本文就不做介绍了~);
2、“防止接口重复提交”的实现思路
值得一提的是,绝大部分情况下,只有POST/PUT/DELETE的请求方式才会出现“重复提交”的情况,而对于GET请求方式,只要不是出现人为的意外情况,那么它就具有“幂等性”,谈不上“重复提交”现象的出现,因此,在实际项目中,出现“重复提交”现象比较多的一般是POST请求方式;
而在实际项目开发中,“防止接口重复提交”的实现方式有两类,一类是纯粹的针对请求链接URL的,即防止对同一个URL发起多次请求:此种方式明显粒度过大,容易误伤友军;另一类是针对请求链接URL + 请求体 的,这种方式可以说是比较人性化而且也是比较合理的,而我们在后面要介绍的实现方式正是基于此进行实战的;
为了便于小伙伴理解,接下来我们以“用户在前端提交注册信息”为例,介绍“如何防止接口重复提交”的实现思路,如下图所示为整体的实现思路:
从该图中可以得知,如果当前提交的请求URL已经存在于缓存中,且 当前提交的请求体 跟 缓存中该URL对应的请求体一毛一样 且 当前请求URL的时间戳跟上次相同请求URL的时间戳 间隔在8s 内,即代表当前请求属于 “重复提交”;如果这其中有一个条件不成立,则意味着当前请求很有可能是第一次请求,或者已经过了8s时间间隔的 第N次请求了,不属于“重复提交”了。
3、“防止接口重复提交”的代码实战
照着这个思路,接下来我们将采用实际的代码进行实战,其中涉及到的技术:Spring Boot2.0 + 自定义注解 + 拦截器 + 本地缓存(也可以分布式缓存);
(1)首先,需要自定义一个用于加在需要“防止重复提交”的请求方法上 的注解RepeatSubmit,该注解的定义代码很简单,就是一个常规的注解定义,如下代码所示:
@Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RepeatSubmit {}
之后,是直接创建一个新的控制器SubmitController,并在其中创建一请求方法,用于处理前端用户提交的注册信息 请求,如下代码所示:
@RestController@RequestMapping("submit")public class SubmitController extends BaseController{ //用户注册 @RepeatSubmit @PostMapping("register") public BaseResponse register(@RequestBody RegisterDto dto) throws Exception{ BaseResponse response=new BaseResponse(StatusCode.Success); //log.info("用户注册,提交上来的请求信息为:{}",dto); //将用户信息插入到db response.setData(dto); return response; }}
其中,RegisterDto 为自定义的实体类,代码定义如下所示:
@Datapublic class RegisterDto implements Serializable{ private String userName; private String nickName; private Integer age;}
(2)将注解加上去之后,接下来需要自定义一个拦截器RepeatSubmitInterceptor,用于拦截并获取 加了上述这个注解的所有请求方法的相关信息,包括其请求URL和请求体数据,其核心代码如下所示:
@Componentpublic abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter{ //开始拦截 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod){ HandlerMethod handlerMethod= (HandlerMethod) handler; Method method=handlerMethod.getMethod(); RepeatSubmit submitAnnotation=method.getAnnotation(RepeatSubmit.class); if (submitAnnotation!=null){ //如果是重复提交,则进行拦截,拒绝请求 if (this.isRepeatSubmit(request)){ BaseResponse subResponse=new BaseResponse(StatusCode.CanNotRepeatSubmit); CommonUtil.renderString(response,new Gson().toJson(subResponse)); return false; } } return true; }else{ return super.preHandle(request, response, handler); } } //自定义方法逻辑-判定是否重复提交 public abstract boolean isRepeatSubmit(HttpServletRequest request);}
在这里我们将其定义为抽象类,并自定义一个抽象方法:“判断当前请求是否为重复提交isRepeatSubmit()”,之所以这样做,是因为“判断是否重复提交”可以有多种实现方式,而每种实现方式可以通过继承该抽象类 并 实现该抽象方法 从而将其区分开来,某种程度降低了耦合性(面向接口/抽象类编程);如下代码所示为该抽象类的其中一种实现方式:
/** * 判断是否重复提交,整体的思路: * 获取当前请求的URL作为键Key,暂且标记为:A1,其取值为映射Map(Map里面的元素由:请求的链接url 和 请求体的数据组成) 暂且标记为V1; * 从缓存中(本地缓存或者分布式缓存)查找Key=A1的值V2,如果V2和V1的值一样,即代表当前请求是重复提交的,拒绝执行后续的请求,否则可以继续往后面执行 * 其中,设定重复提交的请求的间隔有效时间为8秒 * * 注意点:如果在有效时间内,如8秒内,一直发起同个请求url、同个请求体,那么重复提交的有效时间将会自动延长 * @author 修罗debug * @date 2020/10/21 8:12 * @link 微信:debug0868 QQ:1948831260 * @blog fightjava.com */@Componentpublic class SameUrlDataRepeatInterceptor extends RepeatSubmitInterceptor{ private static final String REPEAT_PARAMS = "RepeatParams"; private static final String REPEAT_TIME = "RepeatTime"; //防重提交key public static final String REPEAT_SUBMIT_KEY = "Repeat_Submit:"; private static final int IntervalTime = 8; //构建本地缓存,有效时间为8秒钟 private final Cache<String,String> cache= CacheBuilder.newBuilder().expireAfterWrite(IntervalTime, TimeUnit.SECONDS).build(); //真正实现“是否重复提交的逻辑” @Override public boolean isRepeatSubmit(HttpServletRequest request) { String currParams=HttpHelper.getBodyString(request); if (StringUtils.isBlank(currParams)){ currParams=new Gson().toJson(request.getParameterMap()); } //获取请求地址,充当A1 String url=request.getRequestURI(); //充当B1 RepeatSubmitCacheDto currCacheData=new RepeatSubmitCacheDto(currParams,System.currentTimeMillis(),url); //充当键A1 String cacheRepeatKey=REPEAT_SUBMIT_KEY+url; String cacheValue=cache.getIfPresent(cacheRepeatKey); //从缓存中查找A1对应的值,如果存在,说明当前请求不是第一次了. if (StringUtils.isNotBlank(cacheValue)){ //充当B2 RepeatSubmitCacheDto preCacheData=new Gson().fromJson(cacheValue,RepeatSubmitCacheDto.class); if (this.compareParams(currCacheData,preCacheData) && this.compareTime(currCacheData,preCacheData)){ return true; } } //否则,就是第一次请求 Map<String, Object> cacheMap = new HashMap<>(); cacheMap.put(url, currCacheData); cache.put(cacheRepeatKey,new Gson().toJson(currCacheData)); return false; } //比较参数 private boolean compareParams(RepeatSubmitCacheDto currCacheData, RepeatSubmitCacheDto preCacheData){ Boolean res=currCacheData.getRequestData().equals(preCacheData.getRequestData()); return res; } //判断两次间隔时间 private boolean compareTime(RepeatSubmitCacheDto currCacheData, RepeatSubmitCacheDto preCacheData){ Boolean res=( (currCacheData.getCurrTime() - preCacheData.getCurrTime()) < (IntervalTime * 1000) ); return res; }}
该代码虽然看起来有点多,但是仔细研读,会发现其实这些代码 就是笔者在上文中贴出的实现流程图 的具体实现,可以说是将理论知识进行真正的落地实现;
在这里再重复赘述一下,其整体的实现思路为:获取当前请求的URL作为键Key,暂且标记为:A1,其取值为映射Map(Map里面的元素由:请求的链接url 、 请求体的数据、和 请求时的时间戳 三部分组成) 暂且标记为V1;从缓存中(本地缓存或者分布式缓存)查找Key=A1的值V2,如果V2和V1里的请求体数据一样 且 两次请求是在8s内,即代表当前请求是重复提交的,系统将拒绝执行后续的业务逻辑;否则可以继续往后面执行 “将用户信息插入到数据库中” 的业务逻辑;
(3)最后,需要将上述自定义的拦截器加入中系统全局配置中,如下所示:
@Componentpublic class CustomWebConfig implements WebMvcConfigurer{ @Autowired private RepeatSubmitInterceptor submitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(submitInterceptor); }}
运行项目,打开Postman,连续多番进行测试,如下几张图所示:
至此,我们已经采用实际的代码实战实现了“如何防止接口重复提交”的功能,值得一提的是,上述代码在实现过程中,其核心在于缓存组件的搭建;在“重复提交”这一业务场景中,它需要满足两个条件方可发挥作用:一个是可以用于缓存信息,即具有Key - Value的特性;另一个是可以对存储的数据设置过期时间;
在这里笔者采用的是google开发工具类中的CacheBuilder构建本地缓存组件的,感兴趣的小伙伴可以自行搜索相关资料;然而这种实现方式在集群多实例部署的情况下是有问题的,因为CacheBuilder只适用于单一架构体系,所以如果是多实例集群部署的情况,最好用Redis。
(1)文中涉及到的代码已经放在gitee上了,访问链接如下所示,别忘了给个star哦:。
(2)期间如何有任何问题都可以私信debug。
(3)请继续关注“程序员实战基地”,您的关注和转发 就是 debug勤劳写技术文的动力!!!
标签: #java防重复提交 #java防重复提交面试题 #java如何防止订单重复提交 #java对外接口防止重复提交