SpringBoot实现接口等幂次校验的示例代码
作者:one_smail 发布时间:2022-01-21 10:49:00
标签:SpringBoot,接口,等幂次,校验
接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功;其目的时防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题。
比如:
订单接口, 不能多次创建订单
支付接口, 重复支付同一笔订单只能扣一次钱
支付宝回调接口, 可能会多次回调, 必须处理重复回调
普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
等等
主流的实现方案如下:
1、唯一索引:给表加唯一索引,该方法最简单,当数据重复插入时,直接报sql异常,对应用影响不大;
alter table 表名 add unique(字段)
示例,两个字段为唯一索引,如果出现完全一样的order_name,create_time就直接重复报异常;
alter table 'order' add unique(order_name,create_time)
2、先查询后判断:入库时先查询是否有该数据,如果没有则插入。否则不插入;
3、token机制:发起请求的时候先去redis获取token,将获取的token放入请求的hearder,当请求到达服务端的时候拦截请求,对请求的hearder中的token,进行校验,如果校验通过则放开拦截,删除token,否则使用自定义异常返回错误信息。
第一步:书写redis工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 判断key是否存在
* @param key 键
* @return
*/
public boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 删除key
* @param key 键
* @return
*/
public Boolean del(String key){
if (key != null && key.length() > 0){
return redisTemplate.delete(key);
}else {
return false;
}
}
/**
* 普通缓存获取
* @param key 键
* @return
*/
public Object get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return
*/
public boolean set(String key,Object value,long time){
try {
if (time > 0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
}
第二步、书写token工具类
import com.smile.project.exception.utils.CodeMsg;
import com.smile.project.exception.utils.CommonException;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
/**
* 使用uuid生成随机字符串,
* 通过md5加密防止token被解密,保证token的唯一性与安全性
* 设置过期时间为30秒,即在30秒内之恶能提交成功一次请求
*/
@Component
public class TokenUtils {
@Autowired
RedisUtils redisUtils;
//token过期时间为30秒
private final static Long TOKEN_EXPIRE = 30L;
private final static String TOKEN_NAME = "token";
/**
* 生成token放入缓存
*/
public String generateToken(){
String uuid = UUID.randomUUID().toString();
String token = DigestUtils.md5DigestAsHex(uuid.getBytes());
redisUtils.set(TOKEN_NAME,token,TOKEN_EXPIRE);
return token;
}
/**
* token校验
*/
public boolean verifyToken(HttpServletRequest request){
String token = request.getHeader(TOKEN_NAME);
//header中不存在token
if (StringUtils.isEmpty(token)){
//抛出自定义异常
System.out.println("token不存在");
throw new CommonException(CodeMsg.NOT_TOKEN);
}
//缓存中不存在
if (!redisUtils.hasKey(TOKEN_NAME)){
System.out.println("token已经过期");
throw new CommonException(CodeMsg.TIME_OUT_TOKEN);
}
String cachToken = (String) redisUtils.get(TOKEN_NAME);
if (!token.equals(cachToken)){
System.out.println("token检验失败");
throw new CommonException(CodeMsg.VALIDA_ERROR_TOKEN);
}
//移除token
Boolean del = redisUtils.del(TOKEN_NAME);
if (!del){
System.out.println("token删除失败");
throw new CommonException(CodeMsg.DEL_ERROR_TOKEN);
}
return true;
}
}
第三步:定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 当控制层的方法上被注释时,表示该请求为等幂性请求
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}
第四步: * 配置。选择前置 * ,每次请求都校验到达的方法上是否有等幂性注解,如果有则进行token校验
import com.smile.project.redis.utils.Idempotent;
import com.smile.project.redis.utils.TokenUtils;
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.lang.reflect.Method;
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenUtils tokenUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)){
return true;
}
//对有Idempotent注解的方法进行拦截校验
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
if (methodAnnotation != null){
//token校验
tokenUtils.verifyToken(request);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
第五步:对 * 进行url模式匹配,并注入spring容器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 对 * 进行url模式匹配,并注入spring容器
*/
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
IdempotentInterceptor idempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求
registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**");
}
}
第六步:控制层
对控制层进行编写,发起请求时通过getToken方法获取token,将获取的token放入hearder后,再请求具体方法。正常请求具体方法的时候注意在hearder中加入token,否则是失败
import com.alibaba.fastjson.JSONObject;
import com.smile.project.exception.utils.CodeMsg;
import com.smile.project.exception.utils.ResultPage;
import com.smile.project.redis.utils.Idempotent;
import com.smile.project.redis.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SmileController {
@Autowired
TokenUtils tokenUtils;
@GetMapping("smile/token")
public ResultPage getToken(){
String token = tokenUtils.generateToken();
JSONObject jsonObject = new JSONObject();
jsonObject.put("token",token);
return ResultPage.success(CodeMsg.SUCCESS,jsonObject);
}
@Idempotent
@GetMapping("smile/test")
public ResultPage testIdempotent(){
return ResultPage.success(CodeMsg.SUCCESS,"校验成功");
}
}
来源:https://blog.csdn.net/qq_40386113/article/details/122407783


猜你喜欢
- Lambda表达式的进化之路为什么要使用Lambda表达式可以简洁代码,提高代码的可读性可以避免匿名内部类定义过多导致逻辑紊乱在原先实现接口
- 大致思路:注解实现方式:就是用 反射机制. 获取指定的包下使用了注解的类,存储在一个map容器, 然后获取map容器下类的属性, 利用反射给
- 本文实例为大家介绍了Android自定义视图属性的方法,供大家参考,具体内容如下1. 自定义一个自己的视图类继承自Viewpublic cl
- 本文主要实现在自定义的ListView布局中加入CheckBox控件,通过判断用户是否选中CheckBox来对ListView的选中项进行相
- SpringBoot打jar包遇到的xml文件丢失在pom.xml的build标签中添加如下内容指定资源路径<resources>
- 开发环境:jdk版本:JDK8maven版本:maven-3.5.2开发工具:Itellij IDEA 2017.1前提条件:已安装以上软件
- 目录“头疼”“吃药”工具代码使用代码“头疼”自己在用Angular做项目时,前端要请求后端数据时的代码如下this.http.get(&qu
- 如何实现?1.)首先实现全屏第一种:继承主题特定主题在Android API 19以上可以使用****.TranslucentDecor**
- 目录1、创建 Android 库2、上传aar包至Maven * 3、其他项目使用4、QA1、创建 Android 库按以下步骤在项目中创建新
- 前言在实际开发中,大多数情况下都需要对 SQL 传入参数以获得想要的结果集,传入的情况分为两种情况:1、SQL语句的拼接,比如表名、like
- 一、需求:标题可能写的不够全部,下面来看下图片,大家就明白是什么意思了。视频与票的图标跟在标题后面显示,当标题过长时icon显示到省略号…后
- 前言本文详细介绍如何使用spring-boot2.x快速整合log4j2日志框架。spring-boot2.x使用logback作为默认日志
- springboot初始化器新建项目项目结构idea工具类中初始化本地git仓库选择当前项目目录即可工具类由VCS变成了Gitadd 到缓存
- 1.POM文件导入Springboot整合websocket的依赖 <depen
- createCoroutine 和 startCoroutine协程到底是怎么创建和启动的?本篇文章带你揭晓。在Continuation.k
- 前言代理模式,我们这里结合JAVA的静态代理和 * 来说明,类比Spring AOP面向切面编程:增强消息,也是代理模式。而我们的静态代理
- HashMap类1、HashMap类概述HashMap是 Map 接口使用频率最高的实现类,允许使用null键和null值,与HashSet
- 一、问题场景使用Logger.error方法时只能打印出异常类型,无法打印出详细的堆栈信息,使得定位问题变得困难和不方便。二、先放出结论Lo
- 需要的jar包:数据库代码:create database school character set utf8;use school;CRE
- 本文实例讲述了Java实现的计时器【秒表】功能。分享给大家供大家参考,具体如下:应用名称:Java计时器用到的知识:Java GUI编程开发