自定义注解解决接口幂等性问题

一. 什么是幂等性

简单的说就是对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。
例如:在一次用户的支付订单的操作中,第一次支付时因一些原因显示网络异常,但是后台实际已经扣款了,那么用户支付第二次时,就会给用户显示扣款成功.

实现接口幂等性的设计方案可以有很多种,今天我们就用自定义注解的方式来解决

二. 基于拦截器实现
2.1 实现思路
  1. 自定义注解,在每个需要控制幂等性的接口上加上此注解
  2. 后台提供生成token的接口供前台调用,并在生成时将之存入redis中
  3. 前台每次请求接口时,必须带有从后台拿到的token,每个token只能用一次
  4. 后台在处理前台的一次请求后,将携带的token在redis中删除
  5. 自定义拦截器,对前台的每次请求进行token校验(校验redis中是否有此token),校验成功则处理逻辑,校验失败则不进行任何处理(返回错误信息提示前台)
2.2 引入依赖
 		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
2.3 配置redis
	spring.redis.port=6300
	spring.redis.database=4
	spring.redis.host=10.16.64.30
	spring.redis.password=123456
2.4 封装redis工具类
@Component
public class RedisService {

    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 给key设置过期时间
     *
     * @param key
     * @param value
     * @param time
     * @return
     */
    public boolean setExp(String key, String value, Long time) {

        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        ops.set(key, value);
        Boolean expire = redisTemplate.expire(key, time, TimeUnit.SECONDS);
        if (expire) {
            return true;
        }
        return false;

    }

    /**
     * 判读key是否存在
     *
     * @param key
     * @return
     */
    public boolean exists(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除key
     *
     * @param key
     * @return
     */
    public boolean remove(String key) {
        if (exists(key)) {
            return redisTemplate.delete(key);
        }
        return false;
    }
}
2.5 检验token服务层
@Component
public class TokenService {

    @Autowired
    RedisService redisService;

    /**
     * 创建token 并存入redis
     * @return
     */
    public String createToken() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        if (redisService.setExp(token, token, 1000L)) {
            return token;
        }
        return null;
    }

    /**
     * 校验token
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("token");
            if (StringUtils.isEmpty(token)) {
                return false;
            }
        }

        if (!redisService.exists(token)) {
            return false;
        }

        if (!redisService.remove(token)) {
            return false;
        }
        return true;
    }

}
2.6 自定义注解
@Target(ElementType.METHOD)  //注解作用于方法上
@Retention(RetentionPolicy.RUNTIME) //保留到运行时
public @interface Idempotence {
}
2.7 自定义拦截器
/*
 * @className: IdempotenceInterceptor
 * @description 对有此注解的相关接口作拦截处理,没有此注解的接口直接放行
 * @since JDK1.8
 * @author ljh
 * @createdAt  2020/9/3 0003 
 * @version 1.0.0
 **/
@Component
public class IdempotenceInterceptor implements HandlerInterceptor {

    @Autowired
    TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        Method method = ((HandlerMethod) handler).getMethod();
        Idempotence annotation = method.getAnnotation(Idempotence.class);
        if (annotation != null) {
            if (tokenService.checkToken(request)) {
                return true;
            } else {
                return false;
            }
        }

        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 {

    }
}
2.8 配置拦截器
@Configuration
public class WebMvcConfing implements WebMvcConfigurer {

    @Autowired
    IdempotenceInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

2.9测试
@RestController
public class TestController {

    @Autowired
    TokenService tokenService;

    @Idempotence
    @PostMapping("/test1")
    public String test1(){
        return "111";  //被拦截器拦截作校验处理
    }

    @PostMapping("/test2")
    public String test2(){
        return "222";  //直接返回结果
    }

    @GetMapping("/getToken")
    public String getToken(){
        return tokenService.createToken();
    }
}
三. 基于aop实现

这里就可以直接删除拦截器相关的类了IdempotenceInterceptor和WebMvcConfing

3.1 定义切面
@Component
@Aspect
public class IdempotenceAspect {

    @Autowired
    TokenService tokenService;

    @Pointcut("@annotation(com.cicro.annotation.idempotence.Idempotence)")
    public void pointCut() {

    }

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
    //拿到当前请求request
        HttpServletRequest request =
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        if (!tokenService.checkToken(request)){
            throw new IdempotenceException("请求重复处理"); //没有检验通过直接抛出异常
        }

    }
}
3.2 自定义异常类
public class IdempotenceException extends RuntimeException{

    public IdempotenceException(String message) {
        super(message);
    }
}

3.3 定义全局异常处理类
@RestControllerAdvice
public class GlobalException {

    @ExceptionHandler(IdempotenceException.class)
    public String e(IdempotenceException e) {
        return e.getMessage();
    }

}
3.4 测试
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值