Spring Boot实现接口幂等

Spring Boot实现接口幂等

1、pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.6</version><relativePath/></parent><groupId>com.example</groupId><artifactId>idempotent_demo</artifactId><version>0.0.1-SNAPSHOT</version><name>idempotent_demo</name><description>idempotent_demo</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--springboot data redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- StringUtils工具类 --><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.5</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.25</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

2、Redis工具类

package com.example.idempotent_demo.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.concurrent.TimeUnit;/*** @author tom* Redis工具类*/
@Slf4j
@Component
public class RedisUtil {private StringRedisTemplate stringRedisTemplate;@Autowiredpublic void setRedisTemplate(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 将key和value存入redis** @param key        redis的key* @param value      redis的value* @param expireTime key过期时间* @return 保存进redis是否成功*/public boolean save(String key, String value, Long expireTime) {try {// 存储Token到Redis,且设置过期时间为5分钟stringRedisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 验证key和value并删除key** @param key   redis的key* @param value redis的value* @return 验证是否成功*/public boolean valid(String key, String value) {// 设置Lua脚本,其中KEYS[1]是key,KEYS[2]是valueString script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);// 执行Lua脚本Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, value));// 根据返回结果判断是否成功成功匹配并删除Redis键值对,若果结果不为空和0,则验证通过if (null != result && result != 0L) {log.info("验证 key={},value={} 成功", key, value);return true;}log.error("验证 key={},value={} 失败", key, value);return false;}
}

3、Token服务类

token 服务,里面主要是两个方法,一个用来创建 token,一个用来验证 token。

package com.example.idempotent_demo.service;import javax.servlet.http.HttpServletRequest;/*** @author tom*/
public interface TokenService {/*** 创建token** @return*/String generateToken();/*** 检验token** @param request* @return*/boolean validToken(HttpServletRequest request);}
package com.example.idempotent_demo.service.impl;import com.example.idempotent_demo.constant.Constant;
import com.example.idempotent_demo.exception.NoTokenException;
import com.example.idempotent_demo.exception.ValidTokenException;
import com.example.idempotent_demo.service.TokenService;
import com.example.idempotent_demo.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;
import java.util.UUID;/*** @author tom*/
@Service
@Slf4j
public class TokenServiceImpl implements TokenService {private RedisUtil redisUtil;@Autowiredpublic void setRedisUtil(RedisUtil redisUtil) {this.redisUtil = redisUtil;}/*** 创建token** @return*/@Overridepublic String generateToken() {// 实例化生成ID工具对象String uuid = UUID.randomUUID().toString();String token = Constant.IDEMPOTENT_TOKEN_PREFIX + uuid;boolean success = redisUtil.save(token, token, 5L);if (success) {log.info("save token {} to redis success", token);return token;}log.error("save token {} to redis fail", token);return null;}/*** 检验token** @param request* @return*/@Overridepublic boolean validToken(HttpServletRequest request) {String token = request.getHeader(Constant.IDEMPOTENT_TOKEN_HEADER);// header中不存在tokenif (StringUtils.isBlank(token)) {log.error("用户未携带token!");throw new NoTokenException();}// 验证token失败if (!redisUtil.valid(token, token)) {log.error("重复提交!");throw new ValidTokenException();}return true;}
}

redis.get(token) 、token.equals 、redis.del(token) 如果这几个操作不是原子,可能导致,高并发下,都get到同

样的数据,判断都成功,继续业务并发执行。这里 redis 使用 lua 脚本完成这个操作:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

package com.example.idempotent_demo.exception;/*** 用户为携带token* @author tom*/
public class NoTokenException extends RuntimeException {public NoTokenException() {super();}
}
package com.example.idempotent_demo.exception;/*** @author* 验证token失败*/
public class ValidTokenException extends RuntimeException{public ValidTokenException(){super();}
}
package com.example.idempotent_demo.util;/*** @author 结果集返回封装*/
public class ResponseResult {/*** 响应业务状态*/private Integer code;/*** 响应消息*/private String msg;/*** 响应中的数据*/private Object data;public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Object getData() {return data;}public void setData(Object data) {this.data = data;}/*** 无参构造方法*/public ResponseResult() {}/*** 全参构造方法** @param code* @param msg* @param data*/public ResponseResult(Integer code, String msg, Object data) {this.code = code;this.msg = msg;this.data = data;}}
package com.example.idempotent_demo.constant;/*** @author tom*/
public class Constant {/*** 存入Redis的Token键的前缀*/public static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";/*** 请求头的token名称*/public static final String IDEMPOTENT_TOKEN_HEADER = "idempotent_token";
}

4、Redis配置

spring:redis:ssl: falsehost: 127.0.0.1port: 6379database: 0timeout: 1000password:lettuce:pool:max-active: 100max-wait: -1min-idle: 0max-idle: 20
server:servlet:encoding:charset: UTF-8

5、自定义注解

自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现

自动幂等。

后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解 ElementType.METHOD 表示它

只能放在方法上,EetentionPolicy.RUNTIME 表示它在运行时。

package com.example.idempotent_demo.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author tom*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

6、拦截器配置

主要的功能是拦截扫描到 AutoIdempotent 注解的方法,然后调用 TokenService 的 validToken方法校验 token

是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

package com.example.idempotent_demo.config;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.idempotent_demo.annotation.AutoIdempotent;
import com.example.idempotent_demo.exception.NoTokenException;
import com.example.idempotent_demo.exception.ValidTokenException;
import com.example.idempotent_demo.service.TokenService;
import com.example.idempotent_demo.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;/*** @author tom*/
@Slf4j
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {private TokenService tokenService;@Autowiredpublic void setTokenService(TokenService tokenService) {this.tokenService = tokenService;}/*** 预处理** @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();// 被AutoIdempotent注解标记的方法AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);if (methodAnnotation != null) {try {// 幂等性校验,校验通过则放行,校验失败则抛出异常,并通过统一异常处理返回友好提示return tokenService.validToken(request);} catch (NoTokenException ex) {log.error("用户未携带token!");returnJson(response, JSON.toJSONString(new ResponseResult(10001, "用户未携带token!", null), SerializerFeature.WriteMapNullValue));return false;} catch (ValidTokenException ex) {log.error("重复提交!");returnJson(response, JSON.toJSONString(new ResponseResult(10002, "重复提交!", null), SerializerFeature.WriteMapNullValue));return false;}}//必须返回true,否则会被拦截一切请求return true;}@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 {}private void returnJson(HttpServletResponse response, String json) {PrintWriter writer = null;response.setCharacterEncoding("UTF-8");response.setContentType("application/json; charset=utf-8");try {writer = response.getWriter();writer.print(json);} catch (IOException e) {e.printStackTrace();} finally {if (writer != null) {writer.close();}}}
}

7、注册拦截器

添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,

这样在容器启动是时候就可以添加进入context中。

package com.example.idempotent_demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** @author*/
@Configuration
public class WebConfiguration implements WebMvcConfigurer {@Resourceprivate AutoIdempotentInterceptor autoIdempotentInterceptor;/*** 添加拦截器** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(autoIdempotentInterceptor);}
}

8、启动类

package com.example.idempotent_demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @author tom*/
@SpringBootApplication
public class IdempotentDemoApplication {public static void main(String[] args) {SpringApplication.run(IdempotentDemoApplication.class, args);}}

9、测试

9.1 生成token

在这里插入图片描述

请求生成了 token。

9.2 redis查看生成的token

在这里插入图片描述

redis 中生成了 token。

9.3 无header请求

在这里插入图片描述

请求需要携带token。

9.4 正常请求

在这里插入图片描述

请求成功。

9.5 再次查看redis

在这里插入图片描述

发现该 token 已经被删除了。

9.5 再次请求

在这里插入图片描述

返回重复请求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/215814.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大创项目推荐 协同过滤电影推荐系统

文章目录 1 简介1 设计概要2 课题背景和目的3 协同过滤算法原理3.1 基于用户的协同过滤推荐算法实现原理3.1.1 步骤13.1.2 步骤23.1.3 步骤33.1.4 步骤4 4 系统实现4.1 开发环境4.2 系统功能描述4.3 系统数据流程4.3.1 用户端数据流程4.3.2 管理员端数据流程 4.4 系统功能设计 …

【软件安装】VMware安装Centos7虚拟机并且设置静态IP,实现Windows和Centos7网络互相访问

这篇文章&#xff0c;主要介绍VMware安装Centos7虚拟机并且设置静态IP&#xff0c;实现Windows和Centos7网络互相访问。 目录 一、VMware安装Centos7 1.1、下载Centos7镜像 1.2、安装Centos7系统 二、设置静态IP地址 2.1、查看虚拟机网络IP 2.2、禁用NetworkManager服务 …

每天五分钟计算机视觉:VGG网络相对于AlexNet网络有哪些不同?

本文重点 在前面的课程中&#xff0c;我们已经学习了VGG网络模型&#xff0c;也学习了AlexNet网络模型&#xff0c;AlexNet模型先于VGG网络模型产生&#xff0c;所以VGG在一定程度上要优于AlexNet模型&#xff0c;二者来看一下&#xff0c;二者究竟有什么不同&#xff1f; 深度…

Qt的坐标系系统 - 3个坐标系,2个变换

参考&#xff1a; https://zhuanlan.zhihu.com/p/584048811https://www.zhihu.com/tardis/zm/art/634951149?source_id1005 小谈Qt的坐标系系统 Qt中有三个坐标系 设备坐标系窗口坐标系逻辑坐标系 设备坐标系: 即Device坐标系。也是物理坐标系。即真实的的物理坐标系。 …

给鼠标描述符打上注释防止忘记

static uint8_t g_mouse_hid_desc[] { //通用桌面设备 0x05, 0x01, // USAGE_PAGE (Generic Desktop) //鼠标设备 0x09, 0x02, // USAGE (Mouse) //应用集合 0xa1, 0x01, // COLLECTION (Application) //指针设备 0x09, 0x01, // USAGE (Pointer) //物理集合 0xa1, 0x00, // C…

【Linux】free命令使用

free命令 ​free是指查看当前系统内存的使用情况&#xff0c;它显示系统中剩余及已用的物理内存和交换内存&#xff0c;以及共享内存和被核心使用的缓冲区。 作者 作者&#xff1a;Brian Edmonds。 语法 free [参数] free 命令 -Linux手册页 命令选项及作用 执行令 &am…

【二分查找】【滑动窗口】LeeCode2528:最大化城市的最小电量

作者推荐 【动态规划】【广度优先】LeetCode2258:逃离火灾 本文涉及的基础知识点 二分查找算法合集 滑动窗口 题目 给你一个下标从 0 开始长度为 n 的整数数组 stations &#xff0c;其中 stations[i] 表示第 i 座城市的供电站数目。 每个供电站可以在一定 范围 内给所有城…

Java学习总结

1. Java集合体系框架 java.util中包含 Java 最常用的the collections framework。 Java集合类主要由两个根接口Collection和Map派生出来的。 Collection 接口派生出了三个子接口List、Set、Queue。Map 接口 因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。 …

CDH6.3.2安装

文章目录 [toc]一、CM简介1、ClouderaManager的概念2、ClouderaManager的功能3、ClouderaManager的架构 二、准备清单1、部署步骤2、集群规划3、软件环境准备 三、安装清单1、操作系统iso包2、JDK包3、MySQL包4、CM和CDH包5、部署ansible 四、基础环境准备1、配置网络2、配置ho…

Java项目开发,业务比较复杂如何减少bug

Java项目开发&#xff0c;业务比较复杂如何减少bug 当Java开发工作涉及复杂业务时&#xff0c;可以采取以下方法来减少bug的数量&#xff1a; 1、深入了解业务需求 充分了解业务需求&#xff0c;与业务人员进行充分的沟通和交流&#xff0c;确保对需求的理解正确。在需求分析…

el-collapse 默认展开第一个(实测有效)

<el-collapse accordion v-model"activeCollapse"> <el-collapse-item v-for"(item, index) in assetList" :name"index" :key"item.id" > 我这个是通过循环, 只需要v-model 绑定的值和 name 相等,就可以实现展开 然后就…

重新认识Word——给图、表、公式等自动编号

重新认识Word——给图、表、公式等自动编号 给图增加题注题注失败的情况给图添加“如图xx-xx所示” 给公式插入题注第一步——先加题注第二步——设置两个制表符 解决题注“图一-1”的问题 前面我们已经学习了如何引用多级列表自动编号了&#xff0c;现在我们有第二个问题&…

大数据湖体系规划与建设方案:PPT全文51页,附下载

关键词&#xff1a;大数据解决方案&#xff0c;数据湖解决方案&#xff0c;数据数仓建设方案&#xff0c;大数据湖建设规划&#xff0c;大数据湖发展趋势 一、大数据湖体系规划与建设背景 在传统的企业信息化建设中&#xff0c;各个业务系统通常是独立建设的&#xff0c;导致…

学习笔记10——Mysql的DDL语句

学习笔记系列开头惯例发布一些寻亲消息 链接&#xff1a;https://baobeihuijia.com/bbhj/contents/3/197161.html 数据库创建&#xff1a; CREATE DATABASE books&#xff1b; CREATE DATABASE IF NOT EXISTS books;更改字符集 ALTER DATABASE books CHARACTER SET gbk;库的删…

FFmpeg之AVFilterLink

这个结构体主要是用来link两个filter的,它存在于每个AVFilterContext中 struct AVFilterContext {const AVClass *av_class; ///< needed for av_log() and filters common optionsconst AVFilter *filter; ///< the AVFilter of which this is an inst…

XX.push is not a function

错误通常发生在尝试在非数组类型的变量上使用push方法 问题&#xff1a;定义了数组类型&#xff0c;用push方法一直报错&#xff0c;感觉哪里都没毛病 原因&#xff1a;虽然刚开始定义了数组类型&#xff0c;但可能是因为在代码的某个地方将其重新赋值为了非数组类型的值。 …

【计算机网络基础2】IP地址和子网掩码

1、IP地址 网络地址 IP地址由网络号&#xff08;包括子网号&#xff09;和主机号组成&#xff0c;网络地址的主机号为全0&#xff0c;网络地址代表着整个网络。 广播地址 广播地址通常称为直接广播地址&#xff0c;是为了区分受限广播地址。 广播地址与网络地址的主机号正…

Mybatis-Plus基础之框架基础

文章目录 Mybatis-Plus 框架基础引入 maven 依赖定义实体类&#xff0c;并标注注解定义 Mapper 接口&#xff0c;要求继承自特定父接口使用 MapperScan 注解&#xff0c;扫描 mapper 接口所在位置验证 Mybatis-Plus 框架基础 MyBatis-Plus 是 MyBatis 的一种增强框架&#xff…

C语言常用字符串

目录 1.什么是字符串 2.如何定义字符串 第3和第4定义的区别&#xff1a;3是字符串变量&#xff0c;4是字符串常量&#xff0c;不予许被修改 3.strlen和sizeof的区别 4.地址分配&#xff08;malloc,realloc,free,memset&#xff09; 案例 5.字符串拷贝(strcpy,strncpy) …

kafka创建新topic

创建topic bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic mytopic bin/kafka-topics.sh //bin目录下的.sh --create --bootstrap-server //固定写法 localhost:9092 //ip端口 --replication-fac…