一般设计模式
空白响应
标准的 Delete
方法必须返回 google.protobuf.Empty
才能获得全局一致性。它还可以防止客户端重试时所依赖的元数据不可用。对于自定义方法,它们必须有自己的形如 XxxResponse
的消息,即使它们是空白的,因为很可能在方法功能变化后,需要返回额外的数据。
表示范围
用来表达范围的字段应当使用半开区间的命名约定,形如:[start_xxx,end_xxx)
,例如 [start_key,end_key]
或 [start_time,end_time)
。 C++ STL 库和 Java 标准库通常使用这种半开区间语义。API 应避免使用其他表示范围的方法,例如 (index,count)
或 [first,last]
。
资源标签
在面向资源的 API 中,资源模式由 API 来定义。为了让客户端将少量的简单元数据附加到资源(例如,将一个虚拟机标记为数据库服务器),API 应该使用 google.api.LabelDescriptor
中描述的资源标签设计模式。
为此,设计 API 时应该向资源定义中添加字段 map<string,string>
标签。
message Book {
string name = 1;
map<string, string> labels = 2;
}
长时运行的操作
如果一个 API 方法通常需要很长时间才能执行完成,那么它可以设计成向客户端返回一个长时间运行(Long Running Operation)资源,客户端可以使用该资源来跟踪进度并获取结果。该操作定义了一个标准接口,用于长时间运行的操作。各个 API 禁止为长时间运行的操作定义自己的接口,以避免不一致。
操作资源必须作为响应消息直接返回,并且操作的任何直接后果都应该反映在API中。例如,在创建资源时,尽管资源应该指示它没有准备好被使用,该资源也应该出现在LIST和GET方法中。操作完成后,如果方法不长时间运行,Operation.response
字段应包含直接返回的消息。
列表分页
以列表形式获取的集合应该支持分页,即使结果集通常很小。
原理:即使向现有的 API 添加分页支持仅仅是从 API 表面视角添加,那也是一个会破坏 API 行为的更改。现有的客户端由于不知道分页,将错误地假设会接收到完整的列表结果,而实际客户端只接收到第一页。
为了使 List
方法支持分页(在页面中返回列表结果),API 应该:
- 在
List
方法的请求消息中定义一个string
类型字段page_token
。客户端使用此字段请求列表结果的特定页面。 - 在
List
方法的请求消息中定义一个int32
类型字段page_size
。客户端使用此字段指定服务器一次返回的最大结果数。服务器仍然可以进一步限制在单个页面中返回的结果的最大数量,如果page_size
为0,服务器将决定要返回的结果数。 - 在
List
方法的响应消息中定义一个string
字段next_page_token
。此字段代表用于检索下一页的分页令牌。如果值为空字符串,则表示没有更多结果了。
要检索下一页结果,客户端应该在后续 List
方法调用(在请求消息的 page_token
字段中)传递前一次响应的 next_page_token
的值:
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
message ListBooksRequest {
string name = 1;
int32 page_size = 2;
string page_token = 3;
}
message ListBooksResponse {
repeated Book books = 1;
string next_page_token = 2;
}
当客户端传递除页面令牌之外的查询参数时,如果查询参数与页面令牌不一致,则服务必须使请求失败。
页面令牌的内容应该是一个 base64 编码过的PB(protocol buffer)。 这避免了兼容性问题,如果页面令牌包含潜在的敏感信息,那么该信息应该被加密。 同时服务必须防止通过以下方法篡改页面令牌以泄露数据:
- 在后续请求时必须重新指定查询参数。
- 在页面令牌中只引用服务器端的会话状态。
- 页面令牌中加密并签名的的查询参数,需要在每次调用时都重新验证和重新授权。
分页的具体实现中也可以提供名为 total_size
的 int32
字段来表示总数。
列出子集合
有时,API 需要让客户端在子集合之间执行List/Search
方法。例如,图书馆 API 有一组书架,每个书架都有一组书籍,而客户想要在所有书架上搜索一本书。在这种情况下,建议对子集合使用标准List
方法,并为父集合指定通配符集合标识“-”
。对于 Library API 示例,我们可以使用以下 REST API 请求:
GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx
注意:使用“-”
而不是“*”
的原因是为了避免需要进行URL转义。
从子集合获取唯一资源
有时,子集合中的一个资源具有在其父集合内唯一的标识符。在这种情况下,允许通过 Get
来检索它并且不需要知道其属于哪个父集合。同时,建议使用标准 Get
,并为所有父集合指定通配符集合标识“-”
。例如,在图书馆 API 中,如果图书在所有书架上的所有图书中是唯一的,则可以使用以下 REST API 请求:
GET https://library.googleapis.com/v1/shelves/-/books/{id}
上面这个请求的响应中的资源必须使用资源的规范名称,对于每个父集合,应使用实际父集合标识符而不是“-”
。例如,上面的请求应该返回资源名为 shelves/shelf713/books/book8141
,而不是 shelves/-/books/book8141
。
排序顺序
如果 API 方法允许客户端指定列表结果的排序顺序,请求消息应该包含一个字段:
string order_by = ...;
排序字符串值应该遵循 SQL 语法:逗号分隔字段列表。例如:“foo,bar”
,默认排序顺序为升序,要为字段指定降序,应该在字段名后附加后缀 “desc”
。例如:“foo desc,bar”
。
语法中的冗余空格字符无关紧要。“foo,bar desc”
和 “ foo ,bar desc ”
是等效的。
请求验证
如果 API 方法有副作用,并且需要验证请求而避免此类副作用,请求消息应该包含一个字段:
bool validate_only = ...;
如果此字段设置为 true
,则服务器禁止执行任何副作用逻辑,只能执行特定于实现的与完整请求一致的验证逻辑。
如果验证成功,则必须返回 google.rpc.Code.OK
,任何使用相同请求消息的完整请求都不应返回 google.rpc.Code.INVALID_ARGUMENT
。注意,请求可能由于其他错误(如 google.rpc.Code.ALREADY_EXISTS
)或由于竞争而失败。
请求重复
对于网络 API,幂等 API 方法是更优的选择,因为它们可以在网络故障后安全地重试。然而,一些 API 方法无法容易地实现成幂等的。例如创建资源,并且需要避免不必要的重复。对于这种使用情况,请求消息应该包含唯一的 ID,例如 UUID,服务器将使用该唯一 ID 来检测复制,并确保请求仅被处理一次。
// A unique request ID for server to detect duplicated requests.
// This field **should** be named as `request_id`.
string request_id = ...;
如果检测到重复请求,则服务器应该返回先前成功请求的响应,因为客户端很可能未收到先前的响应。
枚举默认值
每个枚举定义必须以 0
值条目开始,当未明确指定枚举值时,将使用该条目。API 必须在文档中记录如何处理 0
值。
如果存在默认行为,则应该使用枚举值 0
,并且 API 应在文档中记录预期行为。
如果没有默认行为,则枚举值 0
应该命名为 ENUM_TYPE_UNSPECIFIED
,并且应该以错误 INVALID_ARGUMENT
来拒绝使用。
enum Isolation {
// Not specified.
ISOLATION_UNSPECIFIED = 0;
// Reads from a snapshot. Collisions occur if all reads and writes cannot be
// logically serialized with concurrent transactions.
SERIALIZABLE = 1;
// Reads from a snapshot. Collisions occur if concurrent transactions write
// to the same rows.
SNAPSHOT = 2;
...
}
// When unspecified, the server will use an isolation level of SNAPSHOT or
// better.
Isolation level = 1;
可以为 0
值使用一个惯用名称。例如,google.rpc.Code.OK
是指定缺少错误代码的惯用方法。 在这种情况下,OK
在语义上等同于枚举类型定义中的 UNSPECIFIED
。
在存在内在敏感和安全默认值的情况下,该值可以用于“0”值。 例如,BASIC
是资源视图枚举中的“0”值。
语法句法
在某些 API 设计中,有必要为某些数据格式定义简单的语法,例如可接受的文本输入。为了在 API 之间提供一致的开发体验并降低学习曲线,API 设计者必须使用 ISO 14977 扩展的巴斯克-诺尔格式(EBNF)句法来定义语法:
Production = name "=" [ Expression ] ";" ;
Expression = Alternative { "|" Alternative } ;
Alternative = Term { Term } ;
Term = name | TOKEN | Group | Option | Repetition ;
Group = "(" Expression ")" ;
Option = "[" Expression "]" ;
Repetition = "{" Expression "}" ;
注意:TOKEN
代表在语法之外定义的终止符。
整数类型
在 API 设计中,不应使用无符号整数类型,例如 uint32
和 fixed32
,因为一些重要的编程语言不能很好地支持它们,例如 Java 和 JavaScript,他们可能导致溢出错误。另一个问题是,不同的 API 非常喜欢在同一个场景中混用不匹配的有符号和无符号整数类型。
当有符号整数类型用于负值无意义的场景(例如尺寸或超时)时,值 -1(且只有 -1)可能用于指代特殊的含义,例如文件结尾(EOF)、无限超时、无限配额以及未知的年龄。这些用法必须清楚地记录在文档中,以避免混淆。API 创建者还应该记录隐式默认值 0 的行为。
部分响应
有时,一个 API 客户端只需要响应消息中的特定数据子集。为了支持这样的场景,一些 API 平台提供对部分响应的原生支持。Google API 平台通过响应消息中的字段掩码支持这一特性。对于任何 REST API 调用,都有一个隐式系统查询参数 $fields
,它是 google.protobuf.FieldMask
值的 JSON 表示形式。响应消息将在发送回客户端之前由 $field
过滤。 API 平台为所有的 API 方法自动处理此逻辑。
GET https://library.googleapis.com/v1/shelves?$fields=name
资源视图
为了减少网络流量,有时允许客户端限制服务器在其响应中返回的资源的部分,返回资源的视图而不是完整的资源表示形式,这一做法是非常有用的。 API 中的资源视图通过向方法请求添加参数来实现该功能,该参数允许客户端指定需要在响应中接收的资源。
参数:
- 应该是
enum
类型 - 必须命名为
view
每个枚举值定义了资源的哪些部分(哪些字段)将在服务器的响应中返回。每个视图(view
)值应该返回什么样的资源视图是由实现来定义的,同时应该在 API 文档中记录。
package google.example.library.v1;
service Library {
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
option (google.api.http) = {
get: "/v1/{name=shelves/*}/books"
}
};
}
enum BookView {
// Server responses only include author, title, ISBN and unique book ID.
// The default value.
BASIC = 0;
// Full representation of the book is returned in server responses,
// including contents of the book.
FULL = 1;
}
message ListBooksRequest {
string name = 1;
// Specifies which parts of the book resource should be returned
// in the response.
BookView view = 2;
}
这个结构将映射成类似下面的 URL 地址:
GET https://library.googleapis.com/v1/shelves/shelf1/books?view=BASIC
您可以在本设计指南的标准方法一章中找到有关定义方法、请求和响应的更多信息。
ETags
ETag 是一个非透明的标识符,允许客户端进行带条件的请求。为了支持 ETag,API 应该在资源定义中包含一个 string
字段 etag
,其语义必须与ETag的通用用法相匹配。通常,etag包含由服务器计算出的资源的指纹。有关更多详细信息,请参阅 Wikipedia 和 RFC 7232 规范。
ETag 可以被强验证或弱验证,其中弱验证的 ETag 前缀为 W/
。在此上下文中,强验证意味着具有相同 ETag 的两个资源具有逐字节完全相同的内容和相同的额外字段(例如,Content-Type)。这意味着经过强验证的 ETag 允许缓存部分响应并可以稍后用于组合。
相反,具有相同弱验证的 ETag 值的资源意味着在语义上等同,但不一定是逐字节相同的,因此不适合于缓存响应。
例如:
// This is a strong ETag, including the quotes.
"1a2f3e4d5b6c7c"
// This is a weak ETag, including the prefix and quotes.
W/"1a2b3c4d5ef"
输出字段
API 可能需要区分由客户端作为输入提供的字段,以及只由服务器返回的特定资源的输出字段。对于仅为输出的字段,字段属性应该在文档中记录。
注意,如果客户端在请求中对”仅输出字段“设值了,或者客户端指定了仅包含输出字段的 google.protobuf.FieldMask
,则服务器必须接受请求,而不会出错。 这意味着服务器必须忽略那些”仅输出字段“。因为客户端通常会重用由服务器返回的资源,并将其作为另一个请求输入。例如,检索到的 Book
资源将在稍后用作 UPDATE 方法的请求。如果服务器验证“仅输出字段“,则客户端需要额外工作以清除这些“仅输出字段”。
message Book {
string name = 1;
// Output only.
Timestamp create_time = 2;
}