Protobuf與POJO的相互轉化 - 經過Json

前言

這篇文章是《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);
        }
    }
}

這個實現是必然會有問題的,緣由有以下幾點工具

  • ProtoBean不容許有null值,而Pojo容許有null值,從Pojo拷貝到Proto必然會有非空異常
  • BeanUtils 會按照方法名及getter/setter類型進行匹配,嵌套類型由於類型不匹配而沒法正常拷貝
  • Map和List的Proto屬性生成的Java會分別在屬性名後增長Map和List,若是但願可以進行拷貝,則須要按照這個規則明明Projo的屬性名
  • Enum類型不匹配沒法進行拷貝,若是但願可以進行拷貝,能夠嘗試使用ProtoBean的Enum域的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.

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

參考和推薦閱讀

相關文章
相關標籤/搜索