扩展声明

详细描述扩展声明是什么、为什么需要它们以及如何使用它们。

简介

此页面详细描述了扩展声明是什么、为什么需要它们以及如何使用它们。

注意:Proto3 不支持扩展(除了声明自定义选项)。扩展在 proto2 和版本中得到完全支持。

如果您需要了解扩展的介绍,请阅读此扩展指南

动机

扩展声明旨在在常规字段和扩展之间取得一个良好的平衡。与扩展一样,它们避免了对字段的消息类型的依赖,因此在难以或不可能去除未使用消息的环境中,可以产生更精简的构建图和更小的二进制文件。与常规字段一样,字段名称/编号出现在封闭消息中,这使得更容易避免冲突并查看已声明字段的便捷列表。

使用扩展声明列出已占用的扩展编号,使用户更容易选择可用的扩展编号并避免冲突。

用法

扩展声明是扩展范围的一个选项。与 C++ 中的向前声明类似,您可以声明扩展字段的字段类型、字段名称和基数(单数或重复),而无需导入包含完整扩展定义的 .proto 文件。

syntax = "proto2";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 4,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      repeated: true },
    declaration = {
      number: 999,
      full_name: ".foo.package.bar",
      type: "int32"}];
}

此语法具有以下语义

  • 如果范围大小允许,可以在单个扩展范围内定义具有不同扩展编号的多个 declaration
  • 如果扩展范围有任何声明,则所有该范围的扩展也必须声明。这可以防止添加未声明的扩展,并强制任何新扩展使用该范围的声明。
  • 给定的消息类型(.logs.proto.ValidationAnnotations)不需要事先定义或导入。我们只检查它是否是一个有效的名称,可能在另一个 .proto 文件中定义。
  • 当此或其他 .proto 文件定义此消息 (Foo) 的具有此名称或编号的扩展时,我们会强制执行扩展的编号、类型和完整名称与此处前向声明的匹配。

警告:避免对扩展范围组(例如 extensions 4, 999)使用声明。目前尚不清楚声明适用于哪个扩展范围,并且目前不支持。

扩展声明期望两个具有不同包的扩展字段

package my.package;
extend Foo {
  repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
  optional int32 bar = 999;
}

保留声明

扩展声明可以标记为 reserved: true 以指示它不再被积极使用,并且扩展定义已被删除。请勿删除扩展声明或编辑其 typefull_name

reserved 标签与常规字段的保留关键字分开,并且不需要拆分扩展范围

syntax = "proto2";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 500,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      reserved: true }];
}

使用在声明中reserved的编号的扩展字段定义将无法编译。

在 descriptor.proto 中的表示

扩展声明在 descriptor.proto 中表示为 proto2.ExtensionRangeOptions 中的字段

message ExtensionRangeOptions {
  message Declaration {
    optional int32 number = 1;
    optional string full_name = 2;
    optional string type = 3;
    optional bool reserved = 5;
    optional bool repeated = 6;
  }
  repeated Declaration declaration = 2;
}

反射字段查找

扩展声明不会从正常的字段查找函数(如 Descriptor::FindFieldByName()Descriptor::FindFieldByNumber())返回。与扩展一样,它们可以通过扩展查找例程(如 DescriptorPool::FindExtensionByName())发现。这是一个明确的选择,它反映了声明不是定义,并且没有足够的信息来返回完整的 FieldDescriptor

从 TextFormat 和 JSON 的角度来看,已声明的扩展仍然表现得像常规扩展一样。这也意味着将现有字段迁移到已声明的扩展将需要首先迁移该字段的任何反射使用。

使用扩展声明分配编号

扩展使用字段编号,就像普通字段一样,因此每个扩展都必须分配一个在其父消息中唯一的编号非常重要。我们建议使用扩展声明来声明父消息中每个扩展的字段编号和类型。扩展声明充当父消息所有扩展的注册表,并且 protoc 将强制执行没有字段编号冲突。添加新扩展时,通常只需将先前添加的扩展编号加一来选择下一个可用编号。

提示:对于MessageSet有一个特殊指南,它提供了一个脚本来帮助选择下一个可用编号。

每当您删除扩展时,请确保将字段编号标记为 reserved 以消除意外重复使用的风险。

此约定仅为建议 - protobuf 团队没有能力或愿望强迫任何人遵守每个可扩展消息的约定。如果您作为可扩展 proto 的所有者不想通过扩展声明协调扩展编号,您可以选择通过其他方式进行协调。但是,请务必小心,因为意外重复使用扩展编号可能会导致严重问题。

一种规避此问题的方法是完全避免扩展并改用google.protobuf.Any。对于前端存储的 API 或客户端关心 proto 内容但接收它的系统不关心的直通系统,这可能是一个不错的选择。

重复使用扩展编号的后果

扩展是在容器消息外部定义的字段;通常在单独的 .proto 文件中。这种定义的分布使得两个开发人员很容易意外地为同一个扩展字段编号创建不同的定义。

更改扩展定义的后果对于扩展和标准字段是相同的。重复使用字段编号会导致对如何从线格式解码 proto 产生歧义。protobuf 线格式很精简,没有提供很好的方法来检测使用一个定义编码并使用另一个定义解码的字段。

这种歧义可能会在短时间内显现出来,例如客户端使用一个扩展定义而服务器使用另一个扩展定义进行通信。

这种歧义也可能在较长时间内显现出来,例如存储使用一个扩展定义编码的数据,然后稍后使用第二个扩展定义检索和解码。如果在数据编码和存储后删除了第一个扩展定义,则此长期情况可能难以诊断。

结果可能是

  1. 解析错误(最佳情况)。
  2. PII/SPII 泄漏 - 如果使用一个扩展定义写入 PII 或 SPII,并使用另一个扩展定义读取。
  3. 数据损坏 - 如果使用“错误”的定义读取数据、修改和重写。

数据定义歧义几乎肯定会至少导致某人花费时间进行调试。它也可能导致数据泄漏或损坏,需要数月才能清理。

使用技巧

切勿删除扩展声明

删除扩展声明为将来意外重复使用打开了大门。如果不再处理扩展并且已删除定义,则可以将扩展声明标记为保留

切勿为新的扩展声明使用 reserved 列表中的字段名称或编号

保留的编号可能过去曾用于字段或其他扩展。

由于在使用 textproto 时可能存在歧义,因此不建议使用保留字段的 full_name

切勿更改现有扩展声明的类型

更改扩展字段的类型可能导致数据损坏。

如果扩展字段是枚举或消息类型,并且该枚举或消息类型正在重命名,则需要更新声明名称,并且是安全的。为了避免中断,类型、扩展字段定义和扩展声明的更新应全部在同一个提交中完成。

重命名扩展字段时请谨慎

虽然重命名扩展字段对于线格式来说是可以的,但它可能会破坏 JSON 和 TextFormat 解析。