目录
一、基于资源的设计
本设计指南的目标是帮助开发者设计简单、一致且易用的网络 API。同时,它还有助于将 RPC API(基于套接字)与 REST API(基于 HTTP)的设计融合起来。
RPC API 通常根据接口和方法设计。随着时间的推移,接口和方法越来越多,最终结果可能是形成一个庞大而混乱的 API 接口,因为开发者必须单独学习每种方法。显然,这既耗时又容易出错。
引入 REST 架构风格主要是为了与 HTTP/1.1 配合使用,但也有助于解决这个问题。其核心原则是定义可以用少量方法控制的命名资源。这些资源和方法被称为 API 的“名词”和“动词”。使用 HTTP 协议时,资源名称自然映射到网址,方法自然映射到 HTTP 的 POST
、GET
、PUT
、PATCH
和 DELETE
。这使得要学习的内容减少了很多,因为开发人员可以专注于资源及其关系,并假定它们拥有的标准方法同样很少。
近来,HTTP REST API 在互联网上取得了巨大成功。2010 年,大约 74% 的公共网络 API 是 HTTP REST API。
虽然 HTTP REST API 在互联网上非常流行,但它们承载的流量比传统的 RPC API 要小。例如,美国高峰时段大约一半的互联网流量是视频内容,显然出于性能考虑,很少有人会使用 REST API 来传送此类内容。在数据中心内,许多公司使用基于套接字的 RPC API 来承载大多数网络流量,这可能涉及比公共 REST API 高几个数量级的数据(以字节为单位)。
在实际使用中,人们会出于不同目的选择 RPC API 和 HTTP REST API,理想情况下,API 平台应该为所有类型的 API 提供最佳支持。本设计指南可帮助您设计和构建符合此原则的 API。它将面向资源的设计原则应用于通用 API 设计并定义了许多常见的设计模式,从而提高可用性并降低复杂性。
注意:本设计指南介绍了如何将 REST 原则应用于 API 设计,与编程语言、操作系统或网络协议无关。这不仅仅是一个创建 REST API 的指南。
什么是 REST API?
REST API 是可单独寻址的“资源”(API 中的“名词”)的“集合”。资源通过资源名称被引用,并通过一组“方法”(也称为“动词”或“操作”)进行控制。
REST Google API 的标准方法(也称为“REST 方法”)包括 List
、Get
、Create
、Update
和 Delete
。API 设计者还可以使用“自定义方法”(也称为“自定义动词”或“自定义操作”)来实现无法轻易映射到标准方法的功能(例如数据库事务)。
注意:自定义动词并不意味着创建自定义 HTTP 动词来支持自定义方法。对基于 HTTP 的 API 而言,它们只是映射到最合适的 HTTP 动词。
设计流程
设计指南建议在设计面向资源的 API 时采取以下步骤(更多细节将在下面的特定部分中介绍):
- 确定 API 提供的资源类型。
- 确定资源之间的关系。
- 根据类型和关系确定资源名称方案。
- 确定资源架构。
- 将最小的方法集附加到资源。
资源
面向资源的 API 通常被构建为资源层次结构,其中每个节点是一个“简单资源”或“集合资源”。 为方便起见,它们通常被分别称为资源和集合。
- 一个集合包含相同类型的资源列表。 例如,一个用户拥有一组联系人。
- 资源具有一些状态和零个或多个子资源。 每个子资源可以是一个简单资源或一个集合资源。
例如,Gmail API 有一组用户,每个用户都有一组消息、一组线程、一组标签、一个个人资料资源和若干设置资源。
虽然存储系统和 REST API 之间存在一些概念上的对应,但具有面向资源 API 的服务不一定是数据库,并且在解释资源和方法方面具有极大的灵活性。例如,创建日历事件(资源)可以为参与者创建附加事件、向参与者发送电子邮件邀请、预约会议室以及更新视频会议时间安排。
方法
面向资源的 API 的关键特性是,强调资源(数据模型)甚于资源上执行的方法(功能)。典型的面向资源的 API 使用少量方法公开大量资源。方法可以是标准方法或自定义方法。对于本指南,标准方法有:List
、Get
、Create
、Update
和 Delete
。
如果 API 功能能够自然映射到标准方法,则应该在 API 设计中使用该方法。对于不会自然映射到某一标准方法的功能,可以使用自定义方法。自定义方法提供与传统 RPC API 相同的设计自由度,可用于实现常见的编程模式,例如数据库事务或数据分析。
示例
以下部分介绍了如何将面向资源的 API 设计应用于大规模服务的一些实际示例。您可以在 Google API 代码库中找到更多示例。
在这些示例中,星号表示列表中的一个特定资源。
Gmail API
Gmail API 服务实现了 Gmail API 并公开了大多数 Gmail 功能。它具有以下资源模型:
- API 服务:
gmail.googleapis.com
- 用户集合:
users/*
。每个用户都拥有以下资源。- 消息集合:
users/*/messages/*
。 - 线程集合:
users/*/threads/*
。 - 标签集合:
users/*/labels/*
。 - 变更历史记录集合:
users/*/history/*
。 - 表示用户个人资料的资源:
users/*/profile
。 - 表示用户设置的资源:
users/*/settings
。
- 消息集合:
- 用户集合:
Cloud Pub/Sub API
pubsub.googleapis.com
服务实现了 Cloud Pub/Sub AP,后者定义以下资源模型:
- API 服务:
pubsub.googleapis.com
- 主题集合:
projects/*/topics/*
。 - 订阅集合:
projects/*/subscriptions/*
。
- 主题集合:
注意:Pub/Sub API 的其他实现可以选择不同的资源命名方案。
Cloud Spanner API
spanner.googleapis.com
服务实现了 Cloud Spanner API,后者定义了以下资源模型:
- API 服务:
spanner.googleapis.com
- 实例集合:
projects/*/instances/*
。- 实例操作的集合:
projects/*/instances/*/operations/*
。 - 数据库的集合:
projects/*/instances/*/databases/*
。 - 数据库操作的集合:
projects/*/instances/*/databases/*/operations/*
。 - 数据库会话的集合:
projects/*/instances/*/databases/*/sessions/*
。
- 实例操作的集合:
- 实例集合:
二、资源名称
在面向资源的 API 中,“资源”是被命名的实体,“资源名称”是它们的标识符。每个资源都必须具有自己唯一的资源名称。 资源名称由资源自身的 ID、任何父资源的 ID 及其 API 服务名称组成。在下文中,我们将查看资源 ID 以及如何构建资源名称。
gRPC API 应使用无传输协议的 URI 作为资源名称。它们通常遵循 REST 网址规则,其行为与网络文件路径非常相似。它们可以轻松映射到 REST 网址:如需了解详情,请参阅标准方法部分。
“集合”是一种特殊的资源,包含相同类型的子资源列表。例如,目录是文件资源的集合。集合的资源 ID 称为集合 ID。
资源名称由集合 ID 和资源 ID 构成,按分层方式组织并以正斜杠分隔。如果资源包含子资源,则子资源的名称由父资源名称后跟子资源的 ID 组成,也以正斜杠分隔。
示例 1:存储服务具有一组 buckets
,其中每个存储分区都有一组 objects
:
API 服务名称 | 集合 ID | 资源 ID | 集合 ID | 资源 ID |
---|---|---|---|---|
//storage.googleapis.com | /buckets | /bucket-id | /objects | /object-id |
示例 2:电子邮件服务具有一组 users
。每个用户都有一个 settings
子资源,而 settings
子资源拥有包括 customFrom
在内的许多其他子资源:
API 服务名称 | 集合 ID | 资源 ID | 资源 ID | 资源 ID |
---|---|---|---|---|
//mail.googleapis.com | /users | /name@example.com | /settings | /customFrom |
API 生产者可以为资源和集合 ID 选择任何可接受的值,只要它们在资源层次结构中是唯一的。您可以在下文中找到有关选择适当的资源和集合 ID 的更多准则。
通过拆分资源名称(例如 name.split("/")[n]
),可以获得单个集合 ID 和资源 ID(假设任何段都不包含正斜杠)。
完整资源名称
无传输协议的 URI 由 DNS 兼容的 API 服务名称和资源路径组成。资源路径也称为“相对资源名称”。 例如:
"//library.googleapis.com/shelves/shelf1/books/book2"
API 服务名称供客户端定位 API 服务端点;它可以是仅限内部服务的虚构 DNS 名称。如果 API 服务名称在上下文中很明显,则通常使用相对资源名称。
相对资源名称
开头没有“/”的 URI 路径 (path-noscheme)。它标识 API 服务中的资源。例如:
"shelves/shelf1/books/book2"
资源 ID
标识其父资源中资源的非空 URI 段 (segment-nz-nc),请参见上文的示例。
资源名称末尾的资源 ID 可以具有多个 URI 段。例如:
集合 ID | 资源 ID |
---|---|
files | /source/py/parser.py |
API 服务应该尽可能使用网址友好的资源 ID。 资源 ID 必须被清楚地记录,无论它们是由客户端、服务器还是其中一个分配的。例如,文件名通常由客户端分配,而电子邮件消息 ID 通常由服务器分配。
集合 ID
标识其父资源中集合资源的非空 URI 段 (segment-nz-nc),请参见上文的示例。
由于集合 ID 通常出现在生成的客户端库中,因此它们必须符合以下要求:
- 必须是有效的 C/C++ 标识符。
- 必须是复数形式的首字母小写驼峰体。如果该词语没有合适的复数形式,例如“evidence(证据)”和“weather(天气)”,则应该使用单数形式。
- 必须使用简明扼要的英文词语。
- 应该避免过于笼统的词语,或对其进行限定后再使用。例如,
rowValues
优先于values
。应该避免在不加以限定的情况下使用以下词语:- elements
- entries
- instances
- items
- objects
- resources
- types
- values
资源名称和网址
虽然完整的资源名称类似于普通网址,但两者并不相同。单个资源可以由不同的 API 版本、API 协议或 API 网络端点公开。完整资源名称未指明此类信息,因此在实际使用中必须将其映射到特定的 API 版本和 API 协议。
要通过 REST API 使用完整资源名称,必须将其转换为 REST 网址,实现方法为在服务名称之前添加 HTTPS 传输协议、在资源路径之前添加 API 主要版本以及对资源路径进行网址转义。例如:
// This is a calendar event resource name.
"//calendar.googleapis.com/users/john smith/events/123"
// This is the corresponding HTTP URL.
"https://calendar.googleapis.com/v3/users/john%20smith/events/123"
资源名称为字符串
除非存在向后兼容问题,否则 Google API 必须使用纯字符串来表示资源名称。资源名称应该像普通文件路径一样处理,并且它们不支持 % 编码。
对于资源定义,第一个字段应该是资源名称的字符串字段,并且应该称为 name
。
注意:以下代码示例使用 gRPC 转码语法。请点击链接以查看详细信息。
例如:
service LibraryService {
rpc GetBook(GetBookRequest) returns (Book) {
option (google.api.http) = {
get: "/v1/{name=shelves/*/books/*}"
};
};
rpc CreateBook(CreateBookRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/{parent=shelves/*}/books"
body: "book"
};
};
}
message Book {
// Resource name of the book. It must have the format of "shelves/*/books/*".
// For example: "shelves/shelf1/books/book2".
string name = 1;
// ... other properties
}
message GetBookRequest {
// Resource name of a book. For example: "shelves/shelf1/books/book2".
string name = 1;
}
message CreateBookRequest {
// Resource name of the parent resource where to create the book.
// For example: "shelves/shelf1".
string parent = 1;
// The Book resource to be created. Client must not set the `Book.name` field.
Book book = 2;
}
注意:为了保证资源名称的一致性,前导正斜杠不得被任何网址模板变量捕获。例如,必须使用网址模板 "/v1/{name=shelves/*/books/*}"
,而非 "/v1{name=/shelves/*/books/*}"
。
问题
问:为什么不使用资源 ID 来标识资源?
答:任何大型系统都有很多种资源。在使用资源 ID 来标识资源的时候,我们实际上是使用特定于资源的元组来标识资源,例如 (bucket, object)
或 (user, album, photo)
。这会带来几个主要问题:
- 开发者必须了解并记住这些匿名元组。
- 传递元组通常比传递字符串更难。
- 集中式基础架构(例如日志记录和访问控制系统)不理解专用元组。
- 专用元组限制了 API 设计的灵活性,例如提供可重复使用的 API 接口。例如,长时间运行的操作可以与许多其他 API 接口一起使用,因为它们使用灵活的资源名称。
问:为什么特殊字段名为 name
而不是 id
?
答:特殊字段以资源“名称”的概念命名。一般来说,我们发现 name
的概念让开发者感到困惑。例如,文件名实际上只是名称还是完整路径?通过预留标准字段 name
,开发者不得不选择更合适的词语,例如 display_name
或 title
或 full_name
。
三、标准方法
本章定义了标准方法(即 List
、Get
、Create
、Update
和 Delete
的概念。标准方法可降低复杂性并提高一致性。Google API 代码库中超过 70% 的 API 方法都是标准方法,这使得它们更易于学习和使用。
下表描述了如何将标准方法映射到 HTTP 方法:
标准方法 | HTTP 映射 | HTTP 请求正文 | HTTP 响应正文 |
---|---|---|---|
List | GET <collection URL> | 无 | 资源*列表 |
Get | GET <resource URL> | 无 | 资源* |
Create | POST <collection URL> | 资源 | 资源* |
Update | PUT or PATCH <resource URL> | 资源 | 资源* |
Delete | DELETE <resource URL> | 不适用 | google.protobuf.Empty ** |
*如果方法支持响应字段掩码以指定要返回的字段子集,则 List
、Get
、Create
和 Update
方法返回的资源可能包含部分数据。在某些情况下,API 平台对所有方法的字段掩码提供原生支持。
**从不立即移除资源的 Delete
方法(例如更新标志或创建长时间运行的删除操作)返回的响应应该包含长时间运行的操作或修改后的资源。
对于在单个 API 调用的时间跨度内未完成的请求,标准方法还可以返回长时间运行的操作。
以下部分详细描述了每种标准方法。 这些示例显示了.proto 文件中定义的方法和 HTTP 映射的特殊注释。您可以在 Google API 代码库中找到许多使用标准方法的示例。
列出
List
方法将一个集合名称和零个或多个参数作为输入,并返回与输入匹配的资源列表。
List
通常用于搜索资源。List
适用于来自单个集合的数据,该集合的大小有限且不进行缓存。对于更广泛的情况,应该使用自定义方法 Search
。
批量 get(例如,获取多个资源 ID 并为每个 ID 返回对象的方法)应该被实现为自定义 BatchGet
方法,而不是 List
方法。但是,如果您有一个已经存在的可提供相同功能的 List
方法,可以出于此目的重复使用 List
方法。如果您使用的是自定义 BatchGet
方法,则应该将其映射到 HTTP GET
。
HTTP 映射:
List
方法 必须使用 HTTPGET
动词。- 接收其资源正在列出的集合名称的请求消息字段应该映射到网址路径。如果集合名称映射到网址路径,则网址模板的最后一段(集合 ID)必须是字面量。
- 所有剩余的请求消息字段应该映射到网址查询参数。
- 没有请求正文,API 配置不得声明
body
子句。 - 响应正文应该包含资源列表以及可选元数据。
示例:
// Lists books in a shelf.
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
// List method maps to HTTP GET.
option (google.api.http) = {
// The `parent` captures the parent resource name, such as "shelves/shelf1".
get: "/v1/{parent=shelves/*}/books"
};
}
message ListBooksRequest {
// The parent resource name, for example, "shelves/shelf1".
string parent = 1;
// The maximum number of items to return.
int32 page_size = 2;
// The next_page_token value returned from a previous List request, if any.
string page_token = 3;
}
message ListBooksResponse {
// The field name should match the noun "books" in the method name. There
// will be a maximum number of items returned based on the page_size field
// in the request.
repeated Book books = 1;
// Token to retrieve the next page of results, or empty if there are no
// more results in the list.
string next_page_token = 2;
}
获取
Get
方法需要一个资源名称和零个或多个参数作为输入,并返回指定的资源。
HTTP 映射:
Get
方法 必须使用 HTTPGET
动词。- 接收资源名称的请求消息字段应该映射到网址路径。
- 所有剩余的请求消息字段应该映射到网址查询参数。
- 没有请求正文,API 配置不得声明
body
子句。 - 返回的资源应该映射到整个响应正文。
示例:
// Gets a book.
rpc GetBook(GetBookRequest) returns (Book) {
// Get maps to HTTP GET. Resource name is mapped to the URL. No body.
option (google.api.http) = {
// Note the URL template variable which captures the multi-segment resource
// name of the requested book, such as "shelves/shelf1/books/book2"
get: "/v1/{name=shelves/*/books/*}"
};
}
message GetBookRequest {
// The field will contain name of the resource requested, for example:
// "shelves/shelf1/books/book2"
string name = 1;
}
创建
Create
方法需要一个父资源名称、一个资源以及零个或多个参数作为输入。它在指定的父资源下创建新资源,并返回新建的资源。
如果 API 支持创建资源,则应该为每一个可以创建的资源类型设置 Create
方法。
HTTP 映射:
Create
方法 必须使用 HTTPPOST
动词。- 请求消息应该具有字段
parent
,以指定要在其中创建资源的父资源名称。 - 包含资源的请求消息字段必须映射到请求正文。如果将
google.api.http
注释用于Create
方法,则必须使用body: "<resource_field>"
表单。 - 该请求可以包含名为
<resource>_id
的字段,以允许调用者选择客户端分配的 ID。该字段可以在资源内。 - 所有剩余的请求消息字段应该映射到网址查询参数。
- 返回的资源应该映射到整个 HTTP 响应正文。
如果 Create
方法支持客户端分配的资源名称并且资源已存在,则请求应该失败并显示错误代码 ALREADY_EXISTS
或使用服务器分配的不同的资源名称,并且文档应该清楚地记录创建的资源名称可能与传入的不同。
Create
方法必须使用输入资源,以便在资源架构更改时,无需同时更新请求架构和资源架构。对于客户端无法设置的资源字段,必须将它们记录为“仅限输出”字段。
示例:
// Creates a book in a shelf.
rpc CreateBook(CreateBookRequest) returns (Book) {
// Create maps to HTTP POST. URL path as the collection name.
// HTTP request body contains the resource.
option (google.api.http) = {
// The `parent` captures the parent resource name, such as "shelves/1".
post: "/v1/{parent=shelves/*}/books"
body: "book"
};
}
message CreateBookRequest {
// The parent resource name where the book is to be created.
string parent = 1;
// The book id to use for this book.
string book_id = 3;
// The book resource to create.
// The field name should match the Noun in the method name.
Book book = 2;
}
rpc CreateShelf(CreateShelfRequest) returns (Shelf) {
option (google.api.http) = {
post: "/v1/shelves"
body: "shelf"
};
}
message CreateShelfRequest {
Shelf shelf = 1;
}
更新
Update
方法需要一条包含一个资源的请求消息和零个或多个参数作为输入。它更新指定的资源及其属性,并返回更新后的资源。
除了包含资源名称或父资源的属性之外,Update
方法应该可以改变可变资源的属性。Update
方法 不得包含任何“重命名”或“移动”资源的功能,这些功能应该由自定义方法来处理。
HTTP 映射:
- 标准
Update
方法应该支持部分资源更新,并将 HTTP 动词PATCH
与名为update_mask
的FieldMask
字段一起使用。应忽略客户端提供的作为输入的输出字段。 - 需要更高级修补语义的
Update
方法(例如附加到重复字段)应该由自定义方法提供。 - 如果
Update
方法仅支持完整资源更新,则必须使用 HTTP 动词PUT
。但是,强烈建议不要进行完整更新,因为在添加新资源字段时会出现向后兼容性问题。 - 接收资源名称的消息字段必须映射到网址路径。该字段可以位于资源消息本身中。
- 包含资源的请求消息字段必须映射到请求正文。
- 所有剩余的请求消息字段必须映射到网址查询参数。
- 响应消息必须是更新的资源本身。
如果 API 接受客户端分配的资源名称,则服务器可以允许客户端指定不存在的资源名称并创建新资源。 否则,使用不存在的资源名称的 Update
方法应该失败。 如果这是唯一的错误条件,则应该使用错误代码 NOT_FOUND
。
具有支持资源创建的 Update
方法的 API 还应该提供 Create
方法。原因是,如果 Update
方法是唯一的方法,则它将不知道如何创建资源。
例如:
// Updates a book.
rpc UpdateBook(UpdateBookRequest) returns (Book) {
// Update maps to HTTP PATCH. Resource name is mapped to a URL path.
// Resource is contained in the HTTP request body.
option (google.api.http) = {
// Note the URL template variable which captures the resource name of the
// book to update.
patch: "/v1/{book.name=shelves/*/books/*}"
body: "book"
};
}
message UpdateBookRequest {
// The book resource which replaces the resource on the server.
Book book = 1;
// The update mask applies to the resource. For the `FieldMask` definition,
// see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask
FieldMask update_mask = 2;
}
删除
Delete
方法需要一个资源名称和零个或多个参数作为输入,并删除或计划删除指定的资源。Delete
方法 应该返回 google.protobuf.Empty
。
API 不应该依赖于 Delete
方法返回的任何信息,因为它不能重复调用。
HTTP 映射:
Delete
方法 必须使用 HTTPDELETE
动词。- 接收资源名称的请求消息字段应该映射到网址路径。
- 所有剩余的请求消息字段应该映射到网址查询参数。
- 没有请求正文,API 配置不得声明
body
子句。 - 如果
Delete
方法立即移除资源,则应该返回空响应。 - 如果
Delete
方法启动长时间运行的操作,则应该返回长时间运行的操作。 - 如果
Delete
方法仅将资源标记为已删除,则应该返回更新后的资源。
对 Delete
方法的调用在效果上应该是幂等的,但不需要产生相同的响应。任意数量的 Delete
请求都应该导致资源(最终)被删除,但只有第一个请求会产生成功代码。后续请求应生成 google.rpc.Code.NOT_FOUND
。
例如:
// Deletes a book.
rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
// Delete maps to HTTP DELETE. Resource name maps to the URL path.
// There is no request body.
option (google.api.http) = {
// Note the URL template variable capturing the multi-segment name of the
// book resource to be deleted, such as "shelves/shelf1/books/book2"
delete: "/v1/{name=shelves/*/books/*}"
};
}
message DeleteBookRequest {
// The resource name of the book to be deleted, for example:
// "shelves/shelf1/books/book2"
string name = 1;
}
四、自定义方法
本章将讨论如何在 API 设计中使用自定义方法。
自定义方法是指 5 个标准方法之外的 API 方法。这些方法应该仅用于标准方法不易表达的功能。通常情况下,API 设计者应该尽可能优先考虑使用标准方法,而不是自定义方法。标准方法具有大多数开发者熟悉的更简单且定义明确的语义,因此更易于使用且不易出错。另一项优势是 API 平台更加了解和支持标准方法,例如计费、错误处理、日志记录、监控。
自定义方法可以与资源、集合或服务关联。 它可以接受任意请求和返回任意响应,并且还支持流式请求和响应。
自定义方法名称必须遵循方法命名惯例。
HTTP 映射
对于自定义方法,它们应该使用以下通用 HTTP 映射:
https://service.name/v1/some/resource/name:customVerb
使用 :
而不是 /
将自定义动词与资源名称分开以便支持任意路径。例如,恢复删除文件可以映射到 POST /files/a/long/file/name:undelete
选择 HTTP 映射时,应遵循以下准则:
- 自定义方法应该使用 HTTP
POST
动词,因为该动词具有最灵活的语义,但作为替代 get 或 list 的方法(如有可能,可以使用GET
)除外。(详情请参阅第三条。) - 自定义方法不应该使用 HTTP
PATCH
,但可以使用其他 HTTP 动词。在这种情况下,方法必须遵循该动词的标准 HTTP 语义。 - 请注意,使用 HTTP
GET
的自定义方法必须具有幂等性并且无负面影响。例如,在资源上实现特殊视图的自定义方法应该使用 HTTPGET
。 - 接收与自定义方法关联的资源或集合的资源名称的请求消息字段应该映射到网址路径。
- 网址路径必须以包含冒号(后跟自定义动词)的后缀结尾。
- 如果用于自定义方法的 HTTP 动词允许 HTTP 请求正文(其适用于
POST
、PUT
、PATCH
或自定义 HTTP 动词),则此自定义方法的 HTTP 配置必须使用body: "*"
子句,所有其他请求消息字段都应映射到 HTTP 请求正文。 - 如果用于自定义方法的 HTTP 动词不接受 HTTP 请求正文(
GET
、DELETE
),则此方法的 HTTP 配置不得使用body
子句,并且所有其他请求消息字段都应映射到网址查询参数。
警告:如果一个服务会实现多个 API,API 生产者必须仔细创建服务配置,以避免 API 之间的自定义动词发生冲突。
// This is a service level custom method.
rpc Watch(WatchRequest) returns (WatchResponse) {
// Custom method maps to HTTP POST. All request parameters go into body.
option (google.api.http) = {
post: "/v1:watch"
body: "*"
};
}
// This is a collection level custom method.
rpc ClearEvents(ClearEventsRequest) returns (ClearEventsResponse) {
option (google.api.http) = {
post: "/v3/events:clear"
body: "*"
};
}
// This is a resource level custom method.
rpc CancelEvent(CancelEventRequest) returns (CancelEventResponse) {
option (google.api.http) = {
post: "/v3/{name=events/*}:cancel"
body: "*"
};
}
// This is a batch get custom method.
rpc BatchGetEvents(BatchGetEventsRequest) returns (BatchGetEventsResponse) {
// The batch get method maps to HTTP GET verb.
option (google.api.http) = {
get: "/v3/events:batchGet"
};
}
用例
自定义方法适用于以下场景:
- 重启虚拟机。 设计备选方案可能是“在重启集合中创建一个重启资源”,这会让人感觉过于复杂,或者“虚拟机具有可变状态,客户端可以将状态从 RUNNING 更新到 RESTARTING”,这会产生可能存在哪些其他状态转换的问题。 此外,重启是一个常见概念,可以合理转化为一个自定义方法,从直观上来说符合开发者的预期。
- 发送邮件。 创建一个电子邮件消息不一定意味着要发送它(草稿)。与设计备选方案(将消息移动到“发件箱”集合)相比,自定义方法更容易被 API 用户发现,并且可以更直接地对概念进行建模。
- 提拔员工。 如果作为标准
update
方法实现,客户端需要复制企业提拔流程管理政策,以确保提拔发生在正确的级别,并属于同一职业阶梯等等。 - 批处理方法。 对于对性能要求苛刻的方法,提供自定义批处理方法可以有助于减少每个请求的开销。例如,accounts.locations.batchGet。
以下是标准方法比自定义方法更适用的示例:
- 使用不同查询参数的查询资源(使用带有标准列表过滤的标准
list
方法)。 - 简单的资源属性更改(使用带有字段掩码的标准
update
方法)。 - 关闭一个通知(使用标准
delete
方法)。
常用自定义方法
以下是常用或有用的自定义方法名称的精选列表。API 设计者在引入自己的名称之前应该考虑使用这些名称,以提高 API 之间的一致性。
方法名称 | 自定义动词 | HTTP 动词 | 备注 |
---|---|---|---|
Cancel | :cancel | POST | 取消一个未完成的操作,例如 operations.cancel。 |
BatchGet | :batchGet | GET | 批量获取多个资源。如需了解详情,请参阅列表描述。 |
Move | :move | POST | 将资源从一个父级移动到另一个父级,例如 folders.move。 |
Search | :search | GET | List 的替代方法,用于获取不符合 List 语义的数据,例如 services.search。 |
Undelete | :undelete | POST | 恢复之前删除的资源,例如 services.undelete。建议的保留期限为 30 天。
|
五、标准字段
本节介绍了需要类似概念时应使用的一组标准消息字段定义。这将确保相同的概念在不同 API 中具有相同的名称和语义。
名称 | 类型 | 说明 |
---|---|---|
name | string | name 字段应包含相对资源名称。 |
parent | string | 对于资源定义和 List/Create 请求,parent 字段应包含父级相对资源名称。 |
create_time | Timestamp | 创建实体的时间戳。 |
update_time | Timestamp | 最后更新实体的时间戳。注意:执行 create/patch/delete 操作时会更新 update_time。 |
delete_time | Timestamp | 删除实体的时间戳,仅当它支持保留时才适用。 |
expire_time | Timestamp | 实体到期时的到期时间戳。 |
start_time | Timestamp | 标记某个时间段开始的时间戳。 |
end_time | Timestamp | 标记某个时间段或操作结束的时间戳(无论其成功与否)。 |
read_time | Timestamp | 应读取(如果在请求中使用)或已读取(如果在响应中使用)特定实体的时间戳。 |
time_zone | string | 时区名称。它应该是 IANA TZ 名称,例如“America/Los_Angeles”。如需了解详情,请参阅 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones。 |
region_code | string | 位置的 Unicode 国家/地区代码 (CLDR),例如“US”和“419”。如需了解详情,请访问 http://www.unicode.org/reports/tr35/#unicode_region_subtag。 |
language_code | string | BCP-47 语言代码,例如“en-US”或“sr-Latn”。如需了解详情,请参阅 http://www.unicode.org/reports/tr35/#Unicode_locale_identifier。 |
mime_type | string | IANA 发布的 MIME 类型(也称为媒体类型)。如需了解详情,请参阅 https://www.iana.org/assignments/media-types/media-types.xhtml。 |
display_name | string | 实体的显示名称。 |
title | string | 实体的官方名称,例如公司名称。它应被视为 display_name 的正式版本。 |
description | string | 实体的一个或多个文本描述段落。 |
filter | string | List 方法的标准过滤器参数。 |
query | string | 如果应用于搜索方法(即 :search),则与 filter 相同。 |
page_token | string | List 请求中的分页令牌。 |
page_size | int32 | List 请求中的分页大小。 |
total_size | int32 | 列表中与分页无关的项目总数。 |
next_page_token | string | List 响应中的下一个分页令牌。它应该用作后续请求的 page_token 。空值表示不再有结果。 |
order_by | string | 指定 List 请求的结果排序。 |
request_id | string | 用于检测重复请求的唯一字符串 ID。 |
resume_token | string | 用于恢复流式传输请求的不透明令牌。 |
labels | map<string, string> | 表示 Cloud 资源标签。 |
show_deleted | bool | 如果资源允许恢复删除行为,相应的 List 方法必须具有 show_deleted 字段,以便客户端可以发现已删除的资源。 |
update_mask | FieldMask | 它用于 Update 请求消息,该消息用于对资源执行部分更新。此掩码与资源相关,而不是与请求消息相关。 |
validate_only | bool | 如果为 true,则表示仅应验证给定请求,而不执行该请求。 |
系统参数
除标准字段外,Google API 还支持可以在所有 API 方法中使用的一组共同请求参数,这些参数称为“系统参数”。如需了解详情,请参阅系统参数。
六、错误
本章简要介绍了 Google API 的错误模型。它还为开发者提供了正确生成和处理错误的通用指南。
Google API 使用简单的协议无关错误模型,以便我们在不同的 API、API 协议(如 gRPC 或 HTTP)和错误上下文(例如异步、批处理或工作流错误)中能够有一致的体验。
错误模型
错误模型由 google.rpc.Status 在逻辑上定义,该实例将在 API 错误发生时返回给客户端。以下代码段显示了错误模型的总体设计:
package google.rpc;
message Status {
// A simple error code that can be easily handled by the client. The
// actual error code is defined by `google.rpc.Code`.
int32 code = 1;
// A developer-facing human-readable error message in English. It should
// both explain the error and offer an actionable resolution to it.
string message = 2;
// Additional error information that the client code can use to handle
// the error, such as retry delay or a help link.
repeated google.protobuf.Any details = 3;
}
由于大多数 Google API 采用面向资源的 API 设计,因此错误处理遵循相同的设计原则,使用一小组标准错误配合大量资源。例如,服务器没有定义不同类型的“找不到”错误,而是使用一个标准 google.rpc.Code.NOT_FOUND
错误代码并告诉客户端找不到哪个特定资源。状态空间变小降低了文档的复杂性,在客户端库中提供了更好的惯用映射,并降低了客户端的逻辑复杂性,同时不限制是否包含可操作信息。
错误代码
Google API 必须使用 google.rpc.Code 定义的规范错误代码。单个 API 应避免定义其他错误代码,因为开发人员不太可能编写用于处理大量错误代码的逻辑。作为参考,每个 API 调用平均处理 3 个错误代码意味着大多数应用的逻辑只是用于错误处理,这对开发人员而言并非好体验。
错误消息
错误消息应该可以帮助用户轻松快捷地理解和解决 API 错误。通常,在编写错误消息时请考虑以下准则:
- 不要假设用户是您 API 的专家用户。用户可能是客户端开发人员、操作人员、IT 人员或应用的最终用户。
- 不要假设用户了解有关服务实现的任何信息,或者熟悉错误的上下文(例如日志分析)。
- 如果可能,应构建错误消息,以便技术用户(但不一定是 API 开发人员)可以响应错误并改正。
- 确保错误消息内容简洁。如果需要,请提供一个链接,便于有疑问的读者提问、提供反馈或详细了解错误消息中不方便说明的信息。此外,可使用详细信息字段来提供更多信息。
警告:错误消息不属于 API 表面。它们随时都会更改,恕不另行通知。应用代码不得严重依赖于错误消息。
错误详情
Google API 为错误详细信息定义了一组标准错误负载,您可在 google/rpc/error_details.proto 中找到这些错误负载。 它们涵盖了对于 API 错误的最常见需求,例如配额失败和无效参数。与错误代码一样,开发者应尽可能使用这些标准载荷。
只有在可以帮助应用代码处理错误的情况下,才应引入其他错误详细信息类型。如果错误信息只能由人工处理,则应根据错误消息内容,让开发人员手动处理,而不是引入其他错误详细信息类型。
下面是一些示例 error_details
载荷:
RetryInfo
:描述客户端何时可以重试失败的请求,这些内容可能在以下方法中返回:Code.UNAVAILABLE
或Code.ABORTED
QuotaFailure
:描述配额检查失败的方式,这些内容可能在以下方法中返回:Code.RESOURCE_EXHAUSTED
BadRequest
:描述客户端请求中的违规行为,这些内容可能在以下方法中返回:Code.INVALID_ARGUMENT
ErrorInfo
提供既稳定又可扩展的结构化错误信息。
错误信息
ErrorInfo
是标准错误载荷之一。它提供人类和应用可使用的稳定且可扩展错误信息。每个 ErrorInfo
包含 3 条信息:错误域、错误原因和一组错误元数据。如需了解详情,请参阅 ErrorInfo 定义。
HTTP 映射
虽然 proto3 消息具有原生 JSON 编码,但 Google 的 API 平台对 Google JSON REST API 使用了不同的错误架构,以实现向后兼容性。
架构:
// The error schema for Google REST APIs. NOTE: this schema is not used for
// other wire protocols.
message Error {
// This message has the same semantics as `google.rpc.Status`. It has an extra
// field `status` for backward compatibility with Google API Client Library.
message Status {
// This corresponds to `google.rpc.Status.code`.
int32 code = 1;
// This corresponds to `google.rpc.Status.message`.
string message = 2;
// This is the enum version for `google.rpc.Status.code`.
google.rpc.Code status = 4;
// This corresponds to `google.rpc.Status.details`.
repeated google.protobuf.Any details = 5;
}
// The actual error payload. The nested message structure is for backward
// compatibility with Google API client libraries. It also makes the error
// more readable to developers.
Status error = 1;
}
示例:
{
"error": {
"code": 401,
"message": "Request had invalid credentials.",
"status": "UNAUTHENTICATED",
"details": [{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
...
}]
}
}
RPC 映射
不同的 RPC 协议采用不同方式映射错误模型。对于 gRPC,生成的代码和每种支持语言的运行时库为错误模型提供原生支持。您可在 gRPC 的 API 文档中了解更多信息。例如,请参阅 gRPC Java 的 io.grpc.Status。
客户端库映射
Google 客户端库可能会根据语言选择采用不同方式表达错误,以与既定习语保持一致。例如,google-cloud-go 库将返回一个错误,该错误实现与 google.rpc.Status 相同的接口,而 google-cloud-java 将引发异常。
错误本地化
google.rpc.Status 中的 message
字段面向开发人员,必须使用英语。
如果需要面向用户的错误消息,请使用 google.rpc.LocalizedMessage 作为您的详细信息字段。虽然 google.rpc.LocalizedMessage 中的消息字段可以进行本地化,请确保 google.rpc.Status 中的消息字段使用英语。
默认情况下,API 服务应使用经过身份验证的用户的语言区域设置或 HTTP Accept-Language
标头来确定本地化的语言。
处理错误
下面的表格包含在 google.rpc.Code 中定义的所有 gRPC 错误代码及其原因的简短描述。要处理错误,您可以检查返回状态代码的说明并相应地修改您的调用。
HTTP | RPC | 说明 |
---|---|---|
200 | OK | 无错误。 |
400 | INVALID_ARGUMENT | 客户端指定了无效参数。如需了解详情,请查看错误消息和错误详细信息。 |
400 | FAILED_PRECONDITION | 请求无法在当前系统状态下执行,例如删除非空目录。 |
400 | OUT_OF_RANGE | 客户端指定了无效范围。 |
401 | UNAUTHENTICATED | 由于 OAuth 令牌丢失、无效或过期,请求未通过身份验证。 |
403 | PERMISSION_DENIED | 客户端权限不足。可能的原因包括 OAuth 令牌的覆盖范围不正确、客户端没有权限或者尚未为客户端项目启用 API。 |
404 | NOT_FOUND | 找不到指定的资源,或者请求由于未公开的原因(例如白名单)而被拒绝。 |
409 | ABORTED | 并发冲突,例如读取/修改/写入冲突。 |
409 | ALREADY_EXISTS | 客户端尝试创建的资源已存在。 |
429 | RESOURCE_EXHAUSTED | 资源配额不足或达到速率限制。如需了解详情,客户端应该查找 google.rpc.QuotaFailure 错误详细信息。 |
499 | CANCELLED | 请求被客户端取消。 |
500 | DATA_LOSS | 出现不可恢复的数据丢失或数据损坏。客户端应该向用户报告错误。 |
500 | UNKNOWN | 出现未知的服务器错误。通常是服务器错误。 |
500 | INTERNAL | 出现内部服务器错误。通常是服务器错误。 |
501 | NOT_IMPLEMENTED | API 方法未通过服务器实现。 |
503 | UNAVAILABLE | 服务不可用。通常是服务器已关闭。 |
504 | DEADLINE_EXCEEDED | 超出请求时限。仅当调用者设置的时限比方法的默认时限短(即请求的时限不足以让服务器处理请求)并且请求未在时限范围内完成时,才会发生这种情况。 |
错误重试
客户端可能使用指数退避算法重试 503 UNAVAILABLE 错误。 除非另有说明,否则最小延迟应为 1 秒。
对于 429 RESOURCE_EXHAUSTED 错误,客户端可能会在更高层级以最少 30 秒的延迟重试。此类重试仅对长时间运行的后台作业有用。
对于所有其他错误,重试请求可能并不适用。首先确保您的请求具有幂等性,并查看 google.rpc.RetryInfo 以获取指导。
错误传播
如果您的 API 服务依赖于其他服务,则不应盲目地将这些服务的错误传播到您的客户端。在翻译错误时,我们建议执行以下操作:
- 隐藏实现详细信息和机密信息。
- 调整负责该错误的一方。例如,从另一个服务接收
INVALID_ARGUMENT
错误的服务器应该将INTERNAL
传播给它自己的调用者。
生成错误
如果您是服务器开发人员,则应该生成包含足够信息的错误,以帮助客户端开发人员理解并解决问题。同时,您必须重视用户数据的安全性和隐私性,避免在错误消息和错误详细信息中披露敏感信息,因为错误通常会被记录下来并且可能被其他人访问。例如,“客户端 IP 地址不在许可名单 128.0.0.0/8 上”之类的错误消息会披露服务器端政策的相关信息,而用户可能无法访问这些信息。
要生成正确的错误,首先需要熟悉 google.rpc.Code,然后才能为每个错误条件选择最合适的错误代码。服务器应用可以并行检查多个错误条件,并返回第一个错误条件。
下表列出了每个错误代码和恰当的错误消息示例。
HTTP | RPC | 错误消息示例 |
---|---|---|
400 | INVALID_ARGUMENT | 请求字段 x.y.z 是 xxx,预期为 [yyy, zzz] 内的一个。 |
400 | FAILED_PRECONDITION | 资源 xxx 是非空目录,因此无法删除。 |
400 | OUT_OF_RANGE | 参数“age”超出范围 [0,125]。 |
401 | UNAUTHENTICATED | 身份验证凭据无效。 |
403 | PERMISSION_DENIED | 使用权限“xxx”处理资源“yyy”被拒绝。 |
404 | NOT_FOUND | 找不到资源“xxx”。 |
409 | ABORTED | 无法锁定资源“xxx”。 |
409 | ALREADY_EXISTS | 资源“xxx”已经存在。 |
429 | RESOURCE_EXHAUSTED | 超出配额限制“xxx”。 |
499 | CANCELLED | 请求被客户端取消。 |
500 | DATA_LOSS | 请参阅注释。 |
500 | UNKNOWN | 请参阅注释。 |
500 | INTERNAL | 请参阅注释。 |
501 | NOT_IMPLEMENTED | 方法“xxx”未实现。 |
503 | UNAVAILABLE | 请参阅注释。 |
504 | DEADLINE_EXCEEDED | 请参阅备注。 |
注意:由于客户端无法修复服务器错误,因此生成其他错误详细信息没有任何用处。为避免在错误条件下泄露敏感信息,建议不要生成任何错误消息,而仅生成 google.rpc.DebugInfo
错误详细信息。DebugInfo
专为服务器端的日志记录而设计,不得发送到客户端。
google.rpc
软件包定义了一组标准错误载荷,它们优先于自定义错误载荷。下表列出了每个错误代码及其匹配的标准错误负载(如果适用)。
HTTP | RPC | 建议的错误详细信息 |
---|---|---|
400 | INVALID_ARGUMENT | google.rpc.BadRequest |
400 | FAILED_PRECONDITION | google.rpc.PreconditionFailure |
400 | OUT_OF_RANGE | google.rpc.BadRequest |
401 | UNAUTHENTICATED | |
403 | PERMISSION_DENIED | |
404 | NOT_FOUND | google.rpc.ResourceInfo |
409 | ABORTED | |
409 | ALREADY_EXISTS | google.rpc.ResourceInfo |
429 | RESOURCE_EXHAUSTED | google.rpc.QuotaFailure |
499 | CANCELLED | |
500 | DATA_LOSS | |
500 | UNKNOWN | |
500 | INTERNAL | |
501 | NOT_IMPLEMENTED | |
503 | UNAVAILABLE | |
504 | DEADLINE_EXCEEDED |
七、命名规则
为了跨众多 API 长期为开发者提供一致的体验,API 使用的名称都应具有以下特点:
- 简单
- 直观
- 一致
这包括接口、资源、集合、方法和消息的名称。
由于很多开发者不是以英语为母语,所以这些命名惯例的目标之一是确保大多数开发者可以轻松理解 API。对于方法和资源,我们鼓励使用简单、一致和少量的词汇来命名。
- API 中使用的名称应采用正确的美式英语。例如,使用美式英语的 license、color,而非英式英语的 licence、colour。
- 为了简化命名,可以使用已被广泛接受的简写形式或缩写。例如,API 优于 Application Programming Interface。
- 尽量使用直观、熟悉的术语。例如,如果描述移除(和销毁)一个资源,则删除优于擦除。
- 使用相同的名称或术语命名同样的概念,包括跨 API 共享的概念。
- 避免名称过载。使用不同的名称命名不同的概念。
- 避免在 API 的上下文以及范围更大的 Google API 生态系统中使用含糊不清、过于笼统的名称。这些名称可能导致对 API 概念的误解。相反,应选择能准确描述 API 概念的名称。这对定义一阶 API 元素(例如资源)的名称尤其重要。没有需避免名称的明确列表,因为每个名称都必须放在其他名称的上下文中进行评估。实例、信息和服务的名称都曾出现过这类问题。所选择的名称应清楚地描述 API 概念(例如:什么的实例?),并将其与其他相关概念区分开(例如:“alert”是指规则、信号还是通知?)。
- 仔细考虑使用的名称是否可能与常用编程语言中的关键字存在冲突。您可以使用这些名称,但在 API 审核期间可能会触发额外的审查。因此应明智而审慎地使用。
产品名称
产品名称是指 API 的产品营销名称,例如 Google Calendar API。API、界面、文档、服务条款、对帐单和商业合同等信息中使用的产品名称必须一致。Google API 必须使用产品团队和营销团队批准的产品名称。
下表显示了所有相关 API 名称及其一致性的示例。如需详细了解各自名称及其命名惯例,请参阅本页面下方的详细信息。
API 名称 | 示例 |
---|---|
产品名称 | Google Calendar API |
服务名称 | calendar.googleapis.com |
软件包名称 | google.calendar.v3 |
接口名称 | google.calendar.v3.CalendarService |
来源目录 | //google/calendar/v3 |
API 名称 | calendar |
服务名称
服务名称应该是语法上有效的 DNS 名称(遵循 RFC 1035),可以解析为一个或多个网络地址。公开的 Google API 的服务名称采用 xxx.googleapis.com
格式。例如,Google 日历的服务名称是 calendar.googleapis.com
。
如果一个 API 是由多项服务组成,则应采用更容易发现的命名方式。要做到这点,一种方法是使服务名称共享一个通用前缀。例如,build.googleapis.com
和 buildresults.googleapis.com
服务都属于 Google Build API。
软件包名称
API .proto 文件中声明的软件包名称应该与产品名称和服务名称保持一致。软件包名称应该使用单数组件名称,以避免混合使用单数和复数组件名称。软件包名称不能使用下划线。进行版本控制的 API 的软件包名称必须以此版本结尾。例如:
// Google Calendar API
package google.calendar.v3;
与服务无直接关联的抽象 API(例如 Google Watcher API)应该使用与产品名称一致的 proto 软件包名称:
// Google Watcher API
package google.watcher.v1;
API .proto 文件中指定的 Java 软件包名称必须与带有标准 Java 软件包名称前缀(com.
、edu.
、net.
等)的 proto 软件包名称相匹配。例如:
package google.calendar.v3;
// Specifies Java package name, using the standard prefix "com."
option java_package = "com.google.calendar.v3";
集合 ID
集合 ID 应采用复数和 lowerCamelCase
(小驼峰式命名法)格式,并遵循美式英语拼写和语义。例如:events
、children
或 deletedEvents
。
接口名称
为了避免与服务名称(例如 pubsub.googleapis.com
)混淆,术语 “接口名称”是指在 .proto 文件中定义 service
时使用的名称:
// Library is the interface name.
service Library {
rpc ListBooks(...) returns (...);
rpc ...
}
您可以将服务名称视为对一组 API 实际实现的引用,而接口名称则是 API 的抽象定义。
接口名称应该使用直观的名词,例如 Calendar 或 Blob。该名称不得与编程语言及其运行时库(如 File)中的成熟概念相冲突。
在极少数情况下,接口名称会与 API 中的其他名称相冲突,此时应该使用后缀(例如 Api
或 Service
)来消除歧义。
方法名称
服务可以在其 IDL 规范中定义一个或多个远程过程调用 (RPC) 方法,这些方法需与集合和资源上的方法对应。方法名称应采用大驼峰式命名格式并遵循 VerbNoun
的命名惯例,其中 Noun(名词)通常是资源类型。
动词 | 名词 | 方法名称 | 请求消息 | 响应消息 |
---|---|---|---|---|
List | Book | ListBooks | ListBooksRequest | ListBooksResponse |
Get | Book | GetBook | GetBookRequest | Book |
Create | Book | CreateBook | CreateBookRequest | Book |
Update | Book | UpdateBook | UpdateBookRequest | Book |
Rename | Book | RenameBook | RenameBookRequest | RenameBookResponse |
Delete | Book | DeleteBook | DeleteBookRequest | google.protobuf.Empty |
方法名称的动词部分应该使用用于要求或命令的祈使语气,而不是用于提问的陈述语气。
如果关于 API 子资源的方法名称使用提问动词(经常使用陈述语气表示),则容易让人混淆。例如,要求 API 创建一本书,这显然是 CreateBook
(祈使语气),但是询问 API 关于图书发行商的状态可能会使用陈述语气,例如 IsBookPublisherApproved
或 NeedsPublisherApproval
。若要在此类情况下继续使用祈使语气,请使用“check”(CheckBookPublisherApproved
) 和“validate”(ValidateBookPublisher
) 等命令。
方法名称不应包含介词(例如“For”、“With”、“At”、“To”)。通常,带有介词的方法名称表示正在使用新方法,应将一个字段添加到现有方法中,或者该方法应使用不同的动词。
例如,如果 CreateBook
消息已存在且您正在考虑添加 CreateBookFromDictation
,请考虑使用 TranscribeBook
方法。
消息名称
消息名称应该简洁明了。避免不必要或多余的字词。如果不存在无形容词的相应消息,则通常可以省略形容词。例如,如果没有非共享代理设置,则 SharedProxySettings
中的 Shared
是多余的。
消息名称不应包含介词(例如“With”、“For”)。通常,带有介词的消息名称可以通过消息上的可选字段来更好地表示。
请求和响应消息
RPC 方法的请求和响应消息应该分别以带有后缀 Request
和 Response
的方法名称命名,除非方法请求或响应类型为以下类型:
- 一条空消息(使用
google.protobuf.Empty
)、 - 一个资源类型,或
- 一个表示操作的资源
这通常适用于在标准方法 Get
、Create
、Update
或 Delete
中使用的请求或响应。
枚举名称
枚举类型必须使用 UpperCamelCase 格式的名称。
枚举值必须使用 CAPITALIZED_NAMES_WITH_UNDERSCORES 格式。每个枚举值必须以分号(而不是逗号)结尾。第一个值应该命名为 ENUM_TYPE_UNSPECIFIED,因为在枚举值未明确指定时系统会返回此值。
enum FooBar {
// The first value represents the default and must be == 0.
FOO_BAR_UNSPECIFIED = 0;
FIRST_VALUE = 1;
SECOND_VALUE = 2;
}
封装容器
封装 proto2 枚举类型(其中 0
值具有非 UNSPECIFIED
的含义)的消息应该以后缀 Value
来命名,并具有名为 value
的单个字段。
enum OldEnum {
VALID = 0;
OTHER_VALID = 1;
}
message OldEnumValue {
OldEnum value = 1;
}
字段名称
.proto 文件中的字段定义必须使用 lower_case_underscore_separated_names 格式。这些名称将映射到每种编程语言的生成代码中的原生命名惯例。
字段名称不应包含介词(例如“for”、“during”、“at”),例如:
reason_for_error
应该改成error_reason
cpu_usage_at_time_of_failure
应该改成failure_time_cpu_usage
字段名称不应使用后置形容词(名词后面的修饰符),例如:
items_collected
应该改成collected_items
objects_imported
应该改成imported_objects
重复字段名称
API 中的重复字段必须使用正确的复数形式。这符合现有 Google API 的命名惯例和外部开发者的共同预期。
时间和时间段
要表示一个与任何时区或日历无关的时间点,应该使用 google.protobuf.Timestamp
,并且字段名称应该以 time
(例如 start_time
和 end_time
)结尾。
如果时间指向一个活动,则字段名称应该采用 verb_time
的形式,例如 create_time
和 update_time
。请勿使用动词的过去时态,例如 created_time
或 last_updated_time
。
要表示与任何日历和概念(如“天”或“月”)无关的两个时间点之间的时间跨度,应该使用 google.protobuf.Duration
。
message FlightRecord {
google.protobuf.Timestamp takeoff_time = 1;
google.protobuf.Duration flight_duration = 2;
}
如果由于历史性或兼容性原因(包括系统时间、时长、推迟和延迟),您必须使用整数类型表示与时间相关的字段,那么字段名称必须采用以下格式:
xxx_{time|duration|delay|latency}_{seconds|millis|micros|nanos}
message Email {
int64 send_time_millis = 1;
int64 receive_time_millis = 2;
}
如果由于历史性或兼容性原因,您必须使用字符串类型表示时间戳,则字段名称不应该包含任何单位后缀。字符串表示形式应该使用 RFC 3339 格式,例如“2014-07-30T10:43:17Z”。
日期和时间
对于与时区和时段无关的日期,应该使用 google.type.Date
,并且该名称应具有后缀 _date
。如果日期必须表示为字符串,则应采用 ISO 8601 日期格式 YYYY-MM-DD,例如 2014-07-30。
对于与时区和日期无关的时间,应该使用 google.type.TimeOfDay
,并且该名称应具有后缀 _time
。如果时间必须表示为字符串,则应采用 ISO 8601 24 小时制格式 HH:MM:SS[.FFF],例如 14:55:01.672。
message StoreOpening {
google.type.Date opening_date = 1;
google.type.TimeOfDay opening_time = 2;
}
数量
由整数类型表示的数量必须包含度量单位。
xxx_{bytes|width_pixels|meters}
如果数量是条目计数,则该字段应该具有后缀 _count
,例如 node_count
。
列表过滤器字段
如果 API 支持对 List
方法返回的资源进行过滤,则包含过滤器表达式的字段应该命名为 filter
。例如:
message ListBooksRequest {
// The parent resource name.
string parent = 1;
// The filter expression.
string filter = 2;
}
列表响应
List
方法的响应消息(包含资源列表)中的字段名称必须是资源名称本身的复数形式。例如,CalendarApi.ListEvents()
方法必须为返回的资源列表定义一个响应消息ListEventsResponse
,其中包含一个名为 events
的重复字段。
service CalendarApi {
rpc ListEvents(ListEventsRequest) returns (ListEventsResponse) {
option (google.api.http) = {
get: "/v3/{parent=calendars/*}/events";
};
}
}
message ListEventsRequest {
string parent = 1;
int32 page_size = 2;
string page_token = 3;
}
message ListEventsResponse {
repeated Event events = 1;
string next_page_token = 2;
}
驼峰式命名法
除字段名称和枚举值外,.proto
文件中的所有定义都必须使用由 Google Java 样式定义的 UpperCamelCase 格式的名称。
名称缩写
对于软件开发者熟知的名称缩写,例如 config
和 spec
,应该在 API 定义中使用这些缩写,而非完整名称。这将使源代码易于读写。而在正式文档中,应该使用完整名称。示例:
- config (configuration)
- id (identifier)
- spec (specification)
- stats (statistics)
八、常见设计模式
空响应
标准的 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> labels
字段添加到资源定义中。
message Book {
string name = 1;
map<string, string> labels = 2;
}
长时间运行的操作
如果某个 API 方法通常需要很长时间才能完成,您可以通过适当设计,让其向客户端返回“长时间运行的操作”资源,客户端可以使用该资源来跟踪进度和接收结果。 Operation 定义了一个标准接口来使用长时间运行的操作。 各个 API 不得为长时间运行的操作定义自己的接口,以避免不一致性。
操作资源必须作为响应消息直接返回,操作的任何直接后果都应该反映在 API 中。例如,在创建资源时,即便资源表明其尚未准备就绪,该资源也应该出现在 LIST 和 GET 方法中。操作完成后,如果方法并未长时间运行,则 Operation.response
字段应包含本应直接返回的消息。
操作可以使用 Operation.metadata
字段提供有关其进度的信息。即使初始实现没有填充 metadata
字段,API 也应该为此元数据定义消息。
列表分页
可列表集合应该支持分页,即使结果通常很小。
说明:如果某个 API 从一开始就不支持分页,稍后再支持它就比较麻烦,因为添加分页会破坏 API 的行为。 不知道 API 正在使用分页的客户端可能会错误地认为他们收到了完整的结果,而实际上只收到了第一页。
为了在 List
方法中支持分页(在多个页面中返回列表结果),API 应该:
- 在
List
方法的请求消息中定义string
字段page_token
。客户端使用此字段请求列表结果的特定页面。 - 在
List
方法的请求消息中定义int32
字段page_size
。客户端使用此字段指定服务器返回的最大结果数。服务器可以进一步限制单个页面中返回的最大结果数。如果page_size
为0
,则服务器将决定要返回的结果数。 - 在
List
方法的响应消息中定义string
字段next_page_token
。此字段表示用于检索下一页结果的分页令牌。如果值为""
,则表示请求没有进一步的结果。 - 要求在后续请求中重新指定查询参数。
- 仅在页面令牌中引用服务器端会话状态。
- 加密并签署页面令牌中的查询参数,并在每次调用时重新验证并重新授权这些参数。
- 应该是
enum
类型 - 必须命名为
view
。 - 仅限可打印的 ASCII
- RFC 2732 允许的非 ASCII 字符,但对开发者不太友好
- 不能有空格
- 除上述位置外,不能有双引号
- 遵从 RFC 7232 的推荐,避免使用反斜杠,以防止在转义时出现混淆
- Protobuf
Any
类型的表示形式:type.googleapis.com/google.protobuf.Any
- Stackdriver 指标类型:
compute.googleapis.com/instance/cpu/utilization
- 标签键:
cloud.googleapis.com/location
- Kubernetes API 版本:
networking.k8s.io/v1
x-kubernetes-group-version-kind
OpenAPI 扩展程序中的kind
字段。-
如果我们希望获得固定的设计并且有意不想扩展该功能,请使用
bool
类型。例如bool enable_tracing
或bool enable_pretty_print
。 -
如果我们希望获得灵活的设计,但不希望该设计频繁更改,请使用
enum
类型。一般的经验法则是枚举定义每年仅更改一次或更少。例如enum TlsVersion
或enum HttpVersion
。 -
如果我们采用开放式设计或者可以根据外部标准频繁更改设计,请使用
string
类型。必须明确记录支持的值。例如:- 由 Unicode 区域定义的
string region_code
。 - 由 Unicode 语言区域定义的
string language_code
。
- 由 Unicode 区域定义的
-
对于用户元数据、用户设置和其他重要信息,应保留 30 天的数据。例如,监控指标、项目元数据和服务定义。
-
对于大量用户内容,应保留 7 天的数据。例如,二进制 blob 和数据库表。
-
对于暂时性状态或费用昂贵的存储服务,如果可行,应保留 1 天的数据。例如,Memcache 实例和 Redis 服务器。
九、文档
本部分介绍了如何向 API 添加内嵌文档。大多数 API 还拥有概览、教程和简要参考文档,这些内容本设计指南并不涉及。如需了解 API、资源和方法命名,请参阅命名惯例。
proto 文件中的注释格式
使用常用的 Protocol Buffers //
注释格式向 .proto
文件添加注释。
// Creates a shelf in the library, and returns the new Shelf.
rpc CreateShelf(CreateShelfRequest) returns (Shelf) {
option (google.api.http) = { post: "/v1/shelves" body: "shelf" };
}
服务配置中的注释
另一种向 .proto
文件添加文档注释的方法是,您可以在其 YAML 服务配置文件中向 API 添加内嵌文档。如果两个文件中都记录了相同的元素,则 YAML 文件中的文档将优先于 .proto
中的文档。
documentation:
summary: Gets and lists social activities
overview: A simple example service that lets you get and list possible social activities
rules:
- selector: google.social.Social.GetActivity
description: Gets a social activity. If the activity does not exist, returns Code.NOT_FOUND.
...
如果您有多个服务使用相同的 .proto
文件,并且您希望提供服务专用文档,则可能需要使用此方法。YAML 文档规则还允许您向 API 说明添加更详细的 overview
。但一般首选向 .proto
文件添加文档注释。
与向 .proto
添加注释一样,您可以使用 Markdown 在 YAML 文件注释中提供其他格式设置。
API 说明
API 说明是说明 API 功能的短语(以行为动词开头)。在您的 .proto
文件中,API 说明作为注释添加到相应的 service
中,如以下示例所示:
// Manages books and shelves in a simple digital library.
service LibraryService {
...
}
以下是一些 API 说明示例:
- 与世界各地的朋友分享最新动态、照片、视频等。
- 访问云托管的机器学习服务,轻松构建响应数据流的智能应用。
资源说明
资源说明是描述资源表示的内容的句子。如果您需要添加更多细节,请使用更多句子。在您的 .proto
文件中,API 说明作为注释添加到相应的消息类型中,如以下示例所示:
// A book resource in the Library API.
message Book {
...
}
以下是一些资源说明示例:
- 用户待办事项列表中的一项任务。每项任务具有唯一的优先级。
- 用户日历上的一个事件。
字段和参数说明
描述字段或参数定义的名词短语,如以下示例所示:
- 本系列的主题数量。
- 经纬度坐标的精度,以米为单位。 必须是非负数。
- 标记是否为本系列的提交资源返回附件网址值。
series.insert
的默认值为true
。 - 投票信息的容器。仅在记录投票信息时出现。
- 目前未使用或已弃用。
字段和参数说明应该描述哪些值有效和无效。请记住,工程师们会通过一切可能的途径导致服务失败,并且他们无法读取底层代码来澄清任何不清楚的信息。
对于字符串,说明应该描述语法和允许的字符以及任何所需的编码。例如:
- 集合 [A-a0-9] 中的 1-255 个字符
- 遵循 RFC 2332 惯例且以 / 开头的有效网址路径字符串。长度上限为 500 个字符。
说明应该指定任何默认值或行为,但可以省略描述实际为 null 的默认值。
如果字段值是必需、仅限输入、仅限输出,则應該在字段说明开头记录这些值。默认情况下,所有字段和参数都是可选的。例如:
message Table {
// Required. The resource name of the table.
string name = 1;
// Input only. Whether to validate table creation without actually doing it.
bool validate_only = 2;
// Output only. The timestamp when the table was created. Assigned by
// the server.
google.protobuf.Timestamp create_time = 3;
// The display name of the table.
string display_name = 4;
}
十、Protocol Buffers v3
本章将讨论如何在 API 设计中使用 Protocol Buffers。 为了简化开发者体验并提高运行时效率,gRPC API 应使用 Protocol Buffers 版本 3 (proto3) 进行 API 定义。
Protocol Buffers 是一种与语言和平台无关的简单接口定义语言 (IDL),用于定义数据结构模式和编程接口。它支持二进制和文本传输格式,并可在不同平台上与许多不同的线路协议结合使用。
Proto3 是 Protocol Buffers 的最新版本,包括对 proto2 的以下更改:
- 字段存在方法(也称为
hasField
)不适用于原初字段。未设置的初始字段具有语言定义的默认值。- 消息字段的存在方法仍然可用,可以使用编译器生成的
hasField
方法进行测试,或者与 null 或由实现定义的 sentinel 值进行比较。
- 消息字段的存在方法仍然可用,可以使用编译器生成的
- 用户定义的默认字段值不再可用。
- 枚举定义必须从枚举值零开始。
- 必填字段不再可用。
- 扩展程序不再可用,请改用
google.protobuf.Any
。- 由于后向和运行时兼容性原因,特别准许使用
google/protobuf/descriptor.proto
。
- 由于后向和运行时兼容性原因,特别准许使用
- 群组语法已被移除。
之所以移除这些功能,是为了使 API 设计更简单、更稳定、性能更高。例如,在记录消息之前通常需要过滤某些字段,例如移除敏感信息。如果这些字段是必填字段,则无法执行此操作。
如需了解详情,请参阅 Protocol Buffers。
十一、版本控制
本主题介绍 Google API 使用的版本控制政策。通常,这些政策适用于所有 Google 托管式服务。
有时,您需要对 API 进行不向后兼容(或“间断”)的更改。这些类型的更改可能导致依赖于原始功能的代码出现问题或损坏。
Google API 使用版本控制方案来防止重大更改。此外,Google API 仅在某些稳定性级别下提供某些功能,例如 Alpha 版和 Beta 版组件。
注意:Google Cloud 中的稳定性级别与正式版发布阶段(Alpha 版、Beta 版、GA 版)基本一致,但并不相同。Google Cloud 会对此处所列的内容施加自己的其他预期和承诺。
指南
所有 Google API 接口都必须提供一个主要版本号,该版本号在 protobuf 软件包的末尾编码,并作为 REST API 的 URI 路径的第一部分。如果 API 引入了重大更改(例如删除或重命名字段),则该 API 必须增加其 API 版本号,以确保现有用户代码不会突然中断。
注意:上面使用的术语“主要版本”取自语义版本控制。但是,与传统的语义版本控制不同的是,Google API 不得公开次要或补丁版本号。例如,Google API 使用 v1
,而不使用 v1.0
、v1.1
或 v1.4.2
。从用户的角度来看,主要版本可以更新,并且用户无需迁移即可获得新功能。
新的 API 主要版本不得依赖于同一 API 的先前主要版本。某一个 API 可以依赖于其他 API,前提是了解这些 API 的依赖项和稳定性风险。在这种情况下,稳定的 API 版本必须只依赖于其他 API 的最新稳定版本。
在一段时间内,同一 API 的不同版本必须能够在单个客户端应用中同时工作。在该时间段内,客户端可以平稳过渡到新版本。旧版必须必须通过合理且经过良好沟通的弃用期,然后才能关闭。
对于具有 Alpha 版或 Beta 版稳定性的版本,API 必须使用以下两种策略之一在 Protobuf 软件包和 URI 路径的主要版本号后面附加稳定性级别:
- 基于发布版本的版本控制(推荐)
- 基于版本的版本控制
基于发布版本的版本控制
稳定版是在给定稳定性级别接收就地更新的长效版本。主版本的每个稳定性级别不超过一个发布版本。在此策略下,您最多可以使用三种发布版本:Alpha 版、Beta 版和稳定版。
Alpha 版和 Beta 版必须附加其稳定性级别,但稳定版不得附加稳定性级别。例如,v1
是稳定版的可接受版本,但 v1beta
或 v1alpha
不是。同样,v1beta
或 v1alpha
是各自的 Alpha 版和 Beta 版的可接受版本,但 v1
不能作为其中任何一个。每一个发布版本都会获得新的功能和“就地”更新。
Beta 版的功能必须是稳定版功能的超集,而 Alpha 版的功能必须是 Beta 版功能的超集。
弃用 API 功能
API 元素(字段、消息、RPC)可能在任何发布版本中被标记为“已弃用”,表明其已不再使用:
// A representation of a scroll.
// Books are now preferred over scrolls.
message Scroll {
option deprecated = true;
// ...
}
弃用的 API 功能不得从 Alpha 版升级到 Beta 版,而不能从 Beta 版升级到稳定版。换句话说,就是功能不得在任何发布版本中“预先弃用”。
Beta 版的功能可以在其弃用足够时间(建议 180)后移除。对于仅存在于 Alpha 版中的功能,您可以选择是否弃用,该功能可能会被移除,恕不另行通知。如果在移除该功能之前它已在 API 的 Alpha 版中弃用,则 API 应该应用相同的注释,并且可以使用其希望使用的任何时间范围。
基于版本的版本控制
单独版本是 Alpha 版或 Beta 版,预计在其功能整合到稳定版之前,会在有限的时间段内提供,在此之后,我们将关停单独版本。使用基于版本的版本控制策略时,API 在每个稳定性级别上可以有任意数量的单独版本。
注意:基于发布版本的策略和基于版本的策略均会就地更新稳定版。即使使用基于版本的策略,也只有一个稳定的发布版本,而不是单独的稳定版本。
Alpha 和 Beta 版必须附加其稳定性级别,后跟递增的版本号。例如 v1beta1
或 v1alpha5
。API 应该在其文档(如备注)中记录这些版本的时间顺序。
每个 Alpha 版或 Beta 版可以使用向后兼容的更改进行更新。对于 Beta 版,向后兼容的更新应该通过增加版本号并发布带有更改的新版本来创建。例如,如果当前版本为 v1beta1
,则接下来将发布 v1beta2
。
其功能到达稳定版后,应该关闭 Alpha 和 Beta 版本。Alpha 版本随时都可以关闭,而 Beta 版本应该让用户有一个合理的过渡期;建议为 180 天。
十二、兼容字符
本页面详细介绍了版本控制部分提供的重大更改和非重大更改的列表。
何为重大(不兼容)更改,这个问题的答案并不完全明确。此处的指导说明应被视为每个可能更改的指示性列表而非完整列表。
此处列出的规则仅涉及客户端兼容性。API 提供方应该了解自己在部署方面的要求,包括实现详细信息的更改。
总体目标是将服务更新到新的次要版本或应用补丁时客户端不应该受影响。尚在研究的影响类型包括:
- 源兼容性:针对 1.0 编写的代码无法针对 1.1 进行编译
- 二进制兼容性:针对 1.0 编译的代码无法针对 1.1 客户端库来链接/运行。(详细情况取决于客户端平台;这个问题在不同情况下有所不同。)
- 线路兼容性:针对 1.0 构建的应用程序无法与 1.1 服务器通信
- 语义兼容性:一切都在运行,但产生了意想不到的或出乎意料的结果
从另一个角度说:旧客户端应该能与使用同一主要版本号的较新服务器结合使用,当其要更新到新的次要版本时(例如利用新功能),应该可以轻松做到。
除了从基于协议的理论方面来考虑,由于存在涉及生成代码和手写代码的客户端库,因此存在实际考虑因素。测试您正在研究的更改时,应尽可能通过生成新版本的客户端库来测试,并确保其测试仍能通过。
下面的讨论内容将原型消息分为三类:
- 请求消息(例如
GetBookRequest
) - 响应消息(例如
ListBooksResponse
) - 资源消息(例如
Book
,并包括其他资源消息中使用的任何消息)
这些类别的消息具有不同的规则,因为请求消息仅从客户端发送到服务器,响应消息仅从服务器发送到客户端,但资源消息通常可以双向传输。 特别是,可以更新的资源需要从读取/修改/写入周期方面来考虑。
向后兼容的(非重大)更改
向 API 服务定义添加 API 接口
从协议的角度来看,这始终比较安全。唯一需要注意的是,客户端库可能已经使用了手写代码中的新 API 接口名称。如果您的新接口与现有接口完全正交,则不太可能实现;如果它是现有接口的简化版本,则更有可能导致冲突。
向 API 接口添加方法
除非您添加的方法与客户端库中已生成的方法发生冲突,否则这应该没问题。
(可能造成重大后果的例子:如果您有 GetFoo
方法,C# 代码生成器已经创建了 GetFoo
和 GetFooAsync
方法。因此,从客户端库的角度来看,在 API 接口中添加 GetFooAsync
方法将是一个重大更改。)
向方法添加 HTTP 绑定
假设绑定没有引入任何歧义,让服务器响应之前拒绝的网址就是安全的。将现有操作应用于新资源名称模式时,可以执行此操作。
向请求消息添加字段
添加请求字段可以是非重大更改,前提是未指定该字段的客户端将在新版本中采用与旧版本相同的处理方式。
可能错误地执行此操作的最明显示例是使用分页:如果 API 的 v1.0 不包含集合的分页,则无法在 v1.1 中添加它,除非将默认的 page_size
视为无限(这通常是一个坏主意)。否则,希望通过单个请求获得完整结果的 v1.0 客户端可能只收到部分结果,而且不会意识到该集合包含更多资源。
向响应消息添加字段
并非资源(例如 ListBooksResponse
)的响应消息可在不影响客户端的情况下进行扩展,前提是这样不会改变其他响应字段的行为。之前在响应中填充的任何字段都应继续使用相同的语义填充,即使这会引入冗余也如此。
例如,在 1.0 版中的查询响应可能包含 contained_duplicates
的布尔字段,这表示某些结果由于复制而省略。在 1.1 版中,我们可能会在 duplicate_count
字段提供更详细的信息。尽管它在 1.1 版本中是多余的,但 contained_duplicates
字段仍必须填充。
向枚举添加值
只能在请求消息中使用的枚举可以自由扩展以包含新元素。例如,使用资源视图模式可在新的次要版本中添加新视图。客户永远不需要接收这个枚举,因此他们不必知道他们不关心的值。
对于资源消息和响应消息,默认假设客户端应该处理他们不知道的枚举值。但是,API 提供方应该知道编写应用来正确处理新的枚举元素可能很困难。API 所有者应该在遇到未知枚举值时记录预期的客户端行为。
借助 Proto3,客户端可以接收它们不知道的值并重新序列化保持相同值的消息,这样才不会影响读取/修改/写入周期。使用 JSON 格式可在值“名称”未知的情况下发送数值,但服务器通常不知道客户端是否真的知道特定值。因此,JSON 客户端可能知道它们已收到以前未知的值,但它们只会看到名称或数字 - 他们不会同时知道这两者。 在读取/修改/写入循环中将相同的值返回到服务器,此时不应修改该字段,因为服务器应该理解这两种形式。
添加仅限输出的资源字段
可以添加只能由服务器提供的资源实体中的字段。服务器可以验证请求中任何客户端提供的值是否有效,但不能在该值省略时失败。
向后不兼容的(重大)的更改
移除或重命名服务、字段、方法或枚举值
从根本上看,如果客户端代码可能引用某些内容,对其执行移除或重命名操作就是重大更改,必须通过新的主要版本进行。引用旧名称的代码,对于有些语言(例如 C#和 Java)会导致编译失败,对于其他语言则可能导致执行失败或数据丢失。传输格式兼容性与此无关。
更改 HTTP 绑定
此处的“更改”实际上是“删除和添加”。例如,如果您确定确实要支持 PATCH,但您发布的版本支持 PUT,或者您使用了错误的自定义动词名称,则可以添加新绑定,但不能因为相同原因而删除旧绑定,因为移除服务方法是一个重大更改。
更改字段的类型
即使新类型与传输格式兼容,这也可能更改客户端库生成的代码,因此必须通过新的主要版本进行。对于已编译的静态类型语言,这很容易引入编译时错误。
更改资源名称格式
资源不得更改其名称 - 这意味着不能更改集合名称。
与大多数重大更改不同,这也会影响主要版本:如果客户端可以使用 v2.0 访问在 v1.0 中创建的资源(反之亦然),则应在两个版本中使用相同的资源名称。
较容易忽略的是,由于以下原因,有效资源名称集也不应更改:
- 如果它的限制变得更严格,之前成功的请求现在将失败。
- 如果它没有之前记录的限制严格,基于先前文档做出假设的客户端可能无法正常工作。客户很可能采用对允许的字符集和名称长度敏感的方式,将资源名称存储在其他位置。或者,客户很可能执行自己的资源名称验证以遵循文档说明。(例如,在开始支持更长的 EC2 资源 ID 之前,亚马逊为客户提供了大量警告并且有一个迁移期。)
请注意,此类更改可能仅在原型文档中可见。 因此,在审核 CL 是否损坏时,仅查看非评论更改并不够。
更改现有请求的可见行为
客户通常依赖 API 行为和语义,即使此类行为没有得到明确支持或记录。因此,在大多数情况下,更改 API 数据的行为或语义造成的影响将被视为使用者的责任。如果行为未以加密方式隐藏,则应假设用户已发现并将依赖此行为。
由于这个原因(即使数据很无趣),对分页令牌加密也是一个好主意,可以防止用户创建自己的令牌,以及在令牌行为发生更改时影响令牌。
更改 HTTP 定义中的网址格式
除了上面列出的资源名称更改之外,此处需要考虑两种更改:
- 自定义方法名称:虽然并非资源名称的一部分,但自定义方法名称是 REST 客户端发布到的网址的一部分。更改自定义方法名称不应该影响 gRPC 客户端,但公共 API 必须假定它们具有 REST 客户端。
- 资源参数名称:从
v1/shelves/{shelf}/books/{book}
更改为v1/shelves/{shelf_id}/books/{book_id}
不会影响已替代的资源名称,但可能会影响代码生成。
向资源消息添加读取/写入字段
客户端通常会执行读取/修改/写入操作。大多数客户端不会为它们不知道的字段提供值,特别是 proto3,它不支持此操作。您可以指定消息类型(而不是原始类型)的任何缺失字段都表示更新未应用于这些字段,但这使得从实体中显式移除此类字段值变得更加困难。 原初类型(包括 string
和 bytes
)根本无法采用这种方式处理,因为在 proto3 中,将 int32
字段明确指定为 0 与完全不指定没有区别。
如果使用字段掩码执行所有更新,这就不是问题,因为客户端不会隐式覆盖它不知道的字段。但是,这个 API 决策并不寻常:大多数 API 都支持“整个资源”更新。
十三、目录结构
API 服务通常使用 .proto
文件来定义 API 接口,并使用 .yaml
文件来配置 API 服务。每个 API 服务必须在 API 代码库中有一个 API 目录。API 目录应该包含所有 API 定义文件和构建脚本。
每个 API 目录应该具有以下标准布局:
-
API 目录
-
代码库必要条件
BUILD
:构建文件。METADATA
:构建元数据文件。OWNERS
:API 目录所有者。README.md
:有关 API 服务的常规信息。
-
配置文件
{service}.yaml
:基准服务配置文件,google.api.Service
proto 消息的 YAML 表示法。prod.yaml
:生产环境增量服务配置文件。staging.yaml
:模拟环境增量服务配置文件。test.yaml
:测试环境增量服务配置文件。local.yaml
:本地环境增量服务配置文件。
-
文档文件
doc/*
:技术文档文件。它们应采用 Markdown 格式。
-
接口定义
v[0-9]*/*
:每个这样的目录都包含 API 的主要版本,主要是 proto 文件和构建脚本。{subapi}/v[0-9]*/*
:每个{subapi}
目录都包含子 API 的接口定义。每个子 API 可以有自己的独立主要版本。type/*
:proto 文件,包含在不同 API 之间、同一 API 的不同版本之间或 API 与服务实现之间共享的类型。type/*
下的类型定义一旦发布就不应该有破坏性更改。
-
公共 Google API 定义在 GitHub 上发布,请参阅 Google API 代码库。如需详细了解目录结构,请参阅 Service Infrastructure 示例 API。
注意:如果您是 Cloud Endpoints 开发者,则可以按照配置 gRPC 服务的说明配置您的 API 服务。
十四、文件结构
gRPC API 应该使用 proto3 IDL 在 .proto
文件中定义。
文件结构必须将更高级别和更重要的定义置于较低级别和较不重要的项目之前。在每个 proto 文件中,适用的部分应按以下顺序排列:
- 版权和许可声明(如果需要)。
- 采用该顺序的 Proto
syntax
、package
、import
和option
语句。 - API 概览文档,为读取器准备文件的其余部分。
- 按重要性降序排列的 API proto
service
定义。 - 与相应方法采用相同顺序的 RPC 请求和响应
message
定义。每个请求消息必须在其相应的响应消息之前(如果有)。 - 资源
message
定义。父资源必须在其子资源之前定义。
如果单个 proto 文件包含整个 API 接口,应在 API 之后命名:
API | Proto |
---|---|
Library | library.proto |
Calendar | calendar.proto |
大 .proto 文件可能会拆分为多个文件。应根据需要将服务、资源消息和请求/响应消息移到单独的文件中。
我们建议每个服务及其相应的请求和响应使用一个文件。请考虑将此文件命名为 <enclosed service name>.proto
。 对于仅包含资源的 proto 文件,请考虑将此文件简单命名为 resources.proto
。
Proto 文件名
Proto 文件名应该使用 lower_case_underscore_separated_names,并且必须使用扩展名 .proto
。例如:service_controller.proto
。
Proto 选项
为了在不同 API 中生成一致的客户端库,API 开发人员必须在其 .proto
文件中使用一致的 proto 选项。符合本指南的 API 定义必须使用以下文件级 proto 选项:
syntax = "proto3";
// The package name should start with the company name and end with
// the major version.
package google.abc.xyz.v1;
// This option specifies the namespace to be used in C# code. This defaults
// to the PascalCased version of the proto package, which is fine if the
// package name consists of single-word segments.
// For example, a package name of "google.shopping.pets.v1" would use a C#
// namespace of "Google.Shopping.Pets.V1".
// However, if any segment of a package name consists of multiple words,
// this option needs to be specified to avoid only the first word being
// capitalized. For example, a Google Pet Store API might have a package name of
// "google.shopping.petstore.v1", which would mean a C# namespace of
// "Google.Shopping.Petstore.V1". Instead, the option should be used to
// capitalize it properly as "Google.Shopping.PetStore.V1".
//
// For more detail on C#/.NET capitalization rules, see the [Framework Design
// Guidelines](https://msdn.microsoft.com/en-us/library/ms229043).
//
// One corner-case of capitalization: while acronyms are generally
// PascalCased (e.g. Http), two-letter acronyms are normally all in capitals,
// e.g. `IOStream` and `OSVersion`, not `IoStream` and `OsVersion`. However,
// in APIs this should be used carefully, as protoc doesn't know which words
// are abbreviations and which aren't: it would introduce inconsistency to have
// a namespace of (say) `OSLogin` but then a class called `OsDetails` generated
// from a message of the same name. Unless you can be certain that the acronym
// won't crop up in a message or field name, it's safest to stick to regular
// PascalCase.
//
// For pre-releases, the Alpha/Beta should also be capitalized, so "V1Beta1"
// rather than "V1beta1" for example.
option csharp_namespace = "Google.Abc.Xyz.V1";
// This option lets the proto compiler generate Java code inside the package
// name (see below) instead of inside an outer class. It creates a simpler
// developer experience by reducing one-level of name nesting and be
// consistent with most programming languages that don't support outer classes.
option java_multiple_files = true;
// The Java outer classname should be the filename in UpperCamelCase. This
// class is only used to hold proto descriptor, so developers don't need to
// work with it directly.
option java_outer_classname = "XyzProto";
// The Java package name must be proto package name with proper prefix.
option java_package = "com.google.abc.xyz.v1";
// A reasonable prefix for the Objective-C symbols generated from the package.
// It should at a minimum be 3 characters long, all uppercase, and convention
// is to use an abbreviation of the package name. Something short, but
// hopefully unique enough to not conflict with things that may come along in
// the future. 'GPB' is reserved for the protocol buffer implementation itself.
option objc_class_prefix = "GABCX";
// This option specifies the namespace to be used in PHP code. This defaults
// to the PascalCased version of the proto package, which is fine if the
// package name consists of single-word segments.
// For example, a package name of "google.shopping.pets.v1" would use a PHP
// namespace of "Google\\Shopping\\Pets\\V1".
// However, if any segment of a package name consists of multiple words,
// this option needs to be specified to avoid only the first word being
// capitalized. For example, a Google Pet Store API might have a package name of
// "google.shopping.petstore.v1", which would mean a PHP namespace of
// "Google\\Shopping\\Petstore\\V1". Instead, the option should be used to
// capitalize it properly as "Google\\Shopping\\PetStore\\V1".
//
// For pre-releases, the Alpha/Beta should not be capitalized, so "V1beta1"
// rather than "V1Beta1" for example. Note that this is different from the
// capitalization used in the csharp_namespace option for pre-releases.
option php_namespace = "Google\\Abc\\Xyz\\V1";