SpringBoot实现接口等幂次校验的示例代码

 更新时间:2022年01月12日 10:04:13   作者:one_smail  
本文主要介绍了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,"校验成功");
    }
}

到此这篇关于SpringBoot实现接口等幂次校验的示例代码的文章就介绍到这了,更多相关SpringBoot实现接口等幂次校验 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深入浅出讲解Java集合之Collection接口

    深入浅出讲解Java集合之Collection接口

    这篇文章主要介绍了深入浅出讲解Java集合之Collection接口,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • 如何使用BigDecimal实现Java开发商业计算

    如何使用BigDecimal实现Java开发商业计算

    这篇文章主要介绍了如何使用BigDecimal实现Java开发商业计算,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • Spring MVC 请求参数绑定实现方式

    Spring MVC 请求参数绑定实现方式

    Spring MVC 是一个用于构建 Web 应用程序的框架,它提供了一种方便的方式来处理 HTTP 请求和响应,Spring MVC 提供了多种方式来实现请求参数绑定,本文结合实例代码给大家介绍的非常详细,需要的朋友跟随小编一起看看吧
    2023-09-09
  • idea 隐藏target,iml等不需要展示的文件(推荐)

    idea 隐藏target,iml等不需要展示的文件(推荐)

    这篇文章主要介绍了idea 隐藏target,iml等不需要展示的文件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Java SpringMVC 异常处理SimpleMappingExceptionResolver类详解

    Java SpringMVC 异常处理SimpleMappingExceptionResolver类详解

    这篇文章主要介绍了SpringMVC 异常处理SimpleMappingExceptionResolver类详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-09-09
  • Spring学习JdbcTemplate数据库事务参数

    Spring学习JdbcTemplate数据库事务参数

    这篇文章主要为大家介绍了Spring学习JdbcTemplate数据库事务参数使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • SpringBoot使用@PostConstruct注解导入配置方式

    SpringBoot使用@PostConstruct注解导入配置方式

    这篇文章主要介绍了SpringBoot使用@PostConstruct注解导入配置方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java实战之基于I/O流设计的图书管理系统

    Java实战之基于I/O流设计的图书管理系统

    这篇文章主要介绍了Java实战之基于I/O流设计的图书馆管理系统,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • java 使用ImageIO.writer从BufferedImage生成jpeg图像遇到问题总结及解决

    java 使用ImageIO.writer从BufferedImage生成jpeg图像遇到问题总结及解决

    这篇文章主要介绍了java 使用ImageIO.writer从BufferedImage生成jpeg图像遇到问题总结及解决的相关资料,需要的朋友可以参考下
    2017-03-03
  • Java实现创建Zip压缩包并写入文件

    Java实现创建Zip压缩包并写入文件

    这篇文章主要为大家详细介绍了Java实现创建Zip压缩包并写入文件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01

最新评论