Restful 接口在平时的开发中非常常见,但是 Restful 接口应该怎么设计各式各样、五花八门。不同的公司(不管大小)甚至可能不同的项目组都不一样。今天从国外的公司,国内的公司、以及自己工作的公司的 Restful接口设计来讲一下自己对设计的理解。
1. 什么是Restful?
REST是具象状态传输(REpresentational State Transfer)的首字母缩写,是分布式超媒体系统的一种架构风格。Roy Fielding于2000年在他的著名论文中首次提出了这一观点。
简单点说就是一种更加优雅架构风格。让使用者更加的舒服。
2. Restful API设计中面临的问题
在开发过程中(以Java项目举例),在搭建好了项目后如何定义API会面临一下几个问题。
2.1 Restful API接口的版本控制
大多数的公司应该都没用版本控制,或者说版本控制比较随便。这也是有原因的:大多时候项目的快速的迭代开发导致API的使用周期可能就是开发到预生产的时间跨度,然后就业务重来了,更不要说几年。这种情况很大情况是有业务导致,重新梳理发现在原有的代码上改造还不如重新写一个接口给你。加版本不存在的,加了版本也没实际意义,因为之前的接口可能再也用不到了(其他项目或者对外也没用到)。
如果有版本管理(以Java Spring 项目为例),版本管理是设置在 Controller 类上面还是放在项目的 server.context-path
@RestController
@RequestMapping("/v1/log")
public class TestController {
@Async
@GetMapping("")
public String log(){
return null;
}
}
server.context-path=/v1/aaa
控制的粒度不一样, Controller 上控制单个功能模块的,而 server.context-path 的修改这是整个项目。
实际工作中这两种都用过,我们公司很多业务变化速度开发都快跟不上了,所以加版本与不加版本两者用起来我也没觉得差别在哪。
2.2 Restful API HTTP方法选择
HTTP方法选择,在很多实际使用过程中都没按照HTTP方法的语义来进行选择。大多数时候是怎么方便怎么来,能够实现业务功能就可以,管你什么语义不语义。在看如何选择HTTP方法之前来看一下不同方法的含义是什么,同时Restful API需要支持哪些方法:
| 方法 | 说明 | 是否幂等 |
|---|---|---|
| GET | GET方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据 | TRUE |
| HEAD | HEAD方法请求一个与GET请求的响应相同的响应,但没有响应体。 | TRUE |
| POST | POST方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用 | FALSE |
| PUT | PUT方法用请求有效载荷替换目标资源的所有(替换一个对象,或者创建一个命名的对象) | TRUE |
| DELETE | DELETE方法删除指定的资源 | TRUE |
| OPTIONS | OPTIONS方法用于描述目标资源的通信选项 | TRUE |
| PATCH | PATCH方法用于对资源应用部分修改 | FALSE |
上表格少了两个方法:CONNECT 和 TRACE 这两个在实际开发中极少用到。同时也不是Restful API 需要支持的
规范:
-
GET与HEAD
两个方法都是用于获取资源,且只能用于获取资源。区别在于GET有响应体,HEAD没有响应体
-
POST与PUT
POST用于创建一个新对象,或者提交一个命令,而PUT是替换一个对象,或者创建一个已命名的对象,POST是非幂等的而PUT是幂等
-
PATCH和PUT
PATH用于更新对象的部分数据,PUT更新对象的全部数据。
根据HTTP方法的不同语义可以知道,不同的情况下Restful接口来选择不同的方法。创建对象就用 POST, 部分更新用PATCH,获取用对象的信息就用GET。
实际过程中遇到过这样的一个问题:根据批量的ID获取对象的信息入这样的请求:
GET http://api.mxsm.com/accounts?ids=1,2,....,n这里ids数据可能会有上千个,最终导致的问题是URI的长度超过规定的最大长度,导致部分数据传不到后台。
实际过程我们为了解决这个问题会把GET方式改为POST方式ID放在payload中
POST http://api.mxsm.com/accounts { "ids":[1,...,n] }这这方式又违背了HTTP 方法的相关语义。
2.3 接口状态码是否复用HTTP状态码
对于状态码的问题绝大多数的Restful接口设计都是请求的接口不论后端是处理正确还是处理错误 HTTP的状态码:200, 返回的数据根据格式如下:
HTTP Status Code : 200
{
"code": "00000",
"data": 0,
"msg": "string",
"traceId": "string"
}
根据不同的错误code的编码不同, data可能存在或者不存在(取决于是否正确返回),msg有没有取决于处理正确与否或者看自己的定义。
另一种方式就是把接口状态码与HTTP状态码相结合:
| HTTP Status Code | 说明 |
|---|---|
| 2XX | 成功的执行。 方法执行可以通过几种方式成功。 这个状态代码指定了它成功的方式。 |
| 4XX | 这些问题通常与请求、请求中的数据、无效的身份验证或授权等有关。 在大多数情况下,客户端可以修改他们的请求并重新提交。 |
| 5XX | 服务器错误:由于站点中断或软件缺陷,服务器无法执行该方法。 5xx范围状态码不应用于验证或逻辑错误处理。 |
HTTP状态码说明:
接口状态码与HTTP状态码相结合这种方式在公司中比较少。
工作中有这种使用方式,最终大家觉得这样太麻烦了(前端、后台觉得我还需要处理这么多HTTP状态码不太想做)。最终没有能够坚持的执行下去。
2.4 URI的命名规则
URI的设计规则比前面几个都复杂,前面的还有一个固定可参照的规范来进行参照。这个没办法参照,特别是在项目紧急的时候,开发都是本着能用就行的方式进行开发,至于到底符不符合规范都没用那么的重要。
GET https://blog.ljbmxsm.com/blog/1
GET https://blog.ljbmxsm.com/blogs/1
GET https://blog.ljbmxsm.com/bg/1
GET https://blog.ljbmxsm.com/getBlog/1
如上面的例子,我就是想获取一下一篇ID为1的blog内容,就可能被写出这么多五花八门的URI。这里到底需要遵循一个什么样的规范和一个什么标准。后面参照其他比较大的公司的Restful接口设计给出总结。
3. 大公司如何设计Restful API
下面来分析一下国内和国外的知名公司看一下大公司是如何设计 Restful API ,不同公司有什么不同,有哪些是相同的。国内选择 企鹅(微信开放平台)、阿里巴巴(钉钉开放平台),国外的选择 微软、paypal 作为对照。
3.1 微信开放平台
例子地址:developers.weixin.qq.com/doc/oplatfo…
分析说明:
- 微信开放平台的接口没有看到URI中有版本的控制(可以理解为提供对外的接口没有做过版本升级至少接口层面看上去是)
- 对于不同的数据选择不同的HTTP方法进行数据获取
- 当后台验证或者获取失败的情况下,微信开放平台是通过
errcode方式来返回错误信息, HTTP状态码是:200 - URI的请求语义都是不带有动词的,都是名称。单词分隔符用的
-(横杠非下划线),查询的条件使用的是下划线,返回的数据如果是多个单词也是用下划线的方式进行分割。
3.2 钉钉开放平台
钉钉接口约束文档:open.dingtalk.com/document/or…
分析说明:
- URI存在
version的版本管控,API有不同的版本 - 对于不同的数据选择不同的HTTP方法进行数据获取
- 当后台验证或者获取失败的情况下,接口错误码和HTTP状态码相结合(错误码说明:open.dingtalk.com/document/or…)
- URI的请求语义都是不带有动词的,都是名称。路径中的名称使用的是驼峰的风格,查询条件也是使用的驼峰风格。(这里感觉是和开发语言相关,阿里主打Java)
3.3 微软
文档地址:docs.microsoft.com/en-us/rest/… ,这个是在微软Restful接口网站找了一个例子:
分析说明:
- 在路径中没有存在版本,但是在URI的查询条件后面存在一个api版本的查询条件
- 对于不同的数据选择不同的HTTP方法进行数据获取
- 接口错误码和HTTP状态码相结合
- URI的请求语义都是不带有动词的,都是名称。单词分隔符用的
-(横杠非下划线),查询的条件使用的是横杠,返回的数据使用的是驼峰。
参考网站:
3.4 PayPal
PayPal文档地址:developer.paypal.com/api/rest/
curl -v POST https://api-m.sandbox.paypal.com/v1/oauth2/token \
-H "Accept: application/json" \
-H "Accept-Language: en_US" \
-u "CLIENT_ID:SECRET" \
-d "grant_type=client_credentials"
返回值:
{
"scope": "https://uri.paypal.com/services/invoicing https://uri.paypal.com/services/disputes/read-buyer https://uri.paypal.com/services/payments/realtimepayment https://uri.paypal.com/services/disputes/update-seller https://uri.paypal.com/services/payments/payment/authcapture openid https://uri.paypal.com/services/disputes/read-seller https://uri.paypal.com/services/payments/refund https://api-m.paypal.com/v1/vault/credit-card https://api-m.paypal.com/v1/payments/.* https://uri.paypal.com/payments/payouts https://api-m.paypal.com/v1/vault/credit-card/.* https://uri.paypal.com/services/subscriptions https://uri.paypal.com/services/applications/webhooks",
"access_token": "A21AAFEpH4PsADK7qSS7pSRsgzfENtu-Q1ysgEDVDESseMHBYXVJYE8ovjj68elIDy8nF26AwPhfXTIeWAZHSLIsQkSYz9ifg",
"token_type": "Bearer",
"app_id": "APP-80W284485P519543T",
"expires_in": 31668,
"nonce": "2020-04-03T15:35:36ZaYZlGvEkV4yVSz8g6bAKFoGSEzuy3CQcz3ljhibkOHg"
}
分析说明:
- URI存在
version的版本管控,API有不同的版本 - 对于不同的数据选择不同的HTTP方法进行数据获取
- 接口错误返回和HTTP状态码相关联(developer.paypal.com/api/rest/re…)
- URI的请求语义都是不带有动词的,都是名称。单词分隔符用的
-(横杠非下划线),查询的条件使用的是下划线,返回的数据如果是多个单词也是用下划线的方式进行分割
参考网站:
通过上面的四个不同的公司对外开放的API可以总结出来以下几个共同点:
- Restful API设计需要考虑版本,除了微信没有看到明确的版本控制,微软放在查询条件后面
- 根据语义不同使用不同HTTP不同的请求方式
- 接口的错误码和HTTP状态码相结合,微信是通过code表示所有的状态,没有和HTTP状态码进行关联
- URI路径中都不带有动词,都是名称。路径中名称有使用驼峰(钉钉),也有使用
-(横杠)作为分隔符
4. Restful API如何设计
从上面的案例的对比可以看出来,不同公司有着不同的设计风格。不同的风格中也有着共同点。那我们就从这些相同的和不同的,设计一个符合自己工作或者学习过程中的Restful API 规范。
4.1 Restful API 版本管理
API的版本管理还是需要,版本管理最好加载 Controller上面或者对应的方法上面。
@RestController
@RequestMapping("/v1/log")
public class TestController {
@Async
@GetMapping("")
public String log(){
return null;
}
}
@RestController
@RequestMapping("")
public class TestController {
@GetMapping("/v1/log")
public String log(){
return null;
}
}
好处:
- 更细粒度的管理接口版本,并不是每一次API升级都是整个项目的API。
- 对应已存在的项目对于某些接口加上版本管理页更加方便。
4.2 不同的语义接口要使用不同的HTTP方法
| HTTP Method | 说明 | 是否要幂等 |
|---|---|---|
| GET | 主要用于从服务端获取对象信息(仅仅只是使用获取用户对象) | 是 |
| POST | 创建对象 | 否 |
| PUT | 更新对象(更新对象所有的内容) | 是 |
| DELETE | 删除一个对象 | 是 |
| PATCH | 更新对象(部分更新) | 否 |
| OPTIONS | 获取服务器支持的HTTP请求方法,用来检查服务器的性能 | 是 |
CRUD 选择不同的 HTTP Method ,不应该都使用一种方式来操作所有,比如增删改查都用POST的方式(刚毕业那会我就这么干过),这样做是可以但是不符合语义。
前面也提到过,如果批量请求对象的信息,可能会导致URL长度超过,这种情况应该通过分批次请求。而不是一次性请求。在业务上来说一次性获取这么多数据本身就是不合理。
4.3 HTTP状态码和接口错误码相结合
大多数情况下HTTP状态码和接口错误码是分开的,并不会有关联,这也是大多公司的做法。原因也很多,前后端处理起来都麻烦。但是很多监控都可以直接通过检测HTTP状态码来统计。但是如果不管后台抛什么错误都以 HTTP code 200 然后根据接口错误码来判断就会很麻烦。从上面的几个大公司的设计来看,HTTP状态码和接口错误码相结合 是一个不错的选择
公司已存在的项目可以保持原有的,新建的项目可以用这种形式。
结构:
HTTP status code 400
{
"errorCode":"4000",
"errorMessage":"错误"
}
errorCode 错误码定义为业务的错误码, errorMessage 是具体的错误信息。
| 状态码 | 状态码英文名称 | 中文描述 |
|---|---|---|
| 200 | OK | 请求成功。一般用于GET与POST请求 |
| 201 | Created | 已创建。成功请求并创建了新的资源 |
| 202 | Accepted | 已接受。已经接受请求,但未处理完成 |
| 203 | Non-Authoritative Information | 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 |
| 204 | No Content | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 |
| 205 | Reset Content | 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 |
| 206 | Partial Content | 部分内容。服务器成功处理了部分GET请求 |
| 302 | Found | 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI |
| 303 | See Other | 查看其它地址。与301类似。使用GET和POST请求查看 |
| 304 | Not Modified | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 |
| 305 | Use Proxy | 使用代理。所请求的资源必须通过代理访问 |
| 306 | Unused | 已经被废弃的HTTP状态码 |
| 307 | Temporary Redirect | 临时重定向。与302类似。使用GET请求重定向 |
| 400 | Bad Request | 客户端请求的语法错误,服务器无法理解 |
| 401 | Unauthorized | 请求要求用户的身份认证 |
| 402 | Payment Required | 保留,将来使用 |
| 403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
| 404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 |
| 405 | Method Not Allowed | 客户端请求中的方法被禁止 |
| 406 | Not Acceptable | 服务器无法根据客户端请求的内容特性完成请求 |
| 407 | Proxy Authentication Required | 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 |
| 408 | Request Time-out | 服务器等待客户端发送的请求时间过长,超时 |
| 409 | Conflict | 服务器完成客户端的PUT请求是可能返回此代码,服务器处理请求时发生了冲突 |
| 410 | Gone | 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 |
| 411 | Length Required | 服务器无法处理客户端发送的不带Content-Length的请求信息 |
| 412 | Precondition Failed | 客户端请求信息的先决条件错误 |
| 413 | Request Entity Too Large | 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 |
| 414 | Request-URI Too Large | 请求的URI过长(URI通常为网址),服务器无法处理 |
| 415 | Unsupported Media Type | 服务器无法处理请求附带的媒体格式 |
| 416 | Requested range not satisfiable | 客户端请求的范围无效 |
| 417 | Expectation Failed | 服务器无法满足Expect的请求头信息 |
| 500 | Internal Server Error | 服务器内部错误,无法完成请求 |
| 501 | Not Implemented | 服务器不支持请求的功能,无法完成请求 |
| 502 | Bad Gateway | 充当网关或代理的服务器,从远端服务器接收到了一个无效的请求 |
| 503 | Service Unavailable | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 |
| 504 | Gateway Time-out | 充当网关或代理的服务器,未及时从远端服务器获取请求 |
| 505 | HTTP Version not supported | 服务器不支持请求的HTTP协议的版本,无法完成处理 |
4.4 URI设计
4.4.1 一类资源
#资源集合
/accounts
#单个资源
/account/{accountId}
4.4.2 单复数统一
#资源集合
/accounts
#单个资源
/account/{accountId}
#统一
/accounts
/accounts/{accountId}
4.4.3 使用名词而不是动词
使用HTTP方法来表达动作而不是通过URI的中的动词来表达
#获取集合
GET /accounts
#获取集合--这里就是一个反例
GET /get/accounts/
4.4.4 支持复杂查询
| Operator | Description | Example |
|---|---|---|
| Comparison Operators | ||
| eq | Equal | city eq 'Redmond' |
| ne | Not equal | city ne 'London' |
| gt | Greater than | price gt 20 |
| ge | Greater than or equal | price ge 10 |
| lt | Less than | price lt 20 |
| le | Less than or equal | price le 100 |
| Logical Operators | ||
| and | Logical and | price le 200 and price gt 3.5 |
| or | Logical or | price le 3.5 or price gt 200 |
| not | Logical negation | not price le 3.5 |
| Grouping Operators | ||
| ( ) | Precedence grouping | (priority eq 1 or city eq 'Redmond') and price gt 100 |
上面表格来源微软api指导:github.com/microsoft/a…
GET https://api.contoso.com/v1.0/products?$filter=name eq 'Milk'
这种复杂的查询可以实现也可以不实现。
4.5 返回JSON数据格式
{
"error": {
"code": "BadArgument",
"message": "Previous passwords may not be reused",
"target": "password",
"innererror": {
"code": "PasswordError",
"innererror": {
"code": "PasswordDoesNotMeetPolicy",
"minLength": "6",
"maxLength": "64",
"characterTypes": ["lowerCase","upperCase","number","symbol"],
"minDistinctCharacterTypes": "2",
"innererror": {
"code": "PasswordReuseNotAllowed"
}
}
}
}
}
{
"errorCode": “”,
"errorMessage":"",
"data":{
}
}
返回的格式尽量简单,能够表达意思就好。
5. 总结
-
对于公司应该定制一个统一的规范,特别是有对外暴露自己的接口。需要参照一定的规范进行设计(对内、对外最好统一规范)
-
HTTP状态码和接口错误码相结合是有必要的,这样能够更加清楚的表达接口的状态(至少规则指定后项目要遵守)
-
URI的通用规则应该统一,这样整个URI看起来就很整洁,可读性更强
参考文档: