【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的一次封装…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
ubuntu系统文件误删(/lib/x86_64-linux-gnu/libc.so.6)修复方案 [成功解决]
报错信息:libc.so.6: cannot open shared object file: No such file or directory: #ls, ln, sudo...命令都不能用 error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory重启后报错信息&…...
