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"

从输出可以看出,旧的读取器交换了addressname。这表明更新序号以及“新写入器-旧读取器”的组合无法按预期工作。

更重要的是,这里我们有两个字符串,这就是为什么我们可以看到数据。如果我们使用不同的数据类型,例如int32、布尔值、映射等,Protobuf 将放弃并将其视为未知字段。

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

更改字段类型

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

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

  • 就值而言,枚举int32int64兼容,但是客户端可能无法按预期反序列化。

  • 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 位用于符号位)。

广告