1. 简介
Protocol Buffers(简称protobuf)提供了一种快速高效的序列化结构化数据的方式。它是JSON的紧凑、高性能替代方案。
与基于文本且需要解析的JSON不同,protobuf会为多种语言生成优化代码。这使得在不同系统间传输结构化数据更加便捷。
使用protobuf时,我们只需在*.proto*文件中定义一次数据结构。然后利用生成的代码处理跨流和平台的数据传输。当处理类型化结构化数据(尤其是负载只有几兆字节)时,protobuf是理想选择。
Protobuf支持字符串、整数、布尔值和浮点数等常见类型,也能很好地处理列表和Map,使复杂数据管理变得简单。本教程将学习如何在protobuf中使用Map。
2. 理解Protobuf中的Map
让我们探索如何在protobuf消息中定义和使用Map。
2.1. 什么是Map?
Map是键值对数据结构,类似于字典。
每个键都链接到特定值,使查找快速高效。我们可以将DNS系统作为类比:每个域名指向一个IP地址。Map的工作方式类似。
2.2. 定义Map的语法
Protobuf 3原生支持Map。
简单示例如下:
message Dictionary {
map<string, string> pairs = 1;
}
map<key_type, value_type> 定义字段。键必须是标量类型,如string、int32或bool。值可以是任何有效的protobuf类型——标量、枚举,甚至是另一个消息。
3. 在Protobuf中实现Map
既然已经了解了使用protobuf的优势,现在通过构建一个食品配送系统将理论付诸实践,其中每个餐厅都有自己的菜单。
3.1. 在代码库中设置Protobuf
在定义消息结构之前,必须将Protoc编译器集成到构建生命周期中。这可以通过在项目的pom.xml文件中配置protobuf-maven-plugin实现。这样,协议缓冲区定义会在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>
除了编译器,还需要协议缓冲区运行时。将其依赖项添加到Maven POM文件:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.30.2</version>
</dependency>
可以使用其他版本的运行时,只要与编译器版本相同即可。
3.2. 定义包含Map字段的消息
让我们从定义一个包含Map的简单protobuf消息开始。这里创建一个protobuf模式,其中restaurants Map以餐厅名称为键,菜单为值。菜单本身是另一个Map,将食品项映射到价格:
syntax = "proto3";
message Menu {
map<string, float> items = 1;
}
message FoodDelivery {
map<string, Menu> restaurants = 1;
}
3.3. 填充Map
既然已在模式中定义了Map,现在需要在代码中填充数据。
*protobuf中的map<k, v>*结构类似Java的HashMap,允许高效存储键值对**。
本例使用*map<string, Menu>*存储餐厅名称作为键,对应菜单项作为值:
Food.Menu pizzaMenu = Food.Menu.newBuilder()
.putItems("Margherita", 12.99f)
.putItems("Pepperoni", 14.99f)
.build();
Food.Menu sushiMenu = Food.Menu.newBuilder()
.putItems("Salmon Roll", 10.50f)
.putItems("Tuna Roll", 12.33f)
.build();
首先定义餐厅菜单。然后用餐厅名称及其菜单填充Map:
Food.FoodDelivery.Builder foodData = Food.FoodDelivery.newBuilder();
先创建Map实例。接着直接放入餐厅并构建Map:
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)) {
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)));
}
}
这里显示存储的数据。由于Map以餐厅名称为键,对应菜单为值,只需遍历数据并记录每个餐厅及其菜单项和价格。
5. 测试实现
为确保Protobuf Map操作正确,验证序列化是否正确写入文件,并确认反序列化是否正确恢复原始数据。
还需要捕获日志输出并检查是否记录了预期数据。首先验证序列化是否正确:
@Test
void givenProtobufObject_whenSerializeToFile_thenFileShouldExist() {
foodDelivery.serializeToFile(testData);
File file = new File(FILE_PATH);
assertTrue(file.exists(), "Serialized file should exist");
}
验证序列化后,测试反序列化:
@Test
void givenSerializedFile_whenDeserialize_thenShouldMatchOriginalData() {
foodDelivery.serializeToFile(testData);
Food.FoodDelivery deserializedData = foodDelivery.deserializeFromFile(testData);
assertEquals(testData.getRestaurantsMap(), deserializedData.getRestaurantsMap(), "Deserialized data should match the original data");
}
这里先序列化文件,然后检查testData Map是否等于deserializedData的Map。接着验证是否正确记录数据:
@Test
void givenDeserializedObject_whenDisplayRestaurants_thenShouldLogCorrectOutput() {
foodDelivery.serializeToFile(testData);
Food.FoodDelivery deserializedData = foodDelivery.deserializeFromFile(testData);
Logger logger = Logger.getLogger(FoodDelivery.class.getName());
TestLogHandler testHandler = new TestLogHandler();
logger.addHandler(testHandler);
logger.setUseParentHandlers(false);
foodDelivery.displayRestaurants(deserializedData);
List<String> logs = testHandler.getLogs();
assertTrue(logs.stream().anyMatch(log -> log.contains("Restaurant: Pizza Place")),
"Log should contain 'Restaurant: Pizza Place'");
assertTrue(logs.stream().anyMatch(log -> log.contains("Margherita costs $ 12.99")),
"Log should contain 'Margherita costs $ 12.99'");
}
为验证应用程序是否在执行期间记录预期消息,需要一种编程方式捕获和检查日志输出。TestLogHandler通过扩展Java的Handler实现此功能:
static class TestLogHandler extends Handler {
private final List<String> logMessages = new ArrayList<>();
@Override
public void publish(LogRecord record) {
if (record.getLevel().intValue() >= Level.INFO.intValue()) {
logMessages.add(record.getMessage());
}
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
public List<String> getLogs() {
return logMessages;
}
}
它有一个日志消息列表,将每个级别大于等于INFO的LogRecord推入其中。使用列表存储以保持日志在控制台中出现的顺序。
6. 结论
在Protobuf中使用Map为数据模型中的键值关系管理提供了结构化且高效的方式。本文探讨了如何在Java中定义、序列化和反序列化Protobuf Map,确保数据保持紧凑、可读且易于传输。通过实现健壮的单元测试,验证了序列化和反序列化过程正确运行,维护了数据完整性。
通过正确的Maven设置和最佳实践,现在可以自信地将Protobuf Map集成到应用程序中,利用其性能优势,同时保持代码库的整洁和可维护性。
本文相关代码可在GitHub上获取。