写点什么

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

用户头像
DoneSpeak
关注
发布于: 1 小时前
Protobuf与POJO的相互转化 - 通过Json

前言

这篇文章是《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-e1ca0af27774final 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)@Builderpublic 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 中数据类型的默认值。


默认值列表



该列表仅仅是做了一个简单的列举,如果需要更加详细的信息,建议看 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

参考和推荐阅读


发布于: 1 小时前阅读数: 2
用户头像

DoneSpeak

关注

Let the Work That I've Done Speak for Me 2018.05.10 加入

Java后端开发

评论

发布
暂无评论
Protobuf与POJO的相互转化 - 通过Json