常见设计模式

空响应

标准的 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 应该为资源定义添加 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_size0,则服务器将决定要返回的结果数。
  • List 方法的响应消息中定义 string 字段 next_page_token。此字段表示用于检索下一页结果的分页令牌。如果值为 "",则表示请求没有进一步的结果。

要检索下一页结果,客户端应该在后续的 List 方法调用中(在请求消息的 page_token 字段中)传递响应的 next_page_token 值:

rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);

message ListBooksRequest {
  string parent = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListBooksResponse {
  repeated Book books = 1;
  string next_page_token = 2;
}

当客户端传入除页面令牌之外的查询参数时,如果查询参数与页面令牌不一致,则服务必须使请求失败。

页面令牌内容应该是可在网址中安全使用的 base64 编码的协议缓冲区。 这使得内容可以在避免兼容性问题的情况下演变。如果页面令牌包含潜在的敏感信息,则应该对该信息进行加密。服务必须通过以下方法之一防止篡改页面令牌导致数据意外暴露:

  • 要求在后续请求中重新指定查询参数。
  • 仅在页面令牌中引用服务器端会话状态。
  • 加密并签署页面令牌中的查询参数,并在每次调用时重新验证并重新授权这些参数。

分页的实现也可以提供名为 total_sizeint32 字段中的项目总数。

列出子集合

有时,API 需要让客户跨子集执行 List/Search 操作。例如,“API 图书馆”有一组书架,每个书架都有一系列书籍,而客户希望在所有书架上搜索某一本书。在这种情况下,建议在子集合上使用标准 List,并为父集合指定通配符集合 ID "-"。对于“API 图书馆”示例,我们可以使用以下 REST API 请求:

GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx

从子集合中获取唯一资源

有时子集合中的资源具有在其父集合中唯一的标识符。此时,在不知道哪个父集合包含它的情况下使用 Get 检索该资源可能很有用。在这种情况下,建议对资源使用标准 Get,并为资源在其中是唯一的所有父集合指定通配符集合 ID "-"。例如,在 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 应该命名为 ENUM_TYPE_UNSPECIFIED。如果存在共同的默认行为,则在未明确指定枚举值时,应该使用该值。如果没有共同的默认行为,则在使用 0 值时应该予以拒绝,并显示错误 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 设计人员必须使用以下扩展巴科斯范式(Extended Backus-Naur Form,简写为“EBNF”)语法的变体来定义这样的语法:

Production  = name "=" [ Expression ] ";" ;
Expression  = Alternative { "|" Alternative } ;
Alternative = Term { Term } ;
Term        = name | TOKEN | Group | Option | Repetition ;
Group       = "(" Expression ")" ;
Option      = "[" Expression "]" ;
Repetition  = "{" Expression "}" ;

整数类型

在 API 设计中,不应该使用 uint32fixed32 等无符号整数类型,因为某些重要的编程语言和系统(如 Java,JavaScript 和 OpenAPI)不太支持它们。并且它们更有可能导致溢出错误。另一个问题是,不同的 API 很可能会对同一事件使用不匹配的有符号和无符号类型。

当有符号整数类型用于负值无意义的事物(例如大小或超时)时,值 -1 (且仅有 -1可以用于表示特殊含义,例如文件结尾 (EOF)、无限超时、无限制配额限制或未知年龄。必须明确记录此类用法以避免混淆。如果隐式默认值 0 的行为不是非常明显,API 提供方也应对其进行记录。

部分响应

有时,API 客户端只需要响应消息中的特定数据子集。为了支持此类用例,某些 API 平台为部分响应提供原生支持。Google API Platform 通过响应字段掩码来支持它。

任何 REST API 调用都有一个隐式系统查询参数 $fields,它是 google.protobuf.FieldMask 值的 JSON 表示形式。在发送回客户端之前,响应消息将由 $fields 过滤。API 平台会自动为所有 API 方法处理此逻辑。

GET https://library.googleapis.com/v1/shelves?$fields=shelves.name
GET https://library.googleapis.com/v1/shelves/123?$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 {
  // Not specified, equivalent to BASIC.
  BOOK_VIEW_UNSPECIFIED = 0;

  // Server responses only include author, title, ISBN and unique book ID.
  // The default value.
  BASIC = 1;

  // Full representation of the book is returned in server responses,
  // including contents of the book.
  FULL = 2;
}

message ListBooksRequest {
  string name = 1;

  // Specifies which parts of the book resource should be returned
  // in the response.
  BookView view = 2;
}

此构造将映射到网址中,例如:

GET https://library.googleapis.com/v1/shelves/shelf1/books?view=BASIC

您可以在本设计指南的标准方法一章中找到有关定义方法、请求和响应的更多信息。

ETag

ETag 是一个不透明标识符,允许客户端发出条件请求。 为了支持 ETag,API 应该在资源定义中包含字符串字段 etag,并且其语义必须符合 ETag 的常见用法。通常,etag 包含服务器计算的资源的指纹。如需了解更多详情,请参阅 WikipediaRFC 7232

ETag 可以被强验证或弱验证,其中弱验证的 ETag 以 W/ 为前缀。在本上下文中,强验证意味着具有相同 ETag 的两个资源具有逐字节相同的内容和相同的额外字段(即,内容-类型)。这意味着强验证的 ETag 允许缓存部分响应并在稍后组合。

相反,具有相同的弱验证 ETag 值的资源意味着表示法在语义上是等效的,但不一定逐字节相同,因此不适合字节范围请求的响应缓存。

例如:

// This is a strong ETag, including the quotes.
"1a2f3e4d5b6c7c"
// This is a weak ETag, including the prefix and quotes.
W/"1a2b3c4d5ef"

请牢记,引号是 ETag 值的一部分并且必须存在,以符合 RFC 7232。这意味着 ETag 的 JSON 表示法最终会对引号进行转义。例如,ETag 在 JSON 资源正文中表示为:

// Strong
{ "etag": "\"1a2f3e4d5b6c7c\"", "name": "...", ... }
// Weak
{ "etag": "W/\"1a2b3c4d5ef\"", "name": "...", ... }

ETag 中允许的字符摘要:

  • 仅限可打印的 ASCII
    • RFC 2732 允许的非 ASCII 字符,但对开发者不太友好
  • 不能有空格
  • 除上述位置外,不能有双引号
  • 遵从 RFC 7232 的推荐,避免使用反斜杠,以防止在转义时出现混淆

输出字段

API 可能希望区分客户端提供的作为输入的字段,以及由服务器返回的仅在特定资源上输出的字段。对于仅限输出的字段,应该注释字段特性。

请注意,如果在请求中设置了或在 google.protobuf.FieldMask 中包括了仅输出字段,则服务器必须接受请求并且不出现错误。服务器必须忽略仅限输出字段的存在及任何提示。此建议的原因是因为客户端经常将服务器返回的资源作为另一个请求的输入重新使用,例如,检索到的 Book 稍后将在 UPDATE 方法中被重新使用。如果对仅限输出字段进行验证,则会导致客户端需要额外清除仅限输出字段。

import "google/api/field_behavior.proto";

message Book {
  string name = 1;
  Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
}

单例资源

当只有一个资源实例存在于其父资源中(如果没有父资源,则在 API 中)时,可以使用单例资源。

单例资源必须省略标准的 CreateDelete 方法;在创建或删除父资源时即隐式创建或删除了单例资源(如果没有父资源,则单例资源隐式存在)。必须使用标准的 GetUpdate 方法,以及任意适合您的用例的自定义方法访问该资源。

例如,具有 User 资源的 API 可以将每个用户的设置公开为 Settings 单例。

rpc GetSettings(GetSettingsRequest) returns (Settings) {
  option (google.api.http) = {
    get: "/v1/{name=users/*/settings}"
  };
}

rpc UpdateSettings(UpdateSettingsRequest) returns (Settings) {
  option (google.api.http) = {
    patch: "/v1/{settings.name=users/*/settings}"
    body: "settings"
  };
}

[...]

message Settings {
  string name = 1;
  // Settings fields omitted.
}

message GetSettingsRequest {
  string name = 1;
}

message UpdateSettingsRequest {
  Settings settings = 1;
  // Field mask to support partial updates.
  FieldMask update_mask = 2;
}

流式半关闭

对于任何双向或客户端流传输 API,服务器应该依赖 RPC 系统提供的、客户端发起的半关闭来完成客户端流。无需定义显式完成消息。

客户端需要在半关闭之前发送的任何信息都必须定义为请求消息的一部分。

网域范围名称

网域范围名称是以 DNS 域名为前缀的实体名称,旨在防止名称发生冲突。当不同的组织以分散的方式定义其实体名称时,这种设计模式很有用。其语法类似于没有架构的 URI。

网域范围名称广泛用于 Google API 和 Kubernetes API,例如:

  • 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 字段。

布尔值与枚举与字符串

在设计 API 方法时,您通常会为特定功能(例如启用跟踪或停用缓存)提供一组选择。实现此目的的常用方法是引入 boolenumstring 类型的请求字段。对于给定用例,要使用哪种正确的类型通常不是很明显。推荐的选项如下:

  • 如果我们希望获得固定的设计并且有意不想扩展该功能,请使用 bool 类型。例如 bool enable_tracingbool enable_pretty_print

  • 如果我们希望获得灵活的设计,但不希望该设计频繁更改,请使用 enum 类型。一般的经验法则是枚举定义每年仅更改一次或更少。例如 enum TlsVersionenum HttpVersion

  • 如果我们采用开放式设计或者可以根据外部标准频繁更改设计,请使用 string 类型。必须明确记录支持的值。例如:

数据保留

在设计 API 服务时,数据保留是服务可靠性相当重要的部分。通常,用户数据会被软件错误或人为错误误删。没有数据保留和相应的取消删除功能,一个简单的错误就可能对业务造成灾难性的影响。

通常,我们建议 API 服务采用以下数据保留政策:

  • 对于用户元数据、用户设置和其他重要信息,应保留 30 天的数据。例如,监控指标、项目元数据和服务定义。

  • 对于大量用户内容,应保留 7 天的数据。例如,二进制 blob 和数据库表。

  • 对于暂时性状态或费用昂贵的存储服务,如果可行,应保留 1 天的数据。例如,Memcache 实例和 Redis 服务器。

在数据保留期限期间,可以删除数据而不会丢失数据。如果免费提供数据保留的成本很高,则服务可以提供付费的数据保留。

大型载荷

联网 API 通常依赖于多个网络层作为其数据路径。大多数网络层对请求和响应大小有硬性限制。32 MB 是很多系统中常用的限制。

在设计处理大于 10 MB 的载荷的 API 方法时,我们应该谨慎选择合适的策略,以确保易用性和满足未来增长的需求。对于 Google API,我们建议使用流式传输或媒体上传/下载来处理大型载荷。如使用流式传输,服务器会逐步地同步处理大量数据,例如 Cloud Spanner API。如使用媒体,大量数据会流经大型存储系统(如 Google Cloud Storage),服务器可以异步处理数据,例如 Google Drive API。

可选原初字段

Protocol Buffers v3 (proto3) 支持 optional 原初字段,它与许多编程语言的 nullable 类型等效。它们可用于区分空值与未设置的值。

在实践中,开发者很难正确地处理可选字段。 大多数 JSON HTTP 客户端库(包括 Google API 客户端库)都无法区分 proto3 int32google.protobuf.Int32Valueoptional int32。如果备用设计同样清晰并且不需要可选的原初字段,则首选方案是使用备用设计。如果不使用可选原初字段会增加复杂性或造成歧义,请使用可选的原初字段。将来请勿使用封装容器类型。通常情况下,API 设计者应该使用简单原初类型(如 int32),以保证简单性和一致性。