java protobuf 反射_Protobuf与POJO的相互转化 - 通过Json

易英奕
2023-12-01

前言

这篇文章是《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 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 toPojo(Class 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的格式。

com.google.protobuf

protobuf-java-util

3.7.1

com.googlecode.protobuf-java-format

protobuf-java-format

1.4

对于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转化时遵循如下的规则:

*

*

enum -> String

*

bytes -> base64 String

*

* @author Yang Guanrong

* @date 2019/08/18 23:44

*/

public class ProtoBeanUtils {

/**

* 将ProtoBean对象转化为POJO对象

*

* @param destPojoClass 目标POJO对象的类类型

* @param sourceMessage 含有数据的ProtoBean对象实例

* @param 目标POJO对象的类类型范型

* @return

* @throws IOException

*/

public static PojoType toPojoBean(Class 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 reStrVal;

private Map 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 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 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

参考和推荐阅读

 类似资料: