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

作者:one_smail 时间:2022-01-21 10:49:00 

接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功;其目的时防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题。

比如:

  • 订单接口, 不能多次创建订单

  • 支付接口, 重复支付同一笔订单只能扣一次钱

  • 支付宝回调接口, 可能会多次回调, 必须处理重复回调

  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
    等等

主流的实现方案如下:

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

标签:SpringBoot,接口,等幂次,校验
0
投稿

猜你喜欢

  • JAVA8 Stream流中的reduce()方法详解

    2023-09-01 21:10:33
  • SpringMVC中@ModelAttribute与@RequestBody的区别及说明

    2023-11-24 12:09:51
  • java,android,MD5加密算法的实现代码(16位,32位)

    2022-07-12 20:40:10
  • 解决Spring Cloud feign GET请求无法用实体传参的问题

    2023-11-17 14:14:05
  • JAVA 内存溢出案例汇总

    2022-02-16 08:49:22
  • SpringBoot 集成 activiti的示例代码

    2023-01-22 10:22:03
  • Java常用数字工具类 数字转汉字(1)

    2022-06-25 11:01:45
  • spring boot集成smart-doc自动生成接口文档详解

    2023-11-28 23:08:02
  • 深入解析java中的locale

    2023-11-09 18:14:20
  • Android 实现永久保存数据的方法详解

    2023-06-23 22:13:23
  • Stream distinct根据list某个字段去重的解决方案

    2022-06-22 22:23:26
  • springboot 文件上传大小配置的方法

    2023-08-02 11:49:16
  • ELK搭建线上日志收集系统

    2021-11-01 17:34:41
  • TransmittableThreadLocal解决线程间上下文传递烦恼

    2023-11-09 17:09:35
  • java数据库唯一id生成工具类

    2023-04-04 22:53:34
  • Android Studio 引用外部依赖时报错的解决方法

    2023-09-26 18:20:11
  • java Semaphore共享锁实现原理解析

    2021-11-02 23:12:38
  • JAVA.io读写文件方式汇总

    2022-10-14 06:32:21
  • 详解Spring框架注解扫描开启之配置细节

    2022-11-01 18:04:14
  • 浅析Java编程中枚举类型的定义与使用

    2021-07-04 23:46:16
  • asp之家 软件编程 m.aspxhome.com