兼容性
本章提供了有关 版本控制 章节中提供的破坏性和非破坏性修改列表的详细说明。
什么算是一个破坏性(不兼容)的变化并没有明确的定义。本指南应该被视为指示性的,而不是每一种可能变化的全面清单。
这里列出的规则只涉及客户端兼容性。预期API生产者明白在部署方面的要求,包括实现细节的变化。
一般目的是,服务端更新到一个新的minor版本或patch版本不该破坏客户端。可预期的破坏类型有:
- 源代码兼容性:针对1.0编写的代码无法在1.1版本编译
- 二进制兼容性:编译的1.0版本的库在1.1客户端库情况下无法链接/运行。(具体细节取决于客户端平台;在不同情况下有不同表现)。
- 传输兼容性:针对1.0构建的应用程序无法与1.1服务器通信
- 语义兼容性:可以运行,但产生无意义的或令人惊讶的结果
换句话说:旧客户端应该能够兼容同一major版本号的较新的服务器,并且当后者试图更新到新的minor版本(例如利用新功能)时,可以很容易做到。
除了理论上的基于协议的考虑之外,也有出于客户端库生成代码和手写代码的实际考虑。尽可能思考在生成新版本的客户端库过程中会产生哪些变化,针对它们写测试用例,并确保其通过测试。
下面的讨论将原始消息分为三类:
- 请求消息(例如
GetBookRequest
) - 响应消息(例如
ListBooksResponse
) - 资源消息(例如
Book
,以及包括在其他资源消息中使用的任何消息)
这些类别具有不同的规则,因为请求消息只能从客户端发送到服务器,响应消息只能从服务器发送到客户端,但通常资源消息是双向发送。特别强调,对可更新的资源,需要考虑其 读取/修改/写入 循环的顺序。
向后兼容(非破坏性)的修改
给API服务定义添加API接口
从协议的角度来看,这始终是安全的。 唯一需要注意的是,客户端库可能已经在手写的代码中使用了您的新API接口名。 如果新接口与现有接口完全正交,那就没问题(不可能正交); 如果新接口是现有接口的简化版本,则更有可能导致冲突。
给API接口添加方法
除非你添加的方法和客户端库已经生成的方法冲突,否则应该没问题。
(举一个破坏性的例子:如果你有一个 GetFoo
方法,C#代码生成器已经创建了GetFoo
和GetFooAsync
方法.在API接口中添加一个GetFooAsync
方法,从客户端角度,这就是一个破坏性的修改。)
给方法添加HTTP绑定
假设绑定不引入任何歧义,使得服务器响应以前会拒绝的URL,这就是安全的。 当将现有操作应用于新资源名称模式时,可以这样做。
给请求消息添加字段
只要客户端在新版和旧版中对该字段的处理不保持一致,添加请求字段就是兼容的。
最显而易见的反面例子是分页:如果v1.0的API在集合中不包括分页,则不能在v1.1中添加分页,除非新版中的page_size
默认为无限大(这通常是一个坏主意)。 否则,希望在单个请求中获取完整结果的v1.0客户端可能会收到不完整的结果,并且不知道结果集包含更多资源。
给响应消息添加字段
在不改变其他响应字段的行为的前提下,非资源(例如,ListBooksResponse
)的响应消息可以扩展而不必破坏客户端的兼容性。即使会引入冗余,先前在响应中填充的任何字段应继续使用相同的语义填充。
例如,1.0中的查询响应可能包含一个布尔字段contained_duplicates
,用来标示某些结果因为重复而被忽略。 在1.1中,我们可能会在duplicate_count
字段中提供更详细的信息。 即使从1.1版本的角度来看它是冗余的,仍然必须填充contained_duplicates
字段。
给枚举(enum)添加值
仅在请求消息中使用的枚举可以自由地扩展新值。例如,使用 资源视图 模式,可以在新的minor版本中添加新视图。客户端从来不需要接收这个枚举,所以它们不必知道它们不关心的值。
对于资源消息和响应消息,缺省假设客户端应该处理他们不知道的枚举值。然而,API生产者应该意识到,编写能正确处理新枚举值的应用程序可能很困难。 API所有者应该在文档里描述当遇到未知枚举值时,预期的客户端行为。
Proto3允许客户端接收不明意义并保留相同值的消息,因此不会中断 读取/修改/写入 周期。
JSON格式允发送一个名称是未知的值,但是服务器端显然不知道客户端是否能正确理解该字段的确切值。JSON客户端可能意识到已经接收了一个未知的值,要么识别出字符串”name”,要么是数字,不可能两者都识别。客户端会在 读取/修改/写入 周期中原封不动的将值返回服务器,因为服务器应该理解这两种格式。
给仅输出的资源添加字段
仅由服务器提供的资源实体中可以添加字段。 服务器可以验证请求中的任何客户端提供的值,但是如果省略该值,服务器禁止失败。
向后不兼容(破坏性)的修改
删除或重命名服务,字段,方法或枚举值
从根本上说,如果客户端代码可以引用某些东西,那么删除或重命名它都是不兼容的变化,这时必须修改major版本号。 引用旧名称的代码将导致某些语言(例如C#和Java)在编译期出现故障,并可能导致其他语言的执行时失败或丢失数据。 传输格式兼容性与此无关。
修改HTTP绑定
“修改”在这里特指“删除和添加”。 例如,如果您确定要支持PATCH方法,但是您发布的版本支持PUT方法,或者您使用了错误的自定义方法名称,你可以添加新的绑定,但禁止删除旧绑定,原因同删除一个服务方法一样,都是一个不兼容的变化。
修改字段的类型
即使新类型是传输格式兼容的,这也可能会导致客户端库生成的代码发生变化,因此必须增加major版本号。 对于编译型静态语言来说,会容易引入编译错误。
修改资源命名格式
资源禁止修改其名称 - 这意味着集合名称不能修改。
与大多数不兼容修改不同,这也影响major版本号:如果预期客户端使用v2.0 API访问在v1.0 API中创建的资源(反之亦然),则应在两个版本中使用相同的资源名称。
更细微地讲,有效资源名称的集合也不应该修改,原因如下:
如果变得更加严格,则以前成功的请求现在将失败。
如果它变得更宽松,那么基于前面文档做出的客户端可能会不兼容。客户端很可能在其他地方存储资源名称,可能对允许的字符集和名称的长度敏感。或者,客户端可能正在执行自己的资源名称验证。 (例如,在允许更长的EC2资源ID时,亚马逊给客户发出了很多警告,并且提供了一个迁移期)
注意,这样的改变只能在proto文档中可见。因此,当审查CL的兼容性时,仅检查非注释部分修改是不够的。
修改现有请求的可见行为
客户端通常依赖于API行为和语义,即使这样的行为没有被明确支持或记录。 因此,在大多数情况下,修改API数据的行为或语义将被消费者视为是破坏性的。 如果行为没有加密隐藏,您应该假设用户已经发现它,并将依赖于它。 例如,用户对AWS EC2资源标识符进行了逆向。
因此,加密分页令牌(即使数据没有啥意义)是一个好主意,以防止用户创建自己的令牌,有潜在的令牌行为改变时可能会不兼容的风险。
修改HTTP定义中的URL格式
除了上面列出的资源名称修改外,有两种修改需要考虑:
自定义方法名称:虽然不是资源名称的一部分,但自定义方法名称是提交到REST客户端的URL的一部分。 修改自定义方法名称不应破坏gRPC客户端,但公共API必须假定它们具有REST客户端。
资源参数名称:从
v1/shelves/{shelf}/books/{book}
修改为v1/shelves/{shelf_id}/books/{book_id}
不会影响替代资源名称,但可能会影响代码生成。
给资源消息添加 读取/写入 字段
客户端将经常执行 读取/修改/写入 操作。 大多数客户端不会为他们不知道的字段提供值,而且proto3也不支持。 您可以指定任何缺少消息类型(而不是原始类型)的字段,这意味着更新不适用于这些字段,也使得从实体中显式移除该字段值变得更加困难。 原始类型(包括字符串和字节)不能这样处理,因为在显式指定int32字段为0和没有指定之间,proto3的表现没有什么不同。
如果所有更新都是使用字段掩码执行,这就不是问题,因为客户端不会隐式地覆盖其不知道的字段。 然而,这将是一个不寻常的API决策:大多数API允许“整个资源”更新。