当前位置:   article > 正文

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

自定义注解实现接口幂等

一. 什么是幂等性

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

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

二. 基于拦截器实现
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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
2.3 配置redis
	spring.redis.port=6300
	spring.redis.database=4
	spring.redis.host=10.16.64.30
	spring.redis.password=123456
  • 1
  • 2
  • 3
  • 4
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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
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;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
2.6 自定义注解
@Target(ElementType.METHOD)  //注解作用于方法上
@Retention(RetentionPolicy.RUNTIME) //保留到运行时
public @interface Idempotence {
}
  • 1
  • 2
  • 3
  • 4
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 {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
2.8 配置拦截器
@Configuration
public class WebMvcConfing implements WebMvcConfigurer {

    @Autowired
    IdempotenceInterceptor interceptor;

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
三. 基于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("请求重复处理"); //没有检验通过直接抛出异常
        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
3.2 自定义异常类
public class IdempotenceException extends RuntimeException{

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
3.3 定义全局异常处理类
@RestControllerAdvice
public class GlobalException {

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
3.4 测试
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/495848
推荐阅读
相关标签
  

闽ICP备14008679号