這篇文章是《Protobuf與Json的相互轉化》的一個後續,主要是爲了解決系統分層中不一樣ProtoBean與POJO的相互轉化問題。轉化的Protobuf和Pojo具備相同名稱及類型的屬性(當Proto屬性類型爲Message時,對應的爲Pojo的Object類型的屬性,二者應該具備相同的屬性)。java
測試使用的protobuf文件以下: git
StudentProto.protogithub
syntax = "proto3"; option java_package = "io.gitlab.donespeak.javatool.toolprotobuf.proto"; message Student { string name = 1; int32 age = 2; Student deskmate = 3; }
DataTypeProto.protojson
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; }
經過映射的方法,直接將同名同類別的屬性進行復制。該實現方式主要經過反射機制進行實現。ide
[ 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); } } }
這個實現是必然會有問題的,緣由有以下幾點工具
get**Value()
方法,並據此命名Pojo屬性名總的來講,BeanUtils
不適合用於實現這個任務。只能後續考慮使用Protobuf的反射進行實現了。這個不是本文的側重點,咱們繼續看另外一種實現。gitlab
經過一個統一的媒介進行轉化,就比如貨幣同樣,好比人名幣要轉日元,銀行會先將人名幣轉美圓,再將美圓轉爲日元,反向也是如此。post
[ 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.
添加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