当前位置: 首页 > news >正文

【SpringBoot9】HandlerInterceptor拦截器的使用 ——防重复提交

看本篇博客前应当先看完前面三篇,这一篇是基于前面三篇的知识点的整合。所以很多重复的代码这里就不写出了
后台通过拦截器和redis实现防重复提交,避免因为网络原因导致多次请求同时进入业务系统,导致数据错乱,也可以防止对外暴露给第三方的接口在业务尚未处理完的情况下重复调用。

首先引入fastjson

<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.35</version>
</dependency>

 新增一个幂等校验的注解

package com.xxx.util.core.annotation;import javax.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@NameBinding
public @interface Idempotent
{/*** 是否把body数据用来计算幂等key。如果没有登录信息,请设置这个值为true。主要用于第三方接入。** @return*/boolean body() default false;/*** body里的哪些字段用来计算幂等key。body()为true时才有生效。如果这个为空,则计算整个body。主要用于第三方接入。<br/>* <p>* 字段命名规则:<br/>* path: Like xpath, to find the specific value via path. Use :(Colon) to separate different key name or index.* For example:* 	JSON content:* 		{* 			"name": "One Guy",* 			"details": [* 				{"education_first": "xx school"},* 				{"education_second": "yy school"},* 				{"education_third": "zz school"},* 				...* 			],* 			"loan": {"loanNumber":"1234567810","loanAmount":1000000},* 		}** To find the value of "name", the path="name".* To find the value of "education_second", the path="details:0:education_second".* To find the value of "loanNumber"  , the path="loan:loanNumber".* To find the value of "name" and "loanNumber"  , the path="name","loan:loanNumber".** @return*/String[] bodyVals() default {};/*** idempotent lock失效时间,in milliseconds。一些处理时间较长或者数据重复敏感的接口,可以适当设置长点时间。** @return*/int expiredTime() default 60000;}

默认不去读取body中的内容去做幂等,可以@Idempotent(body = true) 将body设为true开启

实现拦截器

package com.xxx.core.filter;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.xxx.common.exception.FastRuntimeException;
import com.xxx.core.annotation.Idempotent;
import com.xxx.core.filter.request.HttpHelper;
import com.xxx.core.filter.request.RequestReaderHttpServletRequestWrapper;import com.xxx.util.core.utils.SpringContextUtil;
import com.xxx.util.redis.SimpleLock;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import redis.clients.jedis.JedisCluster;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;public class IdempotentFilter extends HandlerInterceptorAdapter {private final Logger logger = LoggerFactory.getLogger(IdempotentFilter.class);private static final String IDEMPOTENT = "idempotent.info";private static final String NAMESPACE = "idempotent";private static final String NAMESPACE_LOCK = "idempotent.lock";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {logger.info("request请求地址path[{}] uri[{}]", request.getServletPath(),request.getRequestURI());HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();Idempotent ra = method.getAnnotation(Idempotent.class);if (Objects.nonNull(ra)) {logger.debug("Start doIdempotent");int liveTime = getIdempotentLockExpiredTime(ra);String key = generateKey(request, ra);logger.debug("Finish generateKey:[{}]",key);JedisCluster jedisCluster = getJedisCluster();//上分布式锁 避免相同的请求同时进入调用jedisCluster.get(key) 都为null的情况new SimpleLock(NAMESPACE_LOCK + key,jedisCluster).wrap(new Runnable() {@Overridepublic void run() {//判断key是否存在,如存在抛出重复提交异常,如果不存在 则新增if (jedisCluster.get(key) == null){jedisCluster.setex(key,liveTime,"true");request.setAttribute(IDEMPOTENT, key);}else {logger.debug("the key exist : {}, will be expired after {} mils if not be cleared", key, liveTime);throw new FastRuntimeException(20001,"请勿重复提交");}}});}return true;}private int getIdempotentLockExpiredTime(Idempotent ra){return ra.expiredTime();}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {try{//业务处理完成 删除redis中的keyafterIdempotent(request);}catch (Exception e){// ignore it when exceptionlogger.error("Error after @Idempotent", e);}}private void afterIdempotent(HttpServletRequest request) throws IOException{Object obj = request.getAttribute(IDEMPOTENT);if (obj != null){logger.debug("Start afterIdempotent");String key =  obj.toString();JedisCluster jedisCluster = getJedisCluster();if (StringUtils.isNotBlank(key) && jedisCluster.del(key) == 0){logger.debug("afterIdempotent error Prepared to delete the key:[{}] ",key);}logger.debug("End afterIdempotent");}}/*** generate key** @param request* @param ra* @return*/public String generateKey(HttpServletRequest request, Idempotent ra){String requestURI = request.getRequestURI();String requestMethod = request.getMethod();StringBuilder result = new StringBuilder(NAMESPACE);String token = request.getHeader("H-User-Token");append(result, requestURI);append(result, requestMethod);append(result, token);appendBodyData( request, result, ra);logger.debug("The raw data to be generated key: {}", result.toString());return DigestUtils.sha1Hex(result.toString());}private void appendBodyData(HttpServletRequest request,  StringBuilder src,Idempotent ra){if (Objects.nonNull(ra)){boolean shouldHashBody = (boolean) ra.body();logger.debug("Found attr for body in @Idempotent, the value is {}", shouldHashBody);if (shouldHashBody){String data = null;try {data = HttpHelper.getBodyString(new RequestReaderHttpServletRequestWrapper(request));} catch (IOException e) {logger.warn("Found attr for body in @Idempotent, but the body is blank");return;}if (StringUtils.isBlank(data)){logger.warn("Found attr for body in @Idempotent, but the body is blank");return;}String[] bodyVals = ra.bodyVals();// bodyVals优先if (Objects.nonNull(bodyVals) && bodyVals.length != 0){logger.debug("Found attr for bodyVals in @Idempotent, the value is {}", Arrays.asList(bodyVals));final String finalData = data;Arrays.asList(bodyVals).stream().sorted().forEach(e -> {String val = getEscapedVal(finalData, e);append(src, val);});}else{append(src, data);}}}}private String getEscapedVal(String json, String path){String[] paths = path.split(":");JSONObject jsonObject = null;JSONArray jsonArray = null;String nodeVal = json;for (String fieldName : paths){if (isInteger(fieldName)){try {jsonArray = JSONObject.parseArray(nodeVal);nodeVal= jsonArray.get(Integer.parseInt(fieldName)).toString();} catch (JSONException e) {//如果无法转为jsonArray 则说明不是数组尝试转为jsonObject去取值logger.warn("getEscapedVal JSONObject.parseArray error nodeVal:[{}] fieldName:[{}]",nodeVal,nodeVal);jsonObject = JSONObject.parseObject(nodeVal);nodeVal = jsonObject.get(fieldName).toString();}}else {jsonObject = JSONObject.parseObject(nodeVal);nodeVal = jsonObject.get(fieldName).toString();}}return nodeVal;}public static boolean isInteger(String str) {Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");return pattern.matcher(str).matches();}private void append(StringBuilder src, String str){if (!StringUtils.isBlank(str)){src.append("#").append(str);}}//手动注入public JedisCluster getJedisCluster() {return SpringContextUtil.getBean(JedisCluster.class);}
}

新建SpringContextUtil工具类

package com.xxx.util.core.utils;import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;@Component
public class SpringContextUtil implements ApplicationContextAware {private static ApplicationContext applicationContext; // Spring应用上下文环境public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {SpringContextUtil.applicationContext = applicationContext;}public static ApplicationContext getApplicationContext() {return applicationContext;}@SuppressWarnings("unchecked")public static <T> T getBean(String name) throws BeansException {return (T) applicationContext.getBean(name);}@SuppressWarnings("unchecked")public static <T> T getBean(Class<?> clz) throws BeansException {return (T) applicationContext.getBean(clz);}
}

使用方式异常简单,如果可以根据请求头的内容做区分是否重复提交则直接使用@Idempotent
即可,如果是提供给第三方的接口 请求头无法哦按段需要指定body则@Idempotent(body = true,bodyVals = {“loan:loanNumber”})即可

 

  • 案例代码如下
  • 	@Idempotent(body = true,bodyVals = {"loan:loanNumber"})@PostMapping(Urls.Test.V1_ADD)@ResponseBody@ApiOperation(value = Urls.UserProfiles.V1_GET_USER_PROFILES_BY_PAGE_DESC)public Response add(@RequestBody Test test) {return null;}
    

相关文章:

【SpringBoot9】HandlerInterceptor拦截器的使用 ——防重复提交

看本篇博客前应当先看完前面三篇&#xff0c;这一篇是基于前面三篇的知识点的整合。所以很多重复的代码这里就不写出了 后台通过拦截器和redis实现防重复提交&#xff0c;避免因为网络原因导致多次请求同时进入业务系统&#xff0c;导致数据错乱&#xff0c;也可以防止对外暴露…...

内网渗透(五十八)之域控安全和跨域攻击-约束性委派攻击

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…...

Linux僵尸进程理解作业详解

1 下面有关孤儿进程和僵尸进程的描述&#xff0c;说法错误的是&#xff1f; A.孤儿进程&#xff1a;一个父进程退出&#xff0c;而它的一个或多个子进程还在运行&#xff0c;那么那些子进程将成为孤儿进程。 B.僵尸进程&#xff1a;一个进程使用fork创建子进程&#xff0c;如果…...

每日一题——L1-078 吉老师的回归(15)

L1-078 吉老师的回归 曾经在天梯赛大杀四方的吉老师决定回归天梯赛赛场啦&#xff01; 为了简化题目&#xff0c;我们不妨假设天梯赛的每道题目可以用一个不超过 500 的、只包括可打印符号的字符串描述出来&#xff0c;如&#xff1a;Problem A: Print "Hello world!&qu…...

ESP32设备驱动-DS1264数字温度传感器驱动

DS1264数字温度传感器驱动 1、DS1264介绍 DS1624 由两个独立的功能单元组成:一个 256 字节非易失性 E2 存储器和一个直接数字温度传感器。 非易失性存储器由 256 字节的 E2 存储器组成。 该存储器可用于存储用户希望的任何类型的信息。 这些内存位置通过 2 线串行总线访问。…...

8000+字,就说一个字Volatile

简介 volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制&#xff1a;同步块&#xff08;或方法&#xff09;和 volatile 变量&#xff0c;相比于synchronized&#xff08;synchronized通常称为重量级锁&#xff09;&#xff0c;volatile更轻量级&…...

MySQL的函数

Java知识点总结&#xff1a;想看的可以从这里进入 目录3.3、MySQL的函数3.3.1、字符串函数3.3.2、数学函数3.3.3、聚合函数3.3.4、日期函数3.3.5、条件判断函数3.3.6、系統信息函数3.3.7、其他函数3.3、MySQL的函数 MySQL提供了丰富的内置函数&#xff0c;这些函数使得数据的维…...

python排序算法

排序是指以特定格式排列数据。 排序算法指定按特定顺序排列数据的方式。 最常见的排序是数字或字典顺序。 排序的重要性在于&#xff0c;如果数据是以分类方式存储&#xff0c;数据搜索可以优化到非常高的水平。 排序也用于以更易读的格式表示数据。 下面来看看python中实现的5…...

【C++入门第二期】引用 和 内联函数 的使用方法及注意事项

前言引用的概念初识引用区分引用和取地址引用与对象的关系引用的特性引用的使用场景传值和引用性能比较引用和指针的区别内联函数内联函数的概念内联函数的特性前言 本文主要学习的是引用 及 内联含函数&#xff0c;其中的引用在实际使用中会异常舒适。 引用的概念 概念&…...

数据结构——顺序表讲解

作者&#xff1a;几冬雪来 时间&#xff1a;2023年2月25日 内容&#xff1a;数据结构顺序表内容讲解 目录 前言&#xff1a; 顺序表&#xff1a; 1.线性表&#xff1a; 2.什么是顺序表&#xff1a; 3.顺序表的概念和构成&#xff1a; 4.顺序表的书写&#xff1a; 1…...

Redis 主从复制-服务器搭建【薪火相传/哨兵模式】

Redis 安装参考文章&#xff1a;Centos7 安装并启动 Redis-6.2.6 注意&#xff1a;本篇文章操作&#xff0c;不能在 静态IP地址 下操作&#xff0c;必须是 动态IP地址&#xff0c;否则最后主从服务器配置不成功&#xff01; 管道符查看所有redis进程&#xff1a;ps -ef|grep re…...

数据库|(五)分组查询

&#xff08;五&#xff09;分组查询1. 介绍2. 语法3. 简单分组函数2. 添加筛选条件3. 添加复杂的筛选条件4. 分组查询特点5. 按表达式或函数分组6. 按多个字段分组7. 分组查询添加排序1. 介绍 引入&#xff1a;查询每个部门的平均工资 -- 以前写法&#xff1a;求的是总平均工…...

Orin安装ssh、vnc教程

文章目录一&#xff1a;ssh远程终端的配置PC的配置MobaXterm的下载二&#xff1a;VNC Viewer远程图形界面终端配置&#xff1a;PC配置&#xff1a;一&#xff1a;ssh远程 终端的配置 1.ifconfig查看终端ip地址 其中的eth是网口&#xff0c;我们需要看的是wlan0下的inet&#…...

Allegro如何快速删除孤立铜皮操作指导

Allegro如何快速删除孤立铜皮操作指导 在做PCB设计的时候,铺铜是常用的设计方式,在PCB设计完成之后,需要删除PCB上孤立的铜皮,即铜皮有网络但是却没有任何连接 如下图 通过Status报表也可以看到Isolated shapes 如何快速地删除孤立铜皮,具体操作如下 点击Shape...

从单管单色到单管RGB,这项MicroLED工艺不可忽视

微显示技术商Porotech&#xff0c;在CES 2023期间展示了最新的MicroLED显示模组。近期&#xff0c;AR/VR光学领域的知名博主Karl Guttag深度分析了该公司的微显示技术&#xff0c;并指出Porotech带来了他见过最有趣的MicroLED技术。Guttag表示&#xff1a;Porotech是本届CES上给…...

6-Java中新建一个文件、目录、路径

文章目录前言1-文件、目录、路径2-在当前路径下创建一个文件3-在当前路径下创建一个文件夹&#xff08;目录&#xff09;3.1 测试1-路径已经存在3.2 测试2-路径不存在3.2 创建不存在的路径并新建文件3.3 删除已存在的文件并新建4-总结前言 学习Java中如何新建文件、目录、路径…...

Bootstrap系列之Flex布局

文章目录Bootstrap中的Flexd-flex与d-inline-flex也存在响应式变化flex水平布局flex垂直布局flex水平与垂直也存在响应式变化内容排列&#xff08;justify-content响应式变化也存在于这里sm&#xff0c;md&#xff0c;lg&#xff0c;xl&#xff09;子元素对齐方式Align items&a…...

匈牙利算法与KM算法的区别

前记 在学习过程中&#xff0c;发现很多博客将匈牙利算法和KM算法混为一谈&#xff0c;当时只管用不管分析区别&#xff0c;所以现在来分析一下两个算法之间的区别。 匈牙利算法在二分图匹配的求解过程中共两个原则&#xff1a; 1.最大匹配数原则 2.先到先得原则 而KM算法求…...

You Only Need 90K Parameters to Adapt Light 论文阅读笔记

这是BMVC2022的论文&#xff0c;提出了一个轻量化的局部全局双支路的低光照图像质量增强网络&#xff0c;有监督。 思路是先用encoder f(⋅)f(\cdot)f(⋅)转到raw-RGB域&#xff0c;再用decoder gt(⋅)g_t(\cdot)gt​(⋅)模拟ISP过程转到sRGB域。虽然文章好像没有明确指出&…...

【vue2小知识】实现axios的二次封装

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;在vue2中实现axios的二次封装 目录 一、平常axios的请求发送方式 二、axios的一次封装…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…...

React 第五十五节 Router 中 useAsyncError的使用详解

前言 useAsyncError 是 React Router v6.4 引入的一个钩子&#xff0c;用于处理异步操作&#xff08;如数据加载&#xff09;中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误&#xff1a;捕获在 loader 或 action 中发生的异步错误替…...

SciencePlots——绘制论文中的图片

文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了&#xff1a;一行…...

UDP(Echoserver)

网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法&#xff1a;netstat [选项] 功能&#xff1a;查看网络状态 常用选项&#xff1a; n 拒绝显示别名&#…...

系统设计 --- MongoDB亿级数据查询优化策略

系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log&#xff0c;共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题&#xff0c;不能使用ELK只能使用…...

postgresql|数据库|只读用户的创建和删除(备忘)

CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...

Spring Boot面试题精选汇总

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)

在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马&#xff08;服务器方面的&#xff09;的原理&#xff0c;连接&#xff0c;以及各种木马及连接工具的分享 文件木马&#xff1a;https://w…...

基于SpringBoot在线拍卖系统的设计和实现

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...

云原生安全实战:API网关Kong的鉴权与限流详解

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关&#xff08;API Gateway&#xff09; API网关是微服务架构中的核心组件&#xff0c;负责统一管理所有API的流量入口。它像一座…...