DoneSpeak

Protobuf与POJO的相互转化 - 通过Json

字数: 2.6k时长: 13 min
2019/09/04 Share

前言

这篇文章是《Protobuf与Json的相互转化》的一个后续,主要是为了解决系统分层中不同ProtoBean与POJO的相互转化问题。转化的Protobuf和Pojo具有相同名称及类型的属性(当Proto属性类型为Message时,对应的为Pojo的Object类型的属性,两者应该具有相同的属性)。

转化的基本思路

测试使用的protobuf文件如下:

StudentProto.proto

1
2
3
4
5
6
7
8
9
syntax = "proto3";

option java_package = "io.gitlab.donespeak.javatool.toolprotobuf.proto";

message Student {
string name = 1;
int32 age = 2;
Student deskmate = 3;
}

DataTypeProto.proto

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
syntax = "proto3";

import "google/protobuf/any.proto";

option java_package = "io.gitlab.donespeak.javatool.toolprotobuf.proto";
package data.proto;

enum Color {
NONE = 0;
RED = 1;
GREEN = 2;
BLUE = 3;
}

message BaseData {
double double_val = 1;
float float_val = 2;
int32 int32_val = 3;
int64 int64_val = 4;
uint32 uint32_val = 5;
uint64 uint64_val = 6;
sint32 sint32_val = 7;
sint64 sint64_val = 8;
fixed32 fixed32_val = 9;
fixed64 fixed64_val = 10;
sfixed32 sfixed32_val = 11;
sfixed64 sfixed64_val = 12;
bool bool_val = 13;
string string_val = 14;
bytes bytes_val = 15;

Color enum_val = 16;

repeated string re_str_val = 17;
map<string, BaseData> map_val = 18;
}

直接转化

通过映射的方法,直接将同名同类别的属性进行复制。该实现方式主要通过反射机制进行实现。

1
[ A ] <--> [ B ]

直接转化的方式需要通过protobuf的反射机制才能实现地了,难度会比较大,也正在尝试实现。另一种方式是尝试使用Apache Common BeanUtils 或者 Spring BeanUtils,进行属性拷贝。这里使用Spring BeanUtils进行设计,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ProtoPojoUtilWithBeanUtils {

public static void toProto(Message.Builder destProtoBuilder, Object srcPojo) throws ProtoPojoConversionException {
// Message 都是不可变类,没有setter方法,只能通过Builder进行setter
try {
BeanUtils.copyProperties(srcPojo, destProtoBuilder);
} catch (Exception e) {
throw new ProtoPojoConversionException(e.getMessage(), e);
}
}

public static <PojoType> PojoType toPojo(Class<PojoType> destPojoKlass, Message srcMessage)
throws ProtoPojoConversionException {
try {
PojoType destPojo = destPojoKlass.newInstance();
BeanUtils.copyProperties(srcMessage, destPojo);
return destPojo;
} catch (Exception e) {
throw new ProtoPojoConversionException(e.getMessage(), e);
}
}
}

这个实现是必然会有问题的,原因有如下几点

  • ProtoBean不允许有null值,而Pojo允许有null值,从Pojo拷贝到Proto必然会有非空异常
  • BeanUtils 会按照方法名及getter/setter类型进行匹配,嵌套类型因为类型不匹配而无法正常拷贝
  • Map和List的Proto属性生成的Java会分别在属性名后增加Map和List,如果希望能够进行拷贝,则需要按照这个规则明明Projo的属性名
  • Enum类型不匹配无法进行拷贝,如果希望能够进行拷贝,可以尝试使用ProtoBean的Enum域的get**Value()方法,并据此命名Pojo属性名

总的来说,BeanUtils 不适合用于实现这个任务。只能后续考虑使用Protobuf的反射进行实现了。这个不是本文的侧重点,我们继续看另一种实现。

间接转化(货币兑换)

通过一个统一的媒介进行转化,就好比货币一样,比如人名币要转日元,银行会先将人名币转美元,再将美元转为日元,反向也是如此。

1
[ A ] <--> [ C ] <--> [ B ]

具体到实现中,我们可以将平台无关语言无关的Json作为中间媒介C,先将ProtoBean的A转化为Json的C,再将Json的C转化为ProtoBean的B对象即可。下面将对此方法进行详细的讲解。

代码实现

可以将ProtoBean转化为Json的工具有两个,一个是com.google.protobuf/protobuf-java-util,另一个是com.googlecode.protobuf-java-format/protobuf-java-format,两个的性能和效果还有待对比。这里使用的是com.google.protobuf/protobuf-java-util,原因在于protobuf-java-format 中的JsonFormat会将Map格式化为{"key": "", "value": ""} 的对象列表,而protobuf-java-util中的JsonFormat能够序列化为理想的key-value的结构,也符合Pojo转json的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.7.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.googlecode.protobuf-java-format/protobuf-java-format -->
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.4</version>
</dependency>

对于Pojo与Json的转化,这里采用的是Gson,原因是和Protobuf都出自谷歌家。

完整的实现如下:ProtoBeanUtils.jave

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
58
59
60
import java.io.IOException;

import com.google.gson.Gson;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;

/**
* 相互转化的两个对象的getter和setter字段要完全的匹配。
* 此外,对于ProtoBean中的enum和bytes,与POJO转化时遵循如下的规则:
* <ol>
* <li>enum -> String</li>
* <li>bytes -> base64 String</li>
* </ol>
* @author Yang Guanrong
* @date 2019/08/18 23:44
*/
public class ProtoBeanUtils {

/**
* 将ProtoBean对象转化为POJO对象
*
* @param destPojoClass 目标POJO对象的类类型
* @param sourceMessage 含有数据的ProtoBean对象实例
* @param <PojoType> 目标POJO对象的类类型范型
* @return
* @throws IOException
*/
public static <PojoType> PojoType toPojoBean(Class<PojoType> destPojoClass, Message sourceMessage)
throws IOException {
if (destPojoClass == null) {
throw new IllegalArgumentException
("No destination pojo class specified");
}
if (sourceMessage == null) {
throw new IllegalArgumentException("No source message specified");
}
String json = JsonFormat.printer().print(sourceMessage);
return new Gson().fromJson(json, destPojoClass);
}

/**
* 将POJO对象转化为ProtoBean对象
*
* @param destBuilder 目标Message对象的Builder类
* @param sourcePojoBean 含有数据的POJO对象
* @return
* @throws IOException
*/
public static void toProtoBean(Message.Builder destBuilder, Object sourcePojoBean) throws IOException {
if (destBuilder == null) {
throw new IllegalArgumentException
("No destination message builder specified");
}
if (sourcePojoBean == null) {
throw new IllegalArgumentException("No source pojo specified");
}
String json = new Gson().toJson(sourcePojoBean);
JsonFormat.parser().merge(json, destBuilder);
}
}

《Protobuf与Json的相互转化》一样,上面的实现无法处理 Any 类型的数据。需要自己添加 TypeRegirstry 才能进行转化。

A TypeRegistry is used to resolve Any messages in the JSON conversion. You must provide a TypeRegistry containing all message types used in Any message fields, or the JSON conversion will fail because data in Any message fields is unrecognizable. You don’t need to supply a TypeRegistry if you don’t use Any message fields.

Class JsonFormat.TypeRegistry @JavaDoc

添加TypeRegistry的方法如下:

1
2
3
4
5
6
7
8
9
// https://codeburst.io/protocol-buffers-part-3-json-format-e1ca0af27774
final var typeRegistry = JsonFormat.TypeRegistry.newBuilder()
.add(ProvisionVmCommand.getDescriptor())
.build();
final var jsonParser = JsonFormat.parser()
.usingTypeRegistry(typeRegistry);

final var envelopeBuilder = VmCommandEnvelope.newBuilder();
jsonParser.merge(json, envelopeBuilder);

测试

一个和Proto文件匹配的Pojo类 BaseDataPojo.java

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
import lombok.*;

import java.util.List;
import java.util.Map;

/**
* @author Yang Guanrong
* @date 2019/09/03 20:46
*/
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class BaseDataPojo {
private double doubleVal;
private float floatVal;
private int int32Val;
private long int64Val;
private int uint32Val;
private long uint64Val;
private int sint32Val;
private long sint64Val;
private int fixed32Val;
private long fixed64Val;
private int sfixed32Val;
private long sfixed64Val;
private boolean boolVal;
private String stringVal;
private String bytesVal;

private String enumVal;

private List<String> reStrVal;
private Map<String, BaseDataPojo> mapVal;
}

测试类 ProtoBeanUtilsTest.java

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package io.gitlab.donespeak.javatool.toolprotobuf.withjsonformat;

import static org.junit.Assert.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

import com.google.common.io.BaseEncoding;
import com.google.protobuf.ByteString;

import io.gitlab.donespeak.javatool.toolprotobuf.bean.BaseDataPojo;
import io.gitlab.donespeak.javatool.toolprotobuf.proto.DataTypeProto;

/**
* @author Yang Guanrong
* @date 2019/09/04 14:05
*/
public class ProtoBeanUtilsTest {

private DataTypeProto.BaseData getBaseDataProto() {
DataTypeProto.BaseData baseData = DataTypeProto.BaseData.newBuilder()
.setDoubleVal(100.123D)
.setFloatVal(12.3F)
.setInt32Val(32)
.setInt64Val(64)
.setUint32Val(132)
.setUint64Val(164)
.setSint32Val(232)
.setSint64Val(264)
.setFixed32Val(332)
.setFixed64Val(364)
.setSfixed32Val(432)
.setSfixed64Val(464)
.setBoolVal(true)
.setStringVal("ssss..tring")
.setBytesVal(ByteString.copyFromUtf8("itsbytes"))
.setEnumVal(DataTypeProto.Color.BLUE)
.addReStrVal("re-item-0")
.addReIntVal(33)
.putMapVal("m-key", DataTypeProto.BaseData.newBuilder()
.setStringVal("base-data")
.build())
.build();

return baseData;
}

public BaseDataPojo getBaseDataPojo() {
Map<String, BaseDataPojo> map = new HashMap<>();
map.put("m-key", BaseDataPojo.builder().stringVal("base-data").build());

BaseDataPojo baseDataPojo = BaseDataPojo.builder()
.doubleVal(100.123D)
.floatVal(12.3F)
.int32Val(32)
.int64Val(64)
.uint32Val(132)
.uint64Val(164)
.sint32Val(232)
.sint64Val(264)
.fixed32Val(332)
.fixed64Val(364)
.sfixed32Val(432)
.sfixed64Val(464)
.boolVal(true)
.stringVal("ssss..tring")
.bytesVal("itsbytes")
.enumVal(DataTypeProto.Color.BLUE.toString())
.reStrVal(Arrays.asList("re-item-0"))
.reIntVal(new int[]{33})
.mapVal(map)
.build();

return baseDataPojo;
}

@Test
public void toPojoBean() throws IOException {
DataTypeProto.BaseData baseDataProto = getBaseDataProto();
BaseDataPojo baseDataPojo = ProtoBeanUtils.toPojoBean(BaseDataPojo.class, baseDataProto);

// System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(baseDataPojo));

asserEqualsVerify(baseDataPojo, baseDataProto);
}

@Test
public void toProtoBean() throws IOException {
BaseDataPojo baseDataPojo = getBaseDataPojo();

DataTypeProto.BaseData.Builder builder = DataTypeProto.BaseData.newBuilder();
ProtoBeanUtils.toProtoBean(builder, baseDataPojo);
DataTypeProto.BaseData baseDataProto = builder.build();

// System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(baseDataPojo));
// 不可用Gson转化Message(含有嵌套结构的,且嵌套的Message中含有嵌套结构),会栈溢出的
// 因为Protobuf没有null值
// System.out.println(JsonFormat.printer().print(baseDataProto));

asserEqualsVerify(baseDataPojo, baseDataProto);
}

private void asserEqualsVerify(BaseDataPojo baseDataPojo, DataTypeProto.BaseData baseDataProto) {
assertTrue((baseDataPojo == null) == (!baseDataProto.isInitialized()));
if(baseDataPojo == null) {
return;
}
assertEquals(baseDataPojo.getDoubleVal(), baseDataProto.getDoubleVal(), 0.0000001D);
assertEquals(baseDataPojo.getFloatVal(), baseDataProto.getFloatVal(), 0.00000001D);
assertEquals(baseDataPojo.getInt32Val(), baseDataProto.getInt32Val());
assertEquals(baseDataPojo.getInt64Val(), baseDataProto.getInt64Val());
assertEquals(baseDataPojo.getUint32Val(), baseDataProto.getUint32Val());
assertEquals(baseDataPojo.getUint64Val(), baseDataProto.getUint64Val());
assertEquals(baseDataPojo.getSint32Val(), baseDataProto.getSint32Val());
assertEquals(baseDataPojo.getSint64Val(), baseDataProto.getSint64Val());
assertEquals(baseDataPojo.getFixed32Val(), baseDataProto.getFixed32Val());
assertEquals(baseDataPojo.getInt64Val(), baseDataProto.getInt64Val());
assertEquals(baseDataPojo.isBoolVal(), baseDataProto.getBoolVal());
assertEquals(baseDataPojo.isBoolVal(), baseDataProto.getBoolVal());
assertEquals(baseDataPojo.getStringVal(), baseDataProto.getStringVal());
// ByteString 转 base64 Strings
if(baseDataPojo.getBytesVal() == null) {
// 默认值为 ""
assertTrue(baseDataProto.getBytesVal().isEmpty());
} else {
assertEquals(baseDataPojo.getBytesVal(), BaseEncoding.base64().encode(baseDataProto.getBytesVal().toByteArray()));
}
// Enum 转 String
if(baseDataPojo.getEnumVal() == null) {
// 默认值为 0
assertEquals(DataTypeProto.Color.forNumber(0), baseDataProto.getEnumVal());
} else {
assertEquals(baseDataPojo.getEnumVal(), baseDataProto.getEnumVal().toString());
}
if(baseDataPojo.getReStrVal() == null) {
// 默认为空列表
assertEquals(0, baseDataProto.getReStrValList().size());
} else {
assertEquals(baseDataPojo.getReStrVal().size(), baseDataProto.getReStrValList().size());
for(int i = 0; i < baseDataPojo.getReStrVal().size(); i ++) {
assertEquals(baseDataPojo.getReStrVal().get(i), baseDataProto.getReStrValList().get(i));
}
}
if(baseDataPojo.getReIntVal() == null) {
// 默认为空列表
assertEquals(0, baseDataProto.getReIntValList().size());
} else {
assertEquals(baseDataPojo.getReIntVal().length, baseDataProto.getReIntValList().size());
for(int i = 0; i < baseDataPojo.getReIntVal().length; i ++) {
int v1 = baseDataPojo.getReIntVal()[i];
int v2 = baseDataProto.getReIntValList().get(i);
assertEquals(v1, v2);
}
}

if(baseDataPojo.getMapVal() == null) {
// 默认为空集合
assertEquals(0, baseDataProto.getMapValMap().size());
} else {
assertEquals(baseDataPojo.getMapVal().size(), baseDataProto.getMapValMap().size());
for(Map.Entry<String, DataTypeProto.BaseData> entry: baseDataProto.getMapValMap().entrySet()) {
asserEqualsVerify(baseDataPojo.getMapVal().get(entry.getKey()), entry.getValue());
}
}
}

@Test
public void testDefaultValue() {
DataTypeProto.BaseData baseData = DataTypeProto.BaseData.newBuilder()
.setInt32Val(0)
.setStringVal("")
.addAllReStrVal(new ArrayList<>())
.setBoolVal(false)
.setDoubleVal(3.14D)
.build();
// 默认值不会输出
// double_val: 3.14
System.out.println(baseData);
}
}

以上测试是可以完成通过的,特别需要注意的是类类型的属性的默认值。Protobuf中是没有null值的,所以类类型属性的默认值也不会是null。但映射到了Pojo时,ProtoBean的默认值会转化为Pojo的默认值,也就是Java中数据类型的默认值。

默认值列表

类型 Proto默认值 Pojo默认值
int 0 0
long 0L 0L
float 0F 0F
double 0D 0D
boolean false false
string “” null
BytesString “” (string) null
enum 0 (string) null
message {} (object) null
repeated [] (List/Array) null
map [] (Map) null

该列表仅仅是做了一个简单的列举,如果需要更加详细的信息,建议看protobuf的官方文档。或者还有一种取巧的方法,就是创建一个含有所有数据类型的ProtoBean,如这里的DataTypeProto.BaseData,然后看该类里面得无参构造函数就大概可以知道是什么默认值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
private static final DataTypeProto.BaseData DEFAULT_INSTANCE;
static {
DEFAULT_INSTANCE = new DataTypeProto.BaseData();
}
private BaseData() {
stringVal_ = "";
bytesVal_ = com.google.protobuf.ByteString.EMPTY;
enumVal_ = 0;
reStrVal_ = com.google.protobuf.LazyStringArrayList.EMPTY;
reIntVal_ = emptyIntList();
}
public static iDataTypeProto.BaseData getDefaultInstance() {
return DEFAULT_INSTANCE;
}
...

这里还是特别强调一下,protobuf没有null值,不能设置null值,也获取不到null值。

Protobuf 支持的Java数据类型见:com.google.protobuf.Descriptors.FieldDescriptor.JavaType

参考和推荐阅读

CATALOG
  1. 1. 前言
  2. 2. 转化的基本思路
    1. 2.1. 直接转化
    2. 2.2. 间接转化(货币兑换)
  3. 3. 代码实现
  4. 4. 测试
  5. 5. 参考和推荐阅读