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 位用於符號位)。

廣告
© . All rights reserved.