接口的幂等性设计 一

1 幂等性概念

        在Java领域,我们有时候或者大多时侯都要保证接口幂等。那么什么是幂等呢?简单的来说就是防止重复提交数据或者重复对接口的调用。这在金融领域或者电商领域显得尤为重要。比如一笔订单我们要保证不能重复提交。当前前端也可以做部分的限制,但是我们应该在后端做相应的处理,以保证我们的数据操作符合业务逻辑。

1.1表单重复提价问题

        rpc远程调用时候 发生网络延迟 可能有重试机制

        MQ消费者幂等(保证唯一)一样

2 基于Redis+Token幂等

这也就是我为什么这里为什么是接口的幂等设计一的原因啦。实现的方式当然是多种的。比如乐观锁机制等。这里我们使用了唯一Token的方式来实现。流程如下:

                                                  

说明下流程:

        1 假设前端准备提交支付按钮

        2 此时准备提交表单数据,并且表单存在Token.

        3 后台校验Token,并且此时删除Redis的Token

        4 处理逻辑

        5 当点击重复提交的时候,此时的Token还是之前的Token

        6 后端发现Redis中无该Token,表示重复提交,直接返回!

3 代码实现

 

3.1 注解封装

ExtAPIIdempotent form表单提交封装

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtAPIIdempotent {
    String value();
}

ExtAPIToken Token封装

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtAPIToken {
​
}

3.2 AOP

AOP作用就是拦截到所有的标明上述注解的方法,在跳转到表单的时候设置Token以及表单提交的时候校验Token。

@Component
@Aspect
public class ExtApiAopIdemComponent {
​
    @Autowired
    private RedisTokenComponent redisToken;
    /**
     * 作用的类:切点
     */
    @Pointcut("execution(public * com.gosaint.idempotency.controller.*.*(..))")
    public void rlAop(){
    }
​
    /**
     * 前置通知转发Token参数  进行拦截的逻辑
     * @param joinPoint
     */
    @Before("rlAop()")
    public void before(JoinPoint joinPoint){
        MethodSignature signature = (MethodSignature )joinPoint.getSignature();
        //查询所有的方法上面有Token注解的
        ExtAPIToken apiToken = signature.getMethod().getDeclaredAnnotation(ExtAPIToken.class);
        if(apiToken!=null){
            /**
             * 如果存在Token注解
             * (1)从redis中获取Token,然后存储到request请求头里面
             */
            apiToken();
        }
    }
​
    /**
     * 环绕通知参数验证
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("rlAop()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature )proceedingJoinPoint.getSignature();
        //查询所有的方法上面有Token注解的
        ExtAPIIdempotent extAPIIdempotent =
                signature.getMethod().getDeclaredAnnotation(ExtAPIIdempotent.class);
        if(extAPIIdempotent!=null){
            //有注解的情况 有注解的说明需要进行token校验
            return extAPIIdempotent(proceedingJoinPoint, extAPIIdempotent);
        }
        //如果没有注解。直接放行执行逻辑
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }
​
    private Object extAPIIdempotent(ProceedingJoinPoint proceedingJoinPoint, ExtAPIIdempotent apiIdempotent) throws Throwable {
        HttpServletRequest request = getRequest();
        String valueType = apiIdempotent.value();
        if (StringUtils.isEmpty(valueType)) {
            response("参数错误!");
            return null;
        }
        String token=null;
        //如果存在header中 从头中获取
        if(valueType.equals(ConstantUtils.EXTAPIHEAD)){
             token = request.getHeader("token");
        }else {
            //否则从请求参数中获取
            token=request.getParameter("token");
        }
        if (StringUtils.isEmpty(token)) {
            response("参数错误!");
            return null;
        }
        boolean isToken = redisToken.checkToken(token);
        if(!isToken){
            response("请勿重复提交!");
            return null;
        }
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }
​
    private void response(final String msg) throws IOException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = attributes.getResponse();
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        try {
            writer.println(msg);
        } catch (Exception e) {
        } finally {
            writer.close();
        }
    }
​
    private void apiToken() {
        getRequest().setAttribute("token",redisToken.getToken());
    }
​
    public HttpServletRequest getRequest(){
        ServletRequestAttributes requestAttributes = 
                (ServletRequestAttributes )RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        return request;
    }
}

3.3 Controller

Controller
public class IdempotencyController {
​
    private static final Logger logger =  LoggerFactory.getLogger(IdempotencyController.class);
​
    /**
     * 页面测试
     * @param model
     * @return
     * 统一设置Token
     */
    @RequestMapping("/idemo")
    @ExtAPIToken
    public ModelAndView ideoFormvertify(Model model){
        ModelAndView modelAndView=new ModelAndView();
        modelAndView.setViewName("order");
        return modelAndView;
    }
​
    @RequestMapping(value = "/idemo/trans")
    @ResponseBody
    @ExtAPIIdempotent(value = "form")
    public String addUserPage(HttpServletRequest request) {
        return "添加成功!";
    }
​
}

3.4 表单

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Token幂等</title>
</head>
<body>
<form action="/idemo/trans" method="post">
    <input type="hidden" id="token" name="token" value="${token}">
    name: <input id="name" name="name" />
    <p>age:  <input id="age" name="age" />
    <p><input type="submit" value="submit" />
</form>
</body>
<script type="text/javascript">
    var value = document.getElementById("token").value;
    alert(value);
</script>
</html>

Github地址https://github.com/gosaintmrc/idempotency

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值