如何优雅地设计一个restful风格的接口

510 阅读5分钟

相信大家或多或少都听过restful这个词,所谓的restful接口指的是一种基于 ​REST(Representational State Transfer,表述性状态转移)​ 架构风格设计的网络服务接口,主要用于实现系统间的高效、可扩展的通信。用人话来说restful接口需要满足:

1、使用HTTP协议指向一个URL资源

2、接口内容使用统一的规范,如XML或者JSON

3、不同的请求方式对应不同的行为

1、以资源为中心

想象所有的资源都是长在树上的一颗果实,每次请求都是从树根出发,经过某个树干,最终找到果实所在的位置。restful接口全部指向一个URL资源,一般采用以下标准:

http(s)://域名:端口[/版本]/资源1[/子资源2/.../子资源n][/路径变量]

举例说明:

查找v1版本的api接口中ID为10的MT报文:

test.com:443/api/v1/msg/…

反例:

test.com:443/api/v1/msg/…

select_id表示一个行为,并不是访问一个资源的路径,同时在接口处暴露了代码逻辑,大大增加了被攻击的风险

2、统一接口

一般我们统一使用XML或者JSON格式的报文,数据只包含基础的信息,不要有展示层相关的信息,接口需要在公司内制定统一宣讲形成规范。

举例说明:

请求采用json格式报文,其中header中需要填写服务编号,商户号等内容,body中填写实际请求的内容,auth用于填写权限校验信息:
{
  header:{
    serviceId : XXX,
    merchNo : YYY
  },
  body: {
    id : 123
  },
  auth: {
    check: d8dmf^8
  }
}

返回报文供养采用json格式,其中code表示返回码,msg表示返回信息,body表示实际返回的内容,实际返回内容中不要用HTML标签等表示层的内容
{
  code : 200,
  msg : success,
  body : {}
}

3、不同请求对应不同的行为

GET: 请求对应的资源

GET test.com/v1/blog/art…
获取v1版本中编号为10的博客文章

POST: 新增对应资源

POST test.com/v1/blog/art…
{id:8, msg:{...文章内容...}}
新增ID为8的博客文章

PUT: 更新对应资源

PUT test.com/v1/blog/art…
{id:8, msg:{...文章内容...}}
更新ID为8的博客文章

Delete: 删除对应资源

DELETE test.com/v1/blog/art…
删除ID为8的博客文章

标准的RESTFUL接口通过不同的请求方式定义了不同的行为,但是日常工作中,有些公司并不是完全按照标准的RESTFUL格式进行接口的定义。比如,统一采用POST请求,在POST请求体中定义请求的行为。无论接口的形式是什么,只要能够方便见名知意,通过接口名大概了解这个接口的作用,形成统一的规范,这就是一个合格的接口,这也是RESTFUL风格接口的重要意义之一。

实际开发中的注意事项

1、注意接口语义一定要保持统一的规范

在我们定义接口时,一定要遵循固定的规范,如返回码字段,0统一表示成功,1XX表示参数异常等。部分公司有专门的返回码和返回信息专题,其中定义了每一位返回码的意义,一定要按照公司统一规范要求团队严格按照语义进行编码

2、保持接口的幂等性

所谓的幂等性,就是说相同的接口无论是只请求1次,还是重复请求多次,最后的结果需要保持一致。 举例说明:

POST test.com:443/api/inward
{payee:10001, amount: 100 }

我们定义一个汇入的接口,表示10001这个账号收到了100块的转入申请,收到请求后需要给这个账户余额增加100块。正常逻辑下这个接口没有什么问题,假设某一天由于网络出现了波动,请求方发出的请求没有收到返回,因此请求方重试再次请求了该接口,接口的接收方实际收到了两次增加100快的请求,预期账户余额增加100,实际增加了200。

解决方案有很多,但大体上都是通过限制每个状态流转的原子性。比如通过数据库乐观锁机制进行解决:

POST test.com:443/api/inward
{payee:10001, amount: 100, version: 100 }

在请求中添加一个版本号字段,这样在数据库中执行更新操作时,每次对余额进行增加的同时,版本号递增,类似以下代码:

update account set amount = amount + 100, version=version+1 where accountId = 10001 and version = 100

这样相同的请求,由于版本号已经更新,不会造成重复的余额增加,保证了幂等性。

3、接口需要采用无状态的设计

image.png

假设客户第一次请求,接口访问的ServerA,提示客户需要登陆。客户登陆后,ServerA保存了客户登陆的信息。第二次客户执行一个查询请求,接口发送到ServerB,由于ServerB中没有客户登陆的信息,又需要让客户再重新登陆一次。

也就是说,我们的接口相当于有一个统一的入口,不会因为具体哪台服务器处理而产生不同的结果,结果这个问题一般有两种方案。

1、客户端保存状态,在接口中附带状态信息
2、服务端能够共享地访问状态

第一种方案典型的例子为JWT方案,客户登陆后,服务端生成一个TOKEN,下次请求客户端将TOKEN附在接口请求中,这样无论是后端哪台服务器收到接口请求,都可以根据TOKEN判断客户是否已经登陆。 第二种方案典型的方式为将Session统一保存在分布式缓存中(如:Redis),这样每台服务器收到请求,都可以从缓存中获取该客户是否已经登陆。

总结

restful接口以资源为导向,可以让我们更方便地梳理业务脉络。各位在实际开发中有遇到关于RESTFUL接口方面的经验和教训,欢迎在评论区交流分享。