0%

RESTful API

REST 全称是 Representational State Transfer,由 Roy Fielding 2000年在其博士论文 Architectural Styles and the Design of Network-based Software Architectures 中提出,目标是在基于网络的软件设计中,得到一个功能强、性能好、适宜通信的架构。随着互联网的兴起,软件与网络两个领域开始融合,RESTful API 设计也得到了广泛的应用。本文将介绍 RESTful 架构的约束条件与原则,并介绍 RESTful API 设计的最佳实践。

关键概念

Resource

REST 架构中最关键的一个概念抽象是 resource,任何网络上的一个实体或者具体的信息都可以叫做 resouce:一段文本、一张图片、一个临时的服务、其他 resource 的集合,甚至是一个非虚拟的对象(比如一个人)。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

Representation

在任何一个特定时刻,一个 resource 的状态称作 resource representation。一个 representation 包含data、描述data的metadata,以及能够帮助 client 转换到下一个状态的 hypermedia links。一个 representation 的数据格式被称作一个 media typeMedia Type 定义了一个 representation 该如何被处理的规范。比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。

URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的.html后缀名是不必要的,因为这个后缀名表示格式,属于”表现层”范畴,而URI应该只代表resource的位置。它的具体表现形式,应该在HTTP请求的头信息中用 AcceptContent-Type 字段指定,这两个字段才是对 representation 的描述。

A truly RESTful API looks like hypertext。任何可以被定位的信息包含一个显示或者隐式的地址。根据 Roy Fielding 所言:

Hypertext (or hypermedia) means the simultaneous presentation of information and controls such that the information becomes the affordance through which the user (or automaton) obtains choices and selects actions. Remember that hypertext does not need to be HTML (or XML or JSON) on a browser. Machines can follow links when they understand the data format and relationship types.

State Transfer

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”。

客户端用到的手段,可以是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

约束条件

REST指的是一组架构约束条件和原则。如果一个架构符合REST的约束条件和原则,我们就称它为 RESTful架构。 Roy Fielding 在其论文中提出了下面六个约束条件:

Uniform Interface

这是 RESTful 系统设计的基本出发点. 它简化了系统架构, 减少了耦合性, 可以让所有模块各自独立的进行改进. 包括下列四个限制:

  • 请求中包含资源的 ID (Resource identification in requests)
    • 请求中包含了各种独立资源的标识。例如,在 Web 服务中的 URIs
    • 资源本身和发送给客户端的标识是独立。例如,服务器可以将自身的数据库信息以 XML 或者 JSON 的方式发送给客户端,但是这些可能都不是服务器的内部记录方式.
  • 资源通过标识来操作 (Resource manipulation through representations)
    • 当客户端拥有一个资源的标识,包括附带的元数据,则它就有足够的信息来删除这个资源。
  • 消息的自我描述性 (Self-descriptive messages)
    • 每一个消息都包含足够的信息来描述如何来处理这个信息。例如,媒体类型 (midia-type) 就可以确定需要什么样的分析器来分析媒体数据
  • 用超媒体驱动应用状态 (Hypermedia as the engine of application state (HATEOAS))
    • 同用户访问 Web 服务器的 Home 页面相似,当一个 REST 客户端访问了最初的 REST 应用的 URI 之后,REST 客户端应该可以使用服务器端提供的链接,动态的发现所有的可用的资源和可执行的操作。随着访问的进行,服务器在响应中提供文字超链接,以便客户端可以得到当前可用的操作。
    • 客户端无需用确定的编码的方式记录下服务器端所提供的动态应用的结构信息。

Once a developer becomes familiar with one of your APIs,he should be able to follow a similar approach for other APIs.

Client-Server

client-server 结构限制的目的是将 clientserver 的关注点分离。 将 用户界面所关注的逻辑数据存储所关注的逻辑 分离开来有助于提高用户界面的跨平台的可移植性,通过简化服务器模块也有助于服务器模块的可扩展性。

Servers and clients may also be replaced and developed independently, as long as the interface between them is not altered.

Stateless

服务器不能保存客户端的信息,每一次从客户端发送的请求中,要包含所有的必须的状态信息,会话信息由客户端保存, 服务器端根据这些状态信息来处理请求。 服务器可以将会话状态信息传递给其他服务, 比如数据库服务,这样可以保持一段时间的状态信息,从而实现认证功能。

当客户端可以切换到一个新状态的时候发送请求信息。当一个或者多个请求被发送之后,客户端就处于一个状态变迁过程中,每一个应用的状态描述可以被客户端用来初始化下一次的状态变迁。

No client context shall be stored on the server between requests. The client is responsible for managing the state of the application.

Cacheable

如同万维网一样,客户端和中间的通讯传递者可以将回复缓存起来。 回复必须明确的或者间接的表明本身是否可以进行缓存, 这可以预防客户端在将来进行请求的时候得到陈旧的或者不恰当的数据。管理良好的缓存机制可以减少客户端-服务器之间的交互, 甚至完全避免客户端-服务器交互,这进一步提了高性能和可扩展性。

Well-managed caching partially or completely eliminates some client-server interactions, further improving scalability and performance.

Layered System

客户端一般不知道是否直接连接到了最终的服务器,或者是路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样可也便于安全策略的部署。

Code On Demand (Optional)

服务器可以通过发送可执行代码给客户端的方式临时性的扩展功能或者定制功能。例如Java Applet、Flash或JavaScript。

All the above constraints help you build a truly RESTful API,and you should follow them. Still,at times,you may find yourself violating one or two constraints. Do not worry; you are still making a RESTful API – but not truly RESTful.

最佳实践

Resource Naming

REST 设计中,resource 是最基本的数据表示层,拥有一个一致的资源命名策略是设计中的一个关键问题。

  • A resource can be a singleton or a collection

举例来说,customers 是一种 collection resource,而 customer 是一种 singleton resource。那么我们访问 customers资源可以使用 /customers 这种 URI,访问 customer 资源可以使用 /customers/{customerId} 这种URI。

  • A resource may contain sub-collection resources

还是上面的例子,customer 资源可能有 sub-collection 资源,那么我们可以通过 /customers/{customerId}/accounts 这种 URI 访问。进一步的,针对 accounts 的单一资源,可以使用 /customers/{customerId}/accounts/{accountId} 这种URI来访问。

使用名词表示资源

RESTful URI 使用 名词 来表示资源,而不是动词。一般来说名词有四种类型:

  • document

document 类型的资源类似于一个 object instance 或者是 database record,REST 中可以把它视作是 single resource,URI中一般使用单数形式。

1
2
3
http://api.example.com/device-management/managed-devices/{device-id}
http://api.example.com/user-management/users/{id}
http://api.example.com/user-management/users/admin
  • collection

collection 类型的资源是 server-managed directory of resources,一般用复数形式表示。

1
2
3
http://api.example.com/device-management/managed-devices
http://api.example.com/user-management/users
http://api.example.com/user-management/users/{id}/accounts
  • store

store 类型的资源是 client-managed resource repository,与 collection 不同,这个是 client 维护的,client 可以修改这部分的内容,或者删除它们,一般用复数形式表示。

1
http://api.example.com/song-management/users/{id}/playlists
  • controller

controller 类型的资源代表一个过程,一般是执行的函数,使用 verb 表示这种类型。

1
2
http://api.example.com/cart-management/users/{id}/cart/checkout
http://api.example.com/song-management/users/{id}/playlist/play

不要使用 CURD 动词

不用在 URI 中使用 CURD 动词,而要通过 HTTP Request Method 方法。

1
2
3
4
5
GET http://api.example.com/device-management/managed-devices  //Get all devices
POST http://api.example.com/device-management/managed-devices //Create new Device
GET http://api.example.com/device-management/managed-devices/{id} //Get device for given Id
PUT http://api.example.com/device-management/managed-devices/{id} //Update device for given Id
DELETE http://api.example.com/device-management/managed-devices/{id} //Delete device for given Id

采用查询字符串来过滤

1
2
3
4
http://api.example.com/device-management/managed-devices
http://api.example.com/device-management/managed-devices?region=USA
http://api.example.com/device-management/managed-devices?region=USA&brand=XYZ
http://api.example.com/device-management/managed-devices?region=USA&brand=XYZ&sort=installation-date

URI可读性与持续性

Use forward slash (/) to indicate hierarchical relationships

1
2
3
4
5
http://api.example.com/device-management
http://api.example.com/device-management/managed-devices
http://api.example.com/device-management/managed-devices/{id}
http://api.example.com/device-management/managed-devices/{id}/scripts
http://api.example.com/device-management/managed-devices/{id}/scripts/{id}

Do not use trailing forward slash (/) in URIs

1
2
http://api.example.com/device-management/managed-devices/
http://api.example.com/device-management/managed-devices /*This is much better version*/

Use hyphens (-) to improve the readability of URIs

1
2
http://api.example.com/inventory-management/managed-entities/{id}/install-script-location  //More readable
http://api.example.com/inventory-management/managedEntities/{id}/installScriptLocation //Less readable

Do not use underscores ( _ )

1
2
http://api.example.com/inventory-management/managed-entities/{id}/install-script-location  //More readable
http://api.example.com/inventory_management/managed_entities/{id}/install_script_location //More error prone

Use lowercase letters in URIs

RFC 3986 定义了 URI case-sensitive 但是 scheme 和 host 不敏感,所以下面1和2是相同的,但是3和另外两个并不相同,推荐尽量使用小写字符。

1
2
3
http://api.example.org/my-folder/my-doc  //1
HTTP://API.EXAMPLE.ORG/my-folder/my-doc //2
http://api.example.org/My-Folder/my-doc //3

Do not use file extentions

不要在 URI 中加上文件后缀,相反通过 Content-Type 获取 media-type

1
2
http://api.example.com/device-management/managed-devices.xml  /*Do not use it*/
http://api.example.com/device-management/managed-devices /*This is correct URI*/

Status Code

状态码必须精确

客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。

HTTP 状态码就是一个三位数,分成五个类别。

1
2
3
4
5
`1xx`:相关信息
`2xx`:操作成功
`3xx`:重定向
`4xx`:客户端错误
`5xx`:服务器错误

这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

API 不需要1xx状态码,下面介绍其他四类状态码的精确含义。

2xx 状态码

200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

1
2
3
4
5
GET: 200 OK
POST: 201 Created
PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content

上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。

此外,202 Accepted状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。下面是一个例子。

1
2
3
4
5
6
7
8
HTTP/1.1 202 Accepted

{
"task": {
"href": "/api/company/job-management/jobs/2130040",
"id": "2130040"
}
}

3xx 状态码

API 用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。

API 用到的3xx状态码,主要是303 See Other,表示参考另一个 URL。它与302307的含义一样,也是”暂时重定向”,区别在于302307用于GET请求,而303用于POSTPUTDELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。

1
2
HTTP/1.1 303 See Other
Location: /api/orders/12345

4xx 状态码

4xx状态码表示客户端错误,主要有下面几种:

  • 400 Bad Request:服务器不理解客户端的请求,未做任何处理。
  • 401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证。
  • 403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。
  • 404 Not Found:所请求的资源不存在,或不可用。
  • 405 Method Not Allowed:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。
  • 410 Gone:所请求的资源已从这个地址转移,不再可用。
  • 415 Unsupported Media Type:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。
  • 422 Unprocessable Entity :客户端上传的附件无法处理,导致请求失败。
  • 429 Too Many Requests:客户端的请求次数超过限额。

5xx 状态码

5xx状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

  • 500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。
  • 503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。

Caching

CacheableREST 架构中的一个约束条件,GET 请求默认是都是 Cacheable 的,POST请求默认不是Cacheable的,可以通过设置 Expires 或者 Cache-Control来显示设置,PUTDELETE 请求的响应是完全不可Cache的。

Expires

Expires 响应头包含日期/时间, 即在此时候之后,响应过期。无效的日期,比如 0, 代表着过去的日期,即该资源已经过期。

如果在Cache-Control响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。

1
Expires: Fri, 20 May 2016 19:20:49 IST

Cache-Control

Cache-Control 通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中。

  • 缓存请求指令

客户端可以在HTTP请求中使用的标准 Cache-Control 指令。

1
2
3
4
5
6
7
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: only-if-cached
  • 缓存响应指令

服务器可以在响应中使用的标准 Cache-Control 指令。

1
2
3
4
5
6
7
8
9
Cache-control: must-revalidate
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: public
Cache-control: private
Cache-control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-control: s-maxage=<seconds>

ETag

ETag HTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖(“空中碰撞”)。

如果给定URL中的资源更改,则一定要生成新的Etag值。 因此Etags类似于指纹,也可能被某些服务器用于跟踪。 比较etags能快速确定此资源是否变化,但也可能被跟踪服务器永久存留。

1
ETag: "abcd1234567n34jv"

Last-Modified

The Last-Modified 是一个响应首部,其中包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。

1
Last-Modified: Fri, 10 May 2016 09:17:49 IST

Content Negotiation

一般来说,资源都有多种表现形式,client 选择一个合适的表现形式称作 content negotiation

client 可以在 request 中通过 Content-Type字段指定 client 发送资源的表现形式,常见的有 text/plain, application/xml, text/html, application/json, image/gif, and image/jpeg

1
Content-Type: application/json

client 也可以通过 Accept 指定希望接收的数据类型,这里的 q 表示对不同类型的偏好设置。

1
application/json,application/xml;q=0.9,*/*;q=0.8

HATEOAS

API 的使用者未必知道,URL 是怎么设计的。一个解决方法就是,在回应中,给出相关链接,便于下一步操作。这样的话,用户只要记住一个 URL,就可以发现其他的 URL。这种方法叫做 HATEOAS,也就是 Hypermedia as the Engine of Application State

举个例子,一个用户通过 GET 请求查询银行中某个账户的情况:

1
2
3
4
GET /accounts/12345 HTTP/1.1
Host: bank.example.com
Accept: application/vnd.acme.account+json
...

请求的响应如下,不仅包括账户的余额信息,还包括了进一步操作的链接,用户可以取钱、存钱、转账或者关闭帐户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HTTP/1.1 200 OK
Content-Type: application/vnd.acme.account+json
Content-Length: ...

{
"account": {
"account_number": 12345,
"balance": {
"currency": "usd",
"value": 100.00
},
"links": {
"deposit": "/accounts/12345/deposit",
"withdraw": "/accounts/12345/withdraw",
"transfer": "/accounts/12345/transfer",
"close": "/accounts/12345/close"
}
}
}

当用户透支后,这时候查询账户信息,只剩下一个存钱的链接,这就是 Engine of Applicaiton State 名称的来源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP/1.1 200 OK
Content-Type: application/vnd.acme.account+json
Content-Length: ...

{
"account": {
"account_number": 12345,
"balance": {
"currency": "usd",
"value": -25.00
},
"links": {
"deposit": "/accounts/12345/deposit"
}
}
}

Idempotence

RESTFul API中的幂等性是指调用某个方法1次或N次对资源产生的影响结果都是相同的,需要特别注意的是:这里幂等性指的是对资源产生的影响结果,而不是调用HTTP方法的返回结果。举个例子,RESTFul API中的GET方法是查询资源信息,不会对资源产生影响,所以它是符合幂等性的,但是每次调用GET方法返回的结果有可能不同(可能资源的某个属性在调用GET方法之前已经被其他方法修改了)

An idempotent HTTP method is an HTTP method that can be called many times without different outcomes. It would not matter if the method is called only once, or ten times over. The result should be the same.

Idempotence essentially means that the result of a successfully performed request is independent of the number of times it is executed. For example, in arithmetic, adding zero to a number is an idempotent operation.

除了幂等性之外,HTTP方法的安全性是指不对资源产生修改

常用HTTP方法的幂等性和安全性总结如下:

HTTP方法名称 是否幂等 是否安全
OPTIONS Y Y
HEAD Y Y
GET Y Y
PUT Y N
DELETE Y N
POST N N
PATCH N N

Versioning

随着系统的演进,API可能会发生 breaking change,这个时候需要对API版本化,通常有以下几种做法:

URI Versioning

在 URI 中添加版本是最直接的方式,虽然这违背了 URL 应该只想一个唯一的资源 的原则,kubernetes 就是采用的这种方式。

1
2
https://api.example.com/v1/
https://api.example.com/v2beta1/

Versioning using Accept header

1
2
Accept: application/vnd.example.v1+json
Accept: application/vnd.example+json;version=1.0

总结

  • 资源是由URI来指定。
  • 对资源的操作包括获取、创建、修改和删除,这些操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。
  • 通过操作资源的表现形式来操作资源。
  • 资源的表现形式则是XML或者HTML,取决于读者是机器还是人、是消费Web服务的客户软件还是Web浏览器。当然也可以是任何其他的格式,例如JSON。

参考资料