SpringBoot+Redis+注解+拦截器实现接口幂等性校验

目录

一、概念

⼆ 、常见解决方案

三、实现

四、实现思路


一、概念

幂等性, 通俗的说就是⼀个接口 , 多次发起同⼀个请求, 必须保证操作只能执⾏⼀次,⽐如:

  • 单接⼝ , 不能多次创建订单
  • ⽀付接⼝ , 重复⽀付同⼀笔订单只能扣⼀次
  • ⽀付宝回调接⼝ , 可能会多次回调, 必须处理重复回
  • 普通表单提交接⼝ , 因为网络超时等原因多次点击提交, 只能成功⼀次

 、常见解决方案

  • 索引 -- 防止新增脏数据
  • token-- 防止页面重复提交
  • 悲观锁 -- 获取数据的时候加锁(锁表或锁⾏)
  • 乐观锁 -- 基于版本号version实现, 在更新数据那⼀刻校验数据分布式锁 -- redis(jedisredisson)zookeeper实现
  • 状态机 -- 变更, 更新数据时判断状态

、实现

下面采取第2种方式实现,通过redis + token机制实现接口幂等性校验

四、实现思路

为需要保证幂等性的每⼀次请求创建⼀个唯⼀标识token, 先获取token, 将此token存⼊redis, 求接⼝时, 将此token放到header或者作为请求参数请求接⼝ , 后端接⼝判断redis中是否存在token:

  •   如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提⽰
  •   如果不存在, 说明参数不合法或者是重复请求, 返回提示即可

RedisUtils工具类

@Component
public class RedisUtils {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
    * 指定缓存失效时间 *
    * @param key  键
    * @param time 时间(秒)
    * @return
    */
    public boolean expire(String key, long time) { 
        try {
            if (time > 0) {
            redisTemplate.expire(key, time,TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
    * 根据key 获取过期时间 *
    * @param key 键 不能为null
    * @return 时间(秒) 返回0代表为永久有效 */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key,TimeUnit.SECONDS);
    }
    /**
    * 判断key是否存在 *
    * @param key 键
    * @return true 存在 false不存在 */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
    * 删除缓存 *
    * @param key 可以传一个值 或多个 */
    public boolean del(String... key) {
        Boolean flag = true;
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                flag = redisTemplate.delete(key[0]); } else {
                Long aLong =redisTemplate.delete(CollectionUtils.arrayToList(key)); if (aLong <= 0){
        flag = false;
        } 
    }

}
    return flag;
    }

    /**
    * 普通缓存获取 *
    * @param key 键
    * @return 值
    */
    public Object get(String key) {
        return key == null ? null :
        redisTemplate.opsForValue().get(key);
    }

    /**
    * 普通缓存放入 *
    * @param key   键
    * @param value 值
    * @return true成功 false失败 */
    public boolean set(String key, Object value) { try {
        redisTemplate.opsForValue().set(key, value); return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
    * 普通缓存放入并设置时间 *
    * @param key   键
    * @param value 值
    * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
    * @return true成功 false 失败
    */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
        return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

自定义ApIdempotent

/**
* 自定义幂等性接口,在需要使用的方法上标注
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApIdempotent {
}

ApiIdempotentInterceptor拦截器实现对幂等性请求拦截

/**
* 自定义幂等性拦截器,用于校验标注幂等性注解的方法
*/
public class ApiIdempotentInterceptor implements
    HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

/**
* 重写前置拦截
* @param request
* @param response
* @param handler 选择要执行的处理程序,用于类型和或实例评估
* @return true:程序继续执行,false:程序中断执行
* @throws Exception */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws    Exception {
    //如果handler不是处理方法的处理其,返回true,
    if ( !(handler instanceof HandlerMethod)){ 
        return true;
    }
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    //获取拦截的方法
    Method method = handlerMethod.getMethod();
    //如果拦截的方法使用了自定义幂等性接口,校验token
    ApIdempotent apIdempotent =method.getAnnotation(ApIdempotent.class);
    if (apIdempotent != null){
        //如果方法上标注有幂等性注解,则进行幂等性验证
        check(request);
    }
        return true;
}
    //验证token
    private void check(HttpServletRequest request){ tokenService.checkToken(request);
    }

    }

TokenServiceImpl :实现token保存以及验证逻辑

@Service
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private RedisUtils redisUtils;

    //生成token并将其保存到ServerResponse (http响应对象)
    @Override
    public ServerResponse createToken() {
    //随机生成字符串
    String s = RandomUtil.UUID32();
    //拼接生成token值
    StringBuilder token = new StringBuilder(); token.append("token:").append(s);
    //保存token到redis中   此处失效时间根据需求定
    redisUtils.set(token.toString(), token.toString(),60);
    //将token保存并返回
    return ServerResponse.success(token.toString()); }

/**
* 验证token,从请求头中获取token并进行验证
* @param request */
    @Override
    public void checkToken(HttpServletRequest request) {
    //从请求头中获取名为token的数据
    String token = request.getHeader(TOKEN_NAME);
    //如果请求头中token为空
    if (StringUtils.isEmpty(token)) {
        //尝试从请求参数中获取token
        token = request.getParameter(TOKEN_NAME); if (StringUtils.isEmpty(token)) {
        //如果请求参数中也不包含token,抛出非法操作异常
        throw new ServiceException("非法操作异常 ......");
    }
}

    //判断redis中是否包含token
    if ( !redisUtils.hasKey(token)) {
        //如果redis中不包含token,抛出请勿重复操作异常
        throw new ServiceException("请勿重复操作"); }

        //删除token
        boolean del = redisUtils.del(token);
        //如果删除失败,抛出异常;一定要进行删除的判断,不然还会出现 重复提交

        if (!del) {
        throw new ServiceException("请勿重复提交"); }
    }
}

WebConfig  注解拦截器到web容器中

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //设置跨域请求
    @Bean
    public CorsFilter corsFilter(){
        final UrlBasedCorsConfigurationSource
        urlBasedCorsConfigurationSource =  new
        UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration =  new CorsConfiguration();
        corsConfiguration.setAllowCredentials( true);
        corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedHeader("*");                     
        corsConfiguration.addAllowedMethod("*");

        urlBasedCorsConfigurationSource.registerCorsConfiguration ("/**", corsConfiguration);
        return  new
        CorsFilter(urlBasedCorsConfigurationSource);
}

    //添加注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(apiIdempotentInterceptor());                            
        WebMvcConfigurer.super.addInterceptors(registry);
}
    @Bean
    public ApiIdempotentInterceptor
    apiIdempotentInterceptor(){
        return new ApiIdempotentInterceptor();
    }
}

    @RestController
    @RequestMapping("/token")
    public class TokenController {

        @Autowired
        private TokenService tokenService;

        //获取token
        @GetMapping
        public ServerResponse token(){
            return tokenService.createToken();
    }

        //假设此接口访问具有幂等性,添加幂等注解,拦截器会自动拦截进行 token验证
        @PostMapping("/idemt")
        @ApIdempotent
        public ServerResponse testIdept(){
            return ServerResponse.success("success"); }

    }

发起token请求创建并保存token redis

发起/token/idemt请求测试:

携带正确token请求头时:

 

若不携带token或者携带token数据错误,观察控制台异常打印数据!!!!!

注:服务响应实体类

ServerResponse:

/**
* 自定义封装服务返回状态对象
*/
    public class ServerResponse implements Serializable {
        private static final long serialVersionUID = 7498483649536881777L;
        //响应状态
        private Integer status;
        //响应信息
        private String msg;
        //相应数据
        private Object data;

    public ServerResponse() {
}

    public ServerResponse(Integer status, String msg, Object data) {
    this.status = status;
    this.msg = msg;
    this.data = data;
}

    @JsonIgnore
    public boolean isSuccess() {
        return this.status ==
        ResponseCode.SUCCESS.getCode();
}

    public static ServerResponse success() {
        return new
        ServerResponse(ResponseCode.SUCCESS.getCode(), null, null);
}

    public static ServerResponse success(String msg) { 
        return new
    ServerResponse(ResponseCode.SUCCESS.getCode(), msg, null); }

    public static ServerResponse success(Object data) {
        return new
    ServerResponse(ResponseCode.SUCCESS.getCode(), null, data);
    }

    public static ServerResponse success(String msg, Object data) {
        return new
        ServerResponse(ResponseCode.SUCCESS.getCode(), msg, data); }

    public static ServerResponse error(String msg) { return new
        ServerResponse(ResponseCode.ERROR.getCode(), msg, null); }

    public static ServerResponse error(Object data) { return new
        ServerResponse(ResponseCode.ERROR.getCode(), null, data); }

    public static ServerResponse error(String msg, Object data) {
        return new
    ServerResponse(ResponseCode.ERROR.getCode(), msg, data); }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    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;
    }

}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kk.巴扎嘿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值