在 Protobuf 中使用 Map

1、简介

ProtoBuf 为结构化数据的序列化提供了一种快速高效的方式,它是 JSON 的紧凑型高性能替代品。

与基于文本并需要解析的 JSON 不同,protobuf 可生成适用于多种语言的优化代码。这使得在不同系统间发送结构化数据变得更加容易。

使用 protobuf,只需在 .proto 文件中定义一次数据结构。然后,就可以使用生成的代码来处理跨流和跨平台的数据传输。在处理类型化、结构化数据时,它是理想的选择,尤其是当 Payload 比较大的时候。

Protobuf 支持字符串、整数、布尔值和浮点数等常见类型。它们还能很好地与 ListMap 配合使用,使复杂的数据易于管理。

本文将带你了解如何在 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 类型,如 stringint32bool。值可以是任何有效的 protobuf 类型 - scalarenum 或其他消息。

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