前言
这篇文章是《Protobuf 与 Json 的相互转化》的一个后续,主要是为了解决系统分层中不同 ProtoBean 与 POJO 的相互转化问题。转化的 Protobuf 和 Pojo 具有相同名称及类型的属性(当 Proto 属性类型为 Message 时,对应的为 Pojo 的 Object 类型的属性,两者应该具有相同的属性)。
转化的基本思路
测试使用的 protobuf 文件如下:
StudentProto.proto
syntax = "proto3";
option java_package = "io.gitlab.donespeak.javatool.toolprotobuf.proto";
message Student {
string name = 1;
int32 age = 2;
Student deskmate = 3;
}
DataTypeProto.proto
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;
}
直接转化
通过映射的方法,直接将同名同类别的属性进行复制。该实现方式主要通过反射机制进行实现。
[A] <--> [B]
直接转化的方式需要通过 protobuf 的反射机制才能实现地了,难度会比较大,也正在尝试实现。另一种方式是尝试使用 Apache Common BeanUtils
或者 Spring BeanUtils
,进行属性拷贝。这里使用Spring BeanUtils
进行设计,代码如下:
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 的反射进行实现了。这个不是本文的侧重点,我们继续看另一种实现。
间接转化(货币兑换)
通过一个统一的媒介进行转化,就好比货币一样,比如人名币要转日元,银行会先将人名币转美元,再将美元转为日元,反向也是如此。
[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 的格式。
<!-- 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
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
的方法如下:
// 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
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
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
,然后看该类里面得无参构造函数就大概可以知道是什么默认值了。
...
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
参考和推荐阅读
- Protobuf 与 Json 的相互转化 @DoneSpeak
- Protocol Buffers @Google Developers
- com.google.protobuf/protobuf-java-util @Github
- com.googlecode.protobuf-java-format/protobuf-java-format @Github
- Protocol Buffers, Part 3 — JSON Format
- Converting Protocol Buffers data to Json and back with Gson Type Adapters
- Any 源码 @Github
- Any 官方文档 @Office