1. 概述
在系统中,Protocol Buffer 的使用有两种方式:1)定义 .proto 文件,生成相应的 Bean 对象及序列化的代码;2)根据现有的 Java Bean 对象,动态生成对应的 schema, 使用工具类完成对象的序列化。这两种方式的优缺点也比较明显,.proto 文件支持向前、向后兼容,后者使用简单,引用工具类,便可支持序列化操作,缺点是存在代码兼容性问题。
** 说明: **
这篇文章只是简单讲述两种方式的使用,详细的内容请参考官方文档。
2. 定义 proto 文件
2.1 安装部署 Protocol Buffer 编译器
以 windows 环境为例,从官方下载 Protocol Buffer,如 protoc-3.18.1-win64.zip, 解压缩到指定目录,并将 bin 目录加到 Path 环境变量中即可。
1 2 3 4 5 6 7
| SET PROTOC_HOME=D:\app\protoc-3.18.1-win64 SET PATH=%PAHTH%;%PROTOC_HOME%\bin
C:\Users\Admin>protoc --version libprotoc 3.18.1
|
下载地址:
https://github.com/protocolbuffers/protobuf/releases/tag/v3.18.1
2.2 定义 proto 文件
在官方的 addressbook.proto 文件基础上,添加了 map 对象的变量 people_map,在真实场景中,该对象可以通过 peoples 列表遍历得到,不需要传递 map 对象,否则会传输两份数据,在这里仅仅是演示作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| syntax = "proto3"; package tutorial;
import "google/protobuf/timestamp.proto";
option java_multiple_files = true; option java_package = "com.example.tutorial.protos"; option java_outer_classname = "AddressBookProtos";
message Person { string name = 1; int32 id = 2; string email = 3;
enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; }
message PhoneNumber { string number = 1; PhoneType type = 2; }
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5; }
message AddressBook { repeated Person peoples = 1; map<string, Person> people_map = 2; }
|
说明:
- syntax: PB 版本,有 v2 和 v3 两个版本,值分别为 proto2 和 proto3;
- package: 包名,表示命名空间,用于解决名称冲突,在 Java 中转换为 package 包名;
- java_multiple_files:Java 申明,若为 true, 表示为每一个 message 生成独立的文件(嵌套的 message 除外);
- java_package:Java 申明,指定 Java 的 package 名称,它为覆盖 package 属性指定的包名;
- java_outer_classname:Java 申明,指定输出的 Java 类名;
- import:引入外部的 proto 文件;
- message:定义一个消息,对应 Java 中的对象,message 可以嵌套定义;
- enum:枚举对象,下标从 0 开始;
- repeated:表示 0 或 n 个对象,可以定义数组对象;
- map:map 对象;
- optional:描述符,表示该字段为可选字段,如果没有设置的话,使用默认值,在 proto3 中已经取消 required 字段;
- tag:如同 =1,字段在 message 中的惟一标示,在二进制编码中使用 tag 代表该字段,1~15 使用一个字节表示,处于优化的目的,常用的字段应该放在这个区域。
map 对象等同于如下的定义,它实际上就是一个包含 key 和 value 对象的数组。
1 2 3 4 5 6
| message MapFieldEntry { key_type key = 1; value_type value = 2; }
repeated MapFieldEntry map_field = N;
|
2.3 编译 proto 文件
1
| protoc -I=. --java_out=. addressbookV2.proto
|
说明:
- -I:指定 proto 文件所在的源目录;
- –java_out:指定 Java 语言编译输出的目录;
生成的代码如下所示:
1 2 3 4 5 6 7 8 9
| └─org └─noahsark └─tutorial └─protos AddressBook.java AddressBookOrBuilder.java AddressBookProtos.java Person.java PersonOrBuilder.java
|
2.4 运行代码
将生成的的代码导入工程,引入 Protocol Buffer jar 包。
1 2 3 4 5 6
| <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.18.1</version> </dependency>
|
构造 AddressBook 对象并序列化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private static byte [] marshal() { Person john = Person.newBuilder() .setId(1234) .setName("John Doe") .setEmail("jdoe@example.com") .addPhones( Person.PhoneNumber.newBuilder() .setNumber("555-4321") .setType(Person.PhoneType.HOME)) .build();
AddressBook addressBook = AddressBook.newBuilder() .addPeoples(john) .putPeopleMap(john.getName(),john).build();
byte [] message = addressBook.toByteArray();
return message; }
|
反序列化 AddressBook 对象
1 2 3 4 5
| private static AddressBook unmarshal(byte [] message) throws InvalidProtocolBufferException { AddressBook addressBook = AddressBook.parseFrom(message);
return addressBook; }
|
输出 AddressBook 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private static void listAddressBook(AddressBook addressBook) { for (Person person: addressBook.getPeoplesList()) {
System.out.println("Person ID: " + person.getId()); System.out.println(" Name: " + person.getName()); System.out.println(" E-mail address: " + person.getEmail());
for (Person.PhoneNumber phoneNumber : person.getPhonesList()) { switch (phoneNumber.getType()) { case MOBILE: System.out.print(" Mobile phone #: "); break; case HOME: System.out.print(" Home phone #: "); break; case WORK: System.out.print(" Work phone #: "); break; } System.out.println(phoneNumber.getNumber()); } } }
|
使用 ProtocolBuffer 序列化及反序列化数据
1 2 3 4 5 6 7 8 9 10 11 12
| public static void main(String[] args) throws InvalidProtocolBufferException {
byte [] message = marshal();
AddressBook addressBook = unmarshal(message);
listAddressBook(addressBook);
}
|
输出结果为:
1 2 3 4
| Person ID: 1234 Name: John Doe E-mail address: jdoe@example.com Home phone
|
3. 运行时模式
在该种方式下,不用定义 .proto 文件,引入 protostuff 工具包,然后使用其提供的 API 便可对 Java 对象进行 ProtocolBuffer 序列化/反序列化操作。
3.1 引入 protostuff
在 maven pom 文件中加入如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.7.4</version> </dependency>
<dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.7.4</version> </dependency>
|
3.2 定义 Java Bean 对象
1 2 3 4 5 6 7 8 9
| public class Person { private int id; private String name; private String email; private String mobile;
... }
|
3.3 定义工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public class ProtostuffUtils {
private static LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<Class<?>, Schema<?>>();
@SuppressWarnings("unchecked") public static <T> byte[] serialize(T obj) { Class<T> clazz = (Class<T>) obj.getClass(); Schema<T> schema = getSchema(clazz); byte[] data; try { data = ProtostuffIOUtil.toByteArray(obj, schema, buffer); } finally { buffer.clear(); } return data; }
public static <T> T deserialize(byte[] data, Class<T> clazz) { Schema<T> schema = getSchema(clazz); T obj = schema.newMessage(); ProtostuffIOUtil.mergeFrom(data, obj, schema); return obj; }
@SuppressWarnings("unchecked") private static <T> Schema<T> getSchema(Class<T> clazz) { Schema<T> schema = (Schema<T>) schemaCache.get(clazz); if (schema == null) { schema = RuntimeSchema.getSchema(clazz); if (schema == null) { schemaCache.put(clazz, schema); } } return schema; } }
|
3.4 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public static void main(String[] args) { Person person = new Person();
person.setId(1); person.setName("john"); person.setMobile("13845678912"); person.setEmail("john@gmail.com");
byte [] message = ProtostuffUtils.serialize(person);
Person john = ProtostuffUtils.deserialize(message, Person.class);
System.out.println("person:\n" + john);
}
|
输出结果为:
1 2
| person: Person{id=1, name='john', email='john@gmail.com', mobile='13845678912'}
|
4. 总结
这两种方式不同的使用场景,如果后期不考虑代码的兼容问题,可以使用 protostuff 工具包,简单方便,如果代码迭代更新比较多,需要考虑不同版本的兼容问题,那最好定义合适的 .proto 文件来保证代码的兼容性。
参考:
1. Protocol Buffer Basics: Java
2. Protocol Buffers v3.18.1
3. github:protostuff
4. github:protobuf
5. java序列化机制之protoStuff