Protobuf - 更新定义的规则
假设您已经确定了将在生产环境中使用的 proto 文件的定义。显然,将来有时必须更改此定义。在这种情况下,我们所做的更改必须遵守某些规则,以便更改向后兼容。让我们通过一些注意事项来了解这一点。
在 writer 中添加一个新字段,而 reader 保留旧版本的代码。
假设您决定添加一个新字段。理想情况下,要添加新字段,我们必须同时更新 writer 和 reader。但是,在大规模部署中,这是不可能的。在某些情况下,writer 已更新,但 reader 尚未使用新字段进行更新。上述情况就发生在这种情况。让我们看看实际操作。
继续我们的 theater 示例,假设我们的 proto 文件中只有一个标签 'name'。以下是我们需要指示 Protobuf 的语法−
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; }
要使用 Protobuf,我们现在必须使用 protoc 二进制文件从此 .proto" 文件创建所需的类。让我们看看如何做到这一点 −
protoc --java_out=java/src/main/java proto_files heater.proto
上述命令应该会创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个 writer 来写入 theater 信息 −
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: " + theater); } }
接下来,我们将有一个reader(阅读器)来读取theater(影院)信息−
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()); } } }
现在,编译后,让我们首先执行writer −
> java -cp . arget\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"
现在让我们执行reader来读取同一个文件 −
java -cp . arget\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterReader Reading from file theater_protobuf_output name: "Silver Screener"
未知字段
我们只是按照我们的 Protobuf 定义编写了一个简单的字符串,并且 读取器 能够读取该字符串。我们还看到没有读取器不知道的未知字段。
但是现在,让我们假设我们想在我们的 Protobuf 定义中添加一个新的字符串 'address'。现在,它看起来像这样−
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; string address = 2; }
我们还将更新我们的writer并添加一个address字段 −
Theater theater = Theater.newBuilder() .setName("Silver Screener") .setAddress("212, Maple Street, LA, California") .build();
编译前,将上次编译的JAR重命名为protobuf-tutorial-old-1.0.jar,然后进行编译。
现在,编译后,让我们首先执行writer −
> java -cp . arget\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"
现在让我们执行 reader 来读取同一个文件,但来自较旧的 JAR −
java -cp . arget\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"
从输出的最后一行可以看出,旧的 reader 不知道由新的 writer 添加的 address 字段。它只是展示了"新 writer - 旧 reader"组合如何发挥作用。
删除字段
假设,您决定删除现有字段。理想情况下,为了使删除的字段立即生效,我们必须同时更新 writer 和 reader。然而,在大规模部署中,这是不可能的。会出现 writer 已更新,但 reader 尚未更新的情况。在这种情况下,reader 仍将尝试读取已删除的字段。让我们看看实际效果。
继续使用 theater 示例,假设我们的 proto 文件中只有两个标签。以下是我们需要用来指示 Protobuf 的语法 −
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 heater.proto
上述命令应该会创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个 writer 来写入 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") .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: " + theater); } }
接下来,我们将有一个reader(阅读器)来读取theater(剧院)的信息 −
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()); } } }
现在,编译后,让我们先执行 writer −
> java -cp . arget\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"
现在让我们执行reader来读取同一个文件 −
java -cp . arget\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'。因此,定义将如下所示−
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; }
我们还将对我们的writer进行如下更新 −
Theater theater = Theater.newBuilder() .setName("Silver Screener") .build();
编译前,将上次编译的JAR重命名为protobuf-tutorial-old-1.0.jar,然后进行编译。
现在,编译后,让我们首先执行writer −
> java -cp . arget\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 . arget\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 理解和反序列化数据非常关键。而一些旧的读取器可能依赖此序列号来反序列化数据。因此,建议您 −
不要更改字段的序列号
不要重复使用已删除字段的序列号。
让我们通过交换字段标签来实际操作。
继续使用 theater 示例,假设我们的 proto 文件中只有两个标签。以下是我们需要指示 Protobuf 的语法 −
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 heater.proto
上述命令应该会创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个 writer 来写入 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") .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: " + theater); } }
接下来,我们将有一个reader(阅读器)来读取theater(剧院)的信息 −
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()); } } }
现在,编译后,让我们首先执行writer −
> java -cp . arget\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"
接下来,让我们执行reader来读取同一个文件 −
java -cp . arget\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 定义中的序列号,并使其像这样 −
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。然后进行编译。
现在,编译后,让我们先执行 writer −
> java -cp . arget\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"
现在让我们执行 reader 来读取同一个文件,但来自较旧的 JAR −
java -cp . arget\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"
从输出中可以看出,旧的 reader 交换了 address 和 name。这表明更新序列号以及"新写入器-旧读取器"的组合无法按预期运行。
更重要的是,这里我们有两个字符串,这就是我们可以看到数据的原因。如果我们使用了不同的数据类型,例如 int32、Boolean、map 等,Protobuf 会放弃并将其视为未知字段。
因此,必须不要更改字段的序列号或重复使用已删除字段的序列号。
更改字段类型
在某些情况下,我们需要更新属性/字段的类型。Protobuf 对此有一定的兼容性规则。并非所有类型都可以转换为其他类型。需要注意的几个基本事项 −
如果字节是 UTF-8,则字符串和字节是兼容的。这是因为,字符串无论如何都会被 Protobuf 编码/解码为 UTF-8。
枚举在值方面与int32和int64兼容,但是,客户端可能无法按预期对其进行反序列化。
int32、int64(unsigned也是)以及bool是兼容的,因此可以互换。过多的字符可能会被截断,类似于语言中转换的工作方式。
但是我们在更改类型时需要非常小心。让我们通过一个将 int64 转换为 int32 的错误示例来看一下实际操作。
继续使用 theater 示例,假设我们的 proto 文件中只有两个标签。以下是我们需要指示 Protobuf 的语法 −
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 heater.proto
上述命令应该会创建所需的文件,现在我们可以在 Java 代码中使用它。首先,我们将创建一个 writer 来写入 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") .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: " + theater); } }
现在,编译后,让我们首先执行writer −
> java -cp . arget\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文件 −
syntax = "proto3"; package theater; option java_package = "com.tutorialspoint.theater"; message Theater { string name = 1; int64 total_capacity = 2; }
接下来,我们将有一个reader(阅读器)来读取theater(剧院)的信息 −
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()); } } }
现在让我们执行reader来读取同一个文件 −
java -cp . arget\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 定义中的序列号,并使其像这样−
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,然后进行编译。
现在,编译后,让我们首先执行writer −
> java -cp . arget\protobuf-tutorial-1.0.jar com.tutorialspoint.theater.TheaterWriter Reading from file theater_protobuf_output address: "Silver Screener" total_capcity: -1994967296
从输出中可以看到,旧的 reader 将数字从 int64 转换而来,但是,给定的 int32 没有足够的空间来包含数据,因此它被包装为负数。此包装是 Java 特有的,与 Protobuf 无关。
因此,我们需要从 int32 升级到 int64,而不是反过来。如果我们仍想从 int64 转换为 int32,我们需要确保值实际上可以保存在 31 位中(1 位用于符号位)。