在 Protobuf 中使用 Map
1、简介
ProtoBuf 为结构化数据的序列化提供了一种快速高效的方式,它是 JSON 的紧凑型高性能替代品。
与基于文本并需要解析的 JSON 不同,protobuf 可生成适用于多种语言的优化代码。这使得在不同系统间发送结构化数据变得更加容易。
使用 protobuf,只需在 .proto
文件中定义一次数据结构。然后,就可以使用生成的代码来处理跨流和跨平台的数据传输。在处理类型化、结构化数据时,它是理想的选择,尤其是当 Payload 比较大的时候。
Protobuf 支持字符串、整数、布尔值和浮点数等常见类型。它们还能很好地与 List
和 Map
配合使用,使复杂的数据易于管理。
本文将带你了解如何在 protobuf 中使用 Map
。
2、了解 Protobuf 中的 Map
2.1、Map 是什么?
Map 是一种键值数据结构,类似于字典。
每个键都映射到一个特定的值,从而使查找快速高效。类似于 DNS 系统:每个域名都指向一个 IP 地址。
2.2、定义 Map 的语法
Protobuf 3 对 Map 提供了开箱即用的支持。
示例如下:
message Dictionary {
map<string, string> pairs = 1;
}
使用 map<key_type, value_type>
定义字段。Key 必须是 scalar
类型,如 string
、int32
或 bool
。值可以是任何有效的 protobuf
类型 - scalar
、enum
或其他消息。
3、在 Protobuf 中实现 Map
在理解了使用 protobuf 的好处后,让我们通过一个小的送餐系统来进行实践。在这个送餐系统中,每个餐厅都有自己的菜单。
3.1、在代码中设置 Protobuf
在定义消息结构之前,必须将 Protoc 编译器集成到构建生命周期中。这可以通过在项目的 pom.xml
文件中配置 protobuf-maven-plugin
来实现。这样,protobuf 定义就会在 Maven 构建过程中自动编译成 Java 类。
在 pom.xml
文件的 build
部分添加插件配置:
<build>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<protocArtifact>com.google.protobuf:protoc:4.30.2:exe:${os.detected.classifier}</protocArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
除了编译器,还需要 protobuf 运行时。在 Maven POM 文件中添加它的 依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.30.2</version>
</dependency>
也可以使用其他版本的运行时,只要它与编译器的版本相同即可。
3.2、使用 Map 字段定义信息
定义一个包含 Map
的简单 protobuf schema,其中 restaurants
map 将餐馆名称作为键存储,菜单作为值存储。菜单本身是另一个 map
,包含了食品和价格的映射:
syntax = "proto3"
message Menu {
// 食品和价格
map<string, float> items = 1;
}
message FoodDelivery {
// 餐馆和菜单
map<string, Menu> restaurants = 1;
}
3.3、填充 Map
在 schema 中定义了 map 后,需要在代码中用数据填充它。
protobuf 中的 map<k, v>
结构行为类似于 Java HashMap
,允许我们高效地存储键值对。
首先,初始化菜单:
// pizza 菜单
Food.Menu pizzaMenu = Food.Menu.newBuilder()
.putItems("Margherita", 12.99f)
.putItems("Pepperoni", 14.99f)
.build();
// sushi 菜单
Food.Menu sushiMenu = Food.Menu.newBuilder()
.putItems("Salmon Roll", 10.50f)
.putItems("Tuna Roll", 12.33f)
.build();
然后,初始化餐厅 Builder:
Food.FoodDelivery.Builder foodData = Food.FoodDelivery.newBuilder();
在餐厅中填充各自的菜单:
foodData.putRestaurants("Pizza Place", pizzaMenu);
foodData.putRestaurants("Sushi Place", sushiMenu);
return foodData.build();
4、从二进制文件存储和检索数据
接下来,把 protobuf map 数据写入二进制文件,这一过程称为序列化。这可以确保高效存储和轻松传输。当然,还可以通过反序列化字段来读取数据。
4.1、将 Protobuf Map 序列化为二进制文件
将结构化数据序列化为紧凑的二进制格式,使其在网络上存储或发送时轻便快捷。
首先,为要写入数据的文件定义一个文件路径:
private final String FILE_PATH = "src/main/resources/foodfile.bin";
然后,定义序列化的逻辑:
public void serializeToFile(Food.FoodDelivery delivery) {
try (FileOutputStream fos = new FileOutputStream(FILE_PATH)) {
// 写入到指定的流
delivery.writeTo(fos);
logger.info("Successfully wrote to the file.");
} catch (IOException ioe) {
logger.warning("Error serializing the Map or writing the file");
}
}
生成的源文件可直接写入输出流。
4.2、将二进制文件反序列化为 Protobuf Map
现在,把二进制文件反序列化为 Protobuf map。
打开输入流,然后使用 Protobuf 编译器生成的方法来解析存储的数据:
public Food.FoodDelivery deserializeFromFile(Food.FoodDelivery delivery) {
try (FileInputStream fis = new FileInputStream(FILE_PATH)) {
// 从流中解析 protobuf 数据
return Food.FoodDelivery.parseFrom(fis);
} catch (FileNotFoundException e) {
logger.severe(String.format("File not found: %s location", FILE_PATH));
return Food.FoodDelivery.newBuilder().build();
} catch (IOException e) {
logger.warning(String.format("Error reading file: %s location", FILE_PATH));
return Food.FoodDelivery.newBuilder().build();
}
}
如上,打开一个文件输入流,并将其传递给 parseFrom()
方法,该方法会重建 protobuf 对象。
4.3、显示反序列化后的结果
反序列化了数据后,输出反序列化结果:
public void displayRestaurants(Food.FoodDelivery delivery) {
Map<String, Food.Menu> restaurants = delivery.getRestaurantsMap();
// 迭代、打印数据
for (Map.Entry<String, Food.Menu> restaurant : restaurants.entrySet()) {
logger.info(String.format("Restaurant: %s", restaurant.getKey()));
restaurant.getValue()
.getItemsMap()
.forEach((menuItem, price) -> logger.info(String.format(" - %s costs $ %.2f", menuItem, price)));
}
}
如上,在控制台中输出存储各个餐厅以及对应的菜单数据。
5、总结
在 Protobuf 中使用 Map
为管理数据模型中的键值关系提供了一种结构化的高效方式。本文介绍了如何在 Java 中定义、序列化和反序列化 Protobuf Map,以确保数据保持紧凑、可读性强和易于传输。
Ref:https://www.baeldung.com/java-protobuf-maps