【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拦截器的使用 ——防重复提交
看本篇博客前应当先看完前面三篇,这一篇是基于前面三篇的知识点的整合。所以很多重复的代码这里就不写出了 后台通过拦截器和redis实现防重复提交,避免因为网络原因导致多次请求同时进入业务系统,导致数据错乱,也可以防止对外暴露…...
内网渗透(五十八)之域控安全和跨域攻击-约束性委派攻击
系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…...
Linux僵尸进程理解作业详解
1 下面有关孤儿进程和僵尸进程的描述,说法错误的是? A.孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。 B.僵尸进程:一个进程使用fork创建子进程,如果…...
每日一题——L1-078 吉老师的回归(15)
L1-078 吉老师的回归 曾经在天梯赛大杀四方的吉老师决定回归天梯赛赛场啦! 为了简化题目,我们不妨假设天梯赛的每道题目可以用一个不超过 500 的、只包括可打印符号的字符串描述出来,如:Problem A: Print "Hello world!&qu…...

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

8000+字,就说一个字Volatile
简介 volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级&…...
MySQL的函数
Java知识点总结:想看的可以从这里进入 目录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提供了丰富的内置函数,这些函数使得数据的维…...
python排序算法
排序是指以特定格式排列数据。 排序算法指定按特定顺序排列数据的方式。 最常见的排序是数字或字典顺序。 排序的重要性在于,如果数据是以分类方式存储,数据搜索可以优化到非常高的水平。 排序也用于以更易读的格式表示数据。 下面来看看python中实现的5…...

【C++入门第二期】引用 和 内联函数 的使用方法及注意事项
前言引用的概念初识引用区分引用和取地址引用与对象的关系引用的特性引用的使用场景传值和引用性能比较引用和指针的区别内联函数内联函数的概念内联函数的特性前言 本文主要学习的是引用 及 内联含函数,其中的引用在实际使用中会异常舒适。 引用的概念 概念&…...

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

Redis 主从复制-服务器搭建【薪火相传/哨兵模式】
Redis 安装参考文章:Centos7 安装并启动 Redis-6.2.6 注意:本篇文章操作,不能在 静态IP地址 下操作,必须是 动态IP地址,否则最后主从服务器配置不成功! 管道符查看所有redis进程:ps -ef|grep re…...
数据库|(五)分组查询
(五)分组查询1. 介绍2. 语法3. 简单分组函数2. 添加筛选条件3. 添加复杂的筛选条件4. 分组查询特点5. 按表达式或函数分组6. 按多个字段分组7. 分组查询添加排序1. 介绍 引入:查询每个部门的平均工资 -- 以前写法:求的是总平均工…...

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

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

从单管单色到单管RGB,这项MicroLED工艺不可忽视
微显示技术商Porotech,在CES 2023期间展示了最新的MicroLED显示模组。近期,AR/VR光学领域的知名博主Karl Guttag深度分析了该公司的微显示技术,并指出Porotech带来了他见过最有趣的MicroLED技术。Guttag表示:Porotech是本届CES上给…...

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

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

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

You Only Need 90K Parameters to Adapt Light 论文阅读笔记
这是BMVC2022的论文,提出了一个轻量化的局部全局双支路的低光照图像质量增强网络,有监督。 思路是先用encoder f(⋅)f(\cdot)f(⋅)转到raw-RGB域,再用decoder gt(⋅)g_t(\cdot)gt(⋅)模拟ISP过程转到sRGB域。虽然文章好像没有明确指出&…...

【vue2小知识】实现axios的二次封装
🥳博 主:初映CY的前说(前端领域) 🌞个人信条:想要变成得到,中间还有做到! 🤘本文核心:在vue2中实现axios的二次封装 目录 一、平常axios的请求发送方式 二、axios的一次封装…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
Pinocchio 库详解及其在足式机器人上的应用
Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库,专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性,并提供了一个通用的框架&…...

宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...
人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent
安全大模型训练计划:基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标:为安全大模型创建高质量、去偏、符合伦理的训练数据集,涵盖安全相关任务(如有害内容检测、隐私保护、道德推理等)。 1.1 数据收集 描…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...