Protobuf - 更新定义的规则

假设您已经确定了将在生产环境中使用的 proto 文件的定义。显然,将来有时必须更改此定义。在这种情况下,我们所做的更改必须遵守某些规则,以便更改向后兼容。让我们通过一些注意事项来了解这一点。

writer 中添加一个新字段,而 reader 保留旧版本的代码。

假设您决定添加一个新字段。理想情况下,要添加新字段,我们必须同时更新 writerreader。但是,在大规模部署中,这是不可能的。在某些情况下,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"组合如何发挥作用。

删除字段

假设,您决定删除现有字段。理想情况下,为了使删除的字段立即生效,我们必须同时更新 writerreader。然而,在大规模部署中,这是不可能的。会出现 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 交换了 addressname。这表明更新序列号以及"新写入器-旧读取器"的组合无法按预期运行。

更重要的是,这里我们有两个字符串,这就是我们可以看到数据的原因。如果我们使用了不同的数据类型,例如 int32、Boolean、map 等,Protobuf 会放弃并将其视为未知字段。

因此,必须不要更改字段的序列号或重复使用已删除字段的序列号。

更改字段类型

在某些情况下,我们需要更新属性/字段的类型。Protobuf 对此有一定的兼容性规则。并非所有类型都可以转换为其他类型。需要注意的几个基本事项 −

  • 如果字节是 UTF-8,则字符串和字节是兼容的。这是因为,字符串无论如何都会被 Protobuf 编码/解码为 UTF-8。

  • 枚举在值方面与int32int64兼容,但是,客户端可能无法按预期对其进行反序列化。

  • int32、int64unsigned也是)以及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 位用于符号位)。