- Protocol Buffers 教程
- Protocol Buffers - 首页
- Protocol Buffers - 简介
- Protocol Buffers - 基本应用
- Protocol Buffers - 结构
- Protocol Buffers - 消息
- Protocol Buffers - 字符串
- Protocol Buffers - 数字
- Protocol Buffers - 布尔值
- Protocol Buffers - 枚举
- Protocol Buffers - 重复字段
- Protocol Buffers - 映射
- Protocol Buffers - 嵌套类
- Protocol Buffers - 可选字段和默认值
- Protocol Buffers - 语言无关性
- Protocol Buffers - 复合数据类型
- Protocol Buffers - 命令行使用
- Protocol Buffers - 定义更新规则
- Protocol Buffers - 与 Kafka 集成
- Protocol Buffers - 其他语言
- Protocol Buffers 有用资源
- Protocol Buffers - 快速指南
- Protocol Buffers - 有用资源
- Protocol Buffers - 讨论
Protocol Buffers - 定义更新规则
概述
假设你已经编写了将在生产环境中使用的proto文件的定义。将来显然会有需要更改此定义的时候。在这种情况下,必须使我们所做的更改遵守某些规则,以便更改具有向后兼容性。让我们通过一些“应该做”和“不应该做”的例子来看一下。
在写入器中添加一个新字段,而读取器保留旧版本的代码。
假设你决定添加一个新字段。理想情况下,要添加新字段,必须同时更新写入器和读取器。但是,在大规模部署中,这是不可能的。在某些情况下,写入器已更新,但读取器尚未更新新字段。这就是上述情况发生的地方。让我们来看一下实际情况。
继续我们的剧院示例,假设我们的proto文件中只有一个'name'标签。以下是我们需要用来指示 Protobuf 的语法:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; }
要使用 Protobuf,我们现在必须使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=. theater.proto
上述命令应该创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter.java
package com.tutorialspoint.theater; package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个读取器来读取剧院信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener"
现在让我们执行读取器从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener"
未知字段
我们根据 Protobuf 定义编写了一个简单的字符串,并且读取器能够读取该字符串。我们还看到读取器没有不知道的未知字段。
但是现在,假设我们要向我们的 Protobuf 定义中添加一个新的字符串'address'。现在它看起来像这样:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
我们还将更新我们的写入器并添加一个address字段:
Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build();
在编译之前,将上一次编译生成的 JAR 文件重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
现在让我们执行读取器从同一个文件读取,但使用旧的 JAR 文件:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output Reading from file theater_protobuf_output name: "Silver Screener" 2: "212, Maple Street, LA, California" Unknown fields: 2: "212, Maple Street, LA, California"
从输出的最后一行可以看出,旧的读取器不知道新写入器添加的address字段。它只是显示了“新写入器 - 旧读取器”组合是如何工作的。
删除字段
假设你决定删除一个现有字段。理想情况下,要使删除的字段立即生效,必须同时更新写入器和读取器。但是,在大规模部署中,这是不可能的。在某些情况下,写入器已更新,但读取器尚未更新。在这种情况下,读取器仍将尝试读取已删除的字段。让我们来看一下实际情况。
继续我们的剧院示例,假设我们的proto文件中只有两个标签。以下是我们需要用来指示 Protobuf 的语法:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
要使用 Protobuf,我们现在必须使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=java/src/main/java proto_files\theater.proto
上述命令应该创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个读取器来读取剧院信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
现在让我们执行读取器从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
所以,这里没有什么新东西,我们只是根据 Protobuf 定义编写了一个简单的字符串,并且读取器能够读取该字符串。
但是现在,假设我们要从我们的 Protobuf 定义中删除字符串'address'。因此,定义将如下所示:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; }
我们还将更新我们的写入器如下:
Theater theater = Theater.newBuilder() .setName("Silver Screener") .build();
在编译之前,将上一次编译生成的 JAR 文件重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener"
现在让我们执行读取器从同一个文件读取,但使用旧的 JAR 文件:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output Reading from file theater_protobuf_output name: "Silver Screener" address:
从输出的最后一行可以看出,旧的读取器将“address”的默认值设置为。它显示了“新写入器 - 旧读取器”组合是如何工作的。
避免重复使用字段的序号
在某些情况下,可能会错误地更新字段的“序号”。这可能会导致问题,因为序号对于 Protobuf 理解和反序列化数据至关重要。一些旧的读取器可能依赖于此序号来反序列化数据。因此,建议你:
不要更改字段的序号
不要重复使用已删除字段的序号。
让我们通过交换字段标签来看一下实际情况。
继续我们的剧院示例,假设我们的proto文件中只有两个标签。以下是我们需要用来指示 Protobuf 的语法:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
要使用 Protobuf,我们现在必须使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=java/src/main/java proto_files\theater.proto
上述命令应该创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
接下来,我们将有一个读取器来读取剧院信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" address: "212, Maple Street, LA, California"
接下来,让我们执行读取器从同一个文件读取:
java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
在这里,我们只是根据 Protobuf 定义编写了简单的字符串,并且读取器能够读取字符串。但是现在,让我们在 Protobuf 定义中交换序号,使其像这样:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 2; string address = 1; }
在编译之前,将上一次编译生成的 JAR 文件重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: address: "212, Maple Street, LA, California" name: "Silver Screener"
现在让我们执行读取器从同一个文件读取,但使用旧的 JAR 文件:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "212, Maple Street, LA, California" address: "Silver Screener"
从输出可以看出,旧的读取器交换了address和name。这表明更新序号以及“新写入器-旧读取器”的组合无法按预期工作。
更重要的是,这里我们有两个字符串,这就是为什么我们可以看到数据。如果我们使用不同的数据类型,例如int32、布尔值、映射等,Protobuf 将放弃并将其视为未知字段。
因此,务必不要更改字段的序号或重复使用已删除字段的序号。
更改字段类型
在某些情况下,我们需要更新属性/字段的类型。Protobuf 对此有一些兼容性规则。并非所有类型都可以转换为其他类型。需要注意一些基本类型:
如果字节是 UTF-8,则字符串和字节是兼容的。这是因为字符串无论如何都会被 Protobuf 编码/解码为 UTF-8。
就值而言,枚举与int32和int64兼容,但是客户端可能无法按预期反序列化。
int32、int64(也包括无符号)以及布尔值是兼容的,因此可以互换。类似于语言中的强制类型转换方式,可能会截断多余的字符。
但是更改类型时需要非常小心。让我们通过将int64转换为int32的错误示例来看一下实际情况。
继续我们的剧院示例,假设我们的proto文件中只有两个标签。以下是我们需要用来指示 Protobuf 的语法:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; int64 total_capacity = 2; }
要使用 Protobuf,我们现在必须使用protoc二进制文件从这个".proto"文件创建所需的类。让我们看看如何做到这一点:
protoc --java_out=java/src/main/java proto_files\theater.proto
上述命令应该创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个写入器来写入剧院信息:
TheaterWriter.java
package com.tutorialspoint.theater; import java.io.FileOutputStream; import java.io.IOException; import com.tutorialspoint.theater.TheaterOuterClass.Theater; public class TheaterWriter{ public static void main(String[] args) throws IOException { Theater theater = Theater.newBuilder() .setName("Silver Screener") .setTotalCapacity(2300000000L) .build(); String filename = "theater_protobuf_output"; System.out.println("Saving theater information to file: " + filename); try(FileOutputStream output = new FileOutputStream(filename)){ theater.writeTo(output); } System.out.println("Saved theater information with following data to disk: \n" + theater); } }
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Saving theater information to file: theater_protobuf_output Saved theater information with following data to disk: name: "Silver Screener" total_capacity: 2300000000
假设我们对读取器使用不同版本的proto文件:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; int64 total_capacity = 2; }
接下来,我们将有一个读取器来读取剧院信息:
TheaterReader.java
package com.tutorialspoint.theater; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import com.google.protobuf.ProtocolStringList; import com.tutorialspoint.greeting.Greeting.Greet; import com.tutorialspoint.theater.TheaterOuterClass.Theater; import com.tutorialspoint.theater.TheaterOuterClass.Theater.Builder; public class TheaterReader{ public static void main(String[] args) throws IOException { Builder theaterBuilder = Theater.newBuilder(); String filename = "theater_protobuf_output"; System.out.println("Reading from file " + filename); try(FileInputStream input = new FileInputStream(filename)) { Theater theater = theaterBuilder.mergeFrom(input).build(); System.out.println(theater); System.out.println("Unknwon fields: " + theater.getUnknownFields()); } } }
现在让我们执行读取器从同一个文件读取:
java -cp .\target\protobuf-tutorial-old-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener" address: "212, Maple Street, LA, California"
所以,这里没有什么新东西,我们只是根据 Protobuf 定义编写了简单的字符串,并且读取器能够读取字符串。但是现在,让我们在 Protobuf 定义中交换序号,使其像这样:
theater.proto
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 2; int32 total_capacity = 2; }
在编译之前,将上一次编译生成的 JAR 文件重命名为protobuf-tutorial-old-1.0.jar。然后编译。
现在,编译后,让我们先执行写入器:
> java -cp .\target\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Reading from file theater_protobuf_output address: "Silver Screener" total_capcity: -1994967296
从输出可以看出,旧的读取器将数字从int64转换了,但是给定的int32没有足够的空间来容纳数据,它绕回到负数。这种环绕是 Java 特有的,与 Protobuf 无关。
因此,我们需要从int32升级到int64,而不是反过来。如果我们仍然想将int64转换为int32,我们需要确保值实际上可以保存在 31 位中(1 位用于符号位)。