使用JsonFormat映射protobuf和javabean
JsonFomat是谷歌官方推出的protobuf映射工具,可以将protobuf对象转换成JSON,所以我们可以使用JsonFomat转换成的json在javabean和protobuf对象间转换。
JsonFormat有两个版本:com.googlecode.protobuf.format.JsonFormat(以下简称为F.JsonFormat)和com.google.protobuf.util.JsonFormat(以下简称为U.JsonFormat)。前者相对简单,后者可以指定需要转换的默认字段和对any类型的转换等。bytes类型的字段官方建议只用来传输base64编码的字节数组,不建议传输字符数据。默认情况下枚举类型是转成枚举值字符串。oneof类型可以正常转换。
protobuf2
定义一些基础对象
public class OrikaDTO {
private Integer id;
private String name;
private Date createTime;
private Timestamp lastUpdateTime;
private List<OrikaItemDTO> orikaItem;
private TypeEnum downloadResourceTypeEnum;
private OrikaItemDTO mainOrikaItem;
private Boolean disable;
private BigDecimal prePrice;
private BigDecimal totalPrice;
private Map<String, String> kv;
private String oneString;
private Integer oneInt;
OrikaItemDTO details;
}
public enum TypeEnum {
download_resource_type_enum_audio(0, "音频"),
download_resource_type_enum_vedio(1, "视频");
}
public class OrikaItemDTO {
private Double price;
private Integer uint32Count;
private Long uint64Count;
private Integer sint32Count;
private Long sint64Count;
private Integer fixed32Count;
private Long fixed64Count;
private Integer sfixed32Count;
private Long sfixed64Count;
private String type;
}
private OrikaTest.Builder buildOrika2() {
OrikaItem.Builder builderItem = OrikaItem.newBuilder();
builderItem.setPrice(1.2);
builderItem.setType(ByteString.copyFrom("1X我".getBytes()));
builderItem.setFixed64Count(111);
OrikaTest.Builder builder = OrikaTest.newBuilder();
builder.setPrePrice(12.3f);
builder.setMainOrikaItem(builderItem);
builder.addOrikaItem(builderItem);
builder.setDisable(false);
builder.setName("位置");
builder.putKv("k1", "v1");
builder.setCreateTime("2020-08-05 12:00:00");
builder.setDownloadResourceTypeEnum(DownloadResourceTypeEnum.download_resource_type_enum_audio);
builder.setOneInt(99);
builder.setOneString("ww");
return builder;
}
enum DownloadResourceTypeEnum{
download_resource_type_enum_audio = 0; //音频
download_resource_type_enum_vedio = 1; //视频
}
// orika测试对象
message OrikaTest {
optional int32 id = 1;
optional string name = 2;
optional string create_time = 3;
repeated OrikaItem orika_item = 4;
optional DownloadResourceTypeEnum download_resource_type_enum = 5;
optional OrikaItem main_orika_item = 6;
optional bool disable = 7;
optional float pre_price = 8;
map<string, string> kv = 9;
oneof test_oneof {
string one_string = 10;
int32 one_int = 11;
}
}
message OrikaItem {
optional int64 item_id = 1;
optional double price = 2;
optional uint32 uint32_count = 3; // 对应java int
optional uint64 uint64_count = 4; // 对应java Long
optional sint32 sint32_count = 5; // 对应java INT 比常规int32更有效地编码负数
optional sint64 sint64_count = 6; // 对应java long 比常规int64更有效地编码负数
optional fixed32 fixed32_count = 7; // 对应java int 总是四个字节。如果值通常大于228,则比uint32更有效
optional fixed64 fixed64_count = 8; // 对应java Long 总是八个字节。如果值通常大于256,则比uint64更有效
optional sfixed32 sfixed32_count = 9; // 对应java INT 总是四个字节。
optional sfixed64 sfixed64_count = 10; // 对应java long 总是八个字节。
optional bytes type = 11; // 不可变的字节数组 不涉及转码 所以传输字符时效率会比string高
}
测试方法
@Test
public void protoTest() throws InvalidProtocolBufferException, ParseException {
// protobuf转json
Builder builder = this.buildOrika2();
String builderJson = com.googlecode.protobuf.format.JsonFormat.printToString(builder.build());
OrikaDTO orikaDTO = JSON.parseObject(print, OrikaDTO.class); // 使用fastjson的结果OrikaDTO(id=0, name=位置, createTime=Wed Aug 05 12:00:00 CST 2020, lastUpdateTime=null, orikaItem=[OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=111, sfixed32Count=null, sfixed64Count=null, type=1X₩ネム)], downloadResourceTypeEnum=download_resource_type_enum_audio, mainOrikaItem=OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=111, sfixed32Count=null, sfixed64Count=null, type=1X₩ネム), disable=false, prePrice=12.3, totalPrice=null, kv={"value":"v1","key":"k1"}, oneString=ww, oneInt=null, details=null)
// json转protobuf
OrikaTest.Builder builderProto = OrikaTest.newBuilder();
builderProto.setId(444); // 会被beanJson中设置了值的id覆盖
String beanJson = JSON.toJSONString(orikaDTO);
com.googlecode.protobuf.format.JsonFormat.merge(beanJson, builderProto);
}
builderJson的结果为
从中可以看出所有的复合字段都被转成下划线连接的格式,bytes类型若是传字符串会存在乱码,BigDecimal可以和protobuf的数值转换(也可以和string转换),枚举被转成枚举值字符串,id设了默认值是被转换了,其他没设值的都没有转换。
fastjson转换的beanJson结果
从中可以看出所有的复合字段还是小驼峰格式,日期为时间戳,id的值也被覆盖了。
builderProto结果
然而抛异常了。追异常栈可知在JsonFormat的handleMissingField方法中
private static void handleMissingField(Tokenizer tokenizer,
ExtensionRegistry extensionRegistry,
Message.Builder builder) throws ParseException {
tokenizer.tryConsume(":");
if ("{".equals(tokenizer.currentToken())) {
// Message structure
tokenizer.consume("{");
do {
tokenizer.consumeIdentifier();
handleMissingField(tokenizer, extensionRegistry, builder);
} while (tokenizer.tryConsume(","));
tokenizer.consume("}");
} else if ("[".equals(tokenizer.currentToken())) {
// Collection
tokenizer.consume("[");
do {
handleMissingField(tokenizer, extensionRegistry, builder);
} while (tokenizer.tryConsume(","));
tokenizer.consume("]");
} else { //if (!",".equals(tokenizer.currentToken)){
// Primitive value
if ("null".equals(tokenizer.currentToken())) {
tokenizer.consume("null");
} else if (tokenizer.lookingAtInteger()) { // 在转mainOrikaItem时最终会走到这里,因为“mainOrikaItem”和“main_orika_item”并不能直接转换,“mainOrikaItem”被当成是一种MissingField,而“mainOrikaItem”又是一个对象类型,
// 其中的字段依旧被认为是MissingField, 而且在这里居然没有浮点型。
tokenizer.consumeInt64();
} else if (tokenizer.lookingAtBoolean()) {
tokenizer.consumeBoolean();
} else {
tokenizer.consumeString();
}
}
}
目前了解的解决方法
给java的bean对象显式地设置json映射字段,比如
@JSONField(name = "main_orika_item")
private OrikaItemDTO mainOrikaItem
或者设置PropertyNamingStrategy
SerializeConfig config = new SerializeConfig();
config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; // 以小驼峰输出json字符串
com.googlecode.protobuf.format.JsonFormat.merge(JSON.toJSONString(orikaDTO, config), builderProto);
/*CamelCase策略,Java对象属性:personId,序列化后属性:persionId
PascalCase策略,Java对象属性:personId,序列化后属性:PersonId
SnakeCase策略,Java对象属性:personId,序列化后属性:person_id
KebabCase策略,Java对象属性:personId,序列化后属性:person-id*/
得到的结果
可用发现如果设置了createTime的指定字段还是会报错,不是反而不报错但是没有值,因为这里是时间戳,并不支持转换成protobuf的string。
使用在转换json是要指定日期的转换格式
com.googlecode.protobuf.format.JsonFormat.merge(JSON.toJSONString(orikaDTO, SerializerFeature.UseISO8601DateFormat), builderProto);
这样就可以正常转换了,而且这里的protobuf也可以正常的反向转换成java的bean对象。
protobuf3
给必填字段设置默认值时,是不会转换成JSON结果字段的,且没有hasXXX判断字段是否设置值的方法,也就没法区分是否给该字段设置了默认值还是没设置;而且protobuf3还增加了any类型等其他属性;F.JsonFormat已经不能满足某些需求了,比如:列表的条件查询,某个字段不传值和传默认值其实查询的是两种结果;或是需要对any类型的映射。这就需要U.JsonFormat。maven需额外导入protobuf-java-util包。
定义一些基础对象
public OrikaTest3.Builder buildOrika3() {
OrikaItem3.Builder builderItem3 = OrikaItem3.newBuilder();
builderItem3.setPrice(1.2);
builderItem3.setFixed64Count(0);
OrikaTest3.Builder builder3 = OrikaTest3.newBuilder();
builder3.setMainOrikaItem(builderItem3);
builder3.addOrikaItem(builderItem3);
builder3.setDisable(false);
builder3.setName("是的");
builder3.putKv("k1", "v1");
builder3.setCreateTime("2020-08-05 12:00:00");
OrikaItem3.Builder builderItem3Any = OrikaItem3.newBuilder();
builderItem3Any.setPrice(111.2);
builderItem3Any.setFixed64Count(0);
Any any = Any.pack(builderItem3Any.build());
builder3.setDetails(any);
builder3.setDownloadResourceTypeEnum(DownloadResourceTypeEnum3.download_resource_type_enum_audio);
return builder3;
}
enum DownloadResourceTypeEnum3{
download_resource_type_enum_audio = 0; //音频
download_resource_type_enum_vedio = 1; //视频
}
// orika测试对象
message OrikaTest3 {
int32 id = 1;
string name = 2;
string create_time = 3;
repeated OrikaItem3 orika_item = 4;
DownloadResourceTypeEnum3 download_resource_type_enum = 5;
OrikaItem3 main_orika_item = 6;
bool disable = 7;
float pre_price = 8;
map<string, string> kv = 9;
google.protobuf.Any details = 10;
string total_price = 11;
}
message OrikaItem3 {
int64 item_id = 1;
double price = 2;
uint32 uint32_count = 3; // 对应java int
uint64 uint64_count = 4; // 对应java Long
sint32 sint32_count = 5; // 对应java INT 比常规int32更有效地编码负数
sint64 sint64_count = 6; // 对应java long 比常规int64更有效地编码负数
fixed32 fixed32_count = 7; // 对应java int 总是四个字节。如果值通常大于228,则比uint32更有效
fixed64 fixed64_count = 8; // 对应java Long 总是八个字节。如果值通常大于256,则比uint64更有效
sfixed32 sfixed32_count = 9; // 对应java INT 总是四个字节。
sfixed64 sfixed64_count = 10; // 对应java long 总是八个字节。
bytes type = 11; // 不可变的字节数组 不涉及转码 所以传输字符时效率会比string高
}
测试方法
TypeRegistry typeRegistry = TypeRegistry.newBuilder().add(OrikaItem3.getDescriptor()).build(); // any类型转换,不指定usingTypeRegistry会抛出Cannot find type for url: type.googleapis.com/OrikaItem
List<FieldDescriptor> fields = builder.getDescriptorForType().getFields();
Set<FieldDescriptor> fieldSet = new TreeSet<>();
for (FieldDescriptor field : fields) {
// 设置需要转换的默认值字段集合
if ("disable".equals(field.getName())) {
fieldSet.add(field);
}
}
String json = JsonFormat.printer()
.usingTypeRegistry(typeRegister) // 如果需要映射any类型的字段,需要设置该方法,自定义TypeRegister。
.includingDefaultValueFields(fieldSet) // 设置需要转换的默认值字段,无参情况下会转换所有字段
.printingEnumsAsInts() // 枚举转成int不转成枚举值字符串
.print(builder); // protobuf转json
OrikaDTO orikaDTOFastJson = JSON.parseObject(json, OrikaDTO.class); // fastjson
Gson gson = new Gson();
OrikaDTO orikaDTOGson = gson.fromJson(json, OrikaDTO.class); // gson OrikaDTO(id=null, name=是的, createTime=Wed Aug 05 12:00:00 CST 2020, lastUpdateTime=null, orikaItem=[OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=null, sfixed32Count=null, sfixed64Count=null, type=null)], downloadResourceTypeEnum=null, mainOrikaItem=OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=null, sfixed32Count=null, sfixed64Count=null, type=null), disable=false, prePrice=null, totalPrice=null, kv={k1=v1}, oneString=null, oneInt=null, details=OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=null, sfixed32Count=null, sfixed64Count=null, type=null))
OrikaTest3.Builder builderProto3 = OrikaTest3.newBuilder();
builderProto3.setName("限制"); // orikaDTOGson有name字段值时,无法被覆盖,直接报错
JsonFormat.parser()
.usingTypeRegistry(typeRegister)
.merge(JSON.toJSONString(orikaDTOGson), builderProto3); // json转protobuf
json的结果
从结果可以看出:因为指定了“disable”默认值输出,“fixed64Count”并没有设置,所以结果中只有“disable”的值;和F.JsonFormat不同的是这里的字段即使是复合命名的也是小驼峰输出的,同时any类型也可以正常输出。
在使用fastjson转bean时就直接报错了,虽然fastjson可以使用@type进行任意类型的转换,但是@type的值并不符合fastjson源码中定义的解构(1.2.24前版本的fastjson会有因此而造成的漏洞)。
要想使用any类型目前所知的方法只用使用Gson取代fastjson。使用fastjson的话建议设置@JSONField(serialize=false)、@JSONField(deserialize=false)不进行序列化,之后手动设置。
但是直接将bean用fastjson或Gson转成json再转成protobuf,但是转成json后 “detail”并不符合any类型本身定义的字符串格式,只会被当做是一个json对象,转换会失败。
目前没有找到直接将对象类型转换成any类型的方法,所以要不就不用any类型,要不就手动设置。
使用U.JsonFormat时注意,如果protobuf的对象字段设置了,从json转到这对象上是不会覆盖值,而是报错。
最后试着用U.JsonFormat转protobuf2
String print = JsonFormat.printer().print(builder.build()); // protobuf转json
OrikaDTO orikaDTO = JSON.parseObject(print, OrikaDTO.class); // OrikaDTO(id=0, name=null, createTime=Wed Aug 05 12:00:00 CST 2020, lastUpdateTime=null, orikaItem=[OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=111, sfixed32Count=null, sfixed64Count=null, type=MVjmiJE=)], downloadResourceTypeEnum=download_resource_type_enum_audio, mainOrikaItem=OrikaItemDTO(price=1.2, uint32Count=null, uint64Count=null, sint32Count=null, sint64Count=null, fixed32Count=null, fixed64Count=111, sfixed32Count=null, sfixed64Count=null, type=MVjmiJE=), disable=false, prePrice=12.3, totalPrice=null, kv={k1=v1}, oneString=ww, oneInt=null, details=null)
OrikaTest.Builder builderProto = OrikaTest.newBuilder();
SerializeConfig config = new SerializeConfig();
config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
JsonFormat.parser().merge(JSON.toJSONString(orikaDTO, config), builderProto); // json转protbuf
print的结果
builderProto的结果
总结
当然不使用该方法,习惯get/set也可以,毕竟更保险,此方法仅供参考。
protobuf2的推荐用法:
/* com.googlecode.protobuf.format.JsonFormat 项目中可以直接使用*/
String json = JsonFormat.printToString(builder.build()); // protobuf转json
JSON.parseObject(json , Class.class); // json转bean
SerializeConfig config = new SerializeConfig();
config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; // 配置这个就不需要去给每个字段做json映射了
String json = JSON.toJSONString(Object, config, SerializerFeature.UseISO8601DateFormat); // bean转json,日期格式化,字段名格式化
JsonFormat.merge(json, Bulider); // json转protobuf
/* com.google.protobuf.util.JsonFormat 需要额外导入此jar包*/
String json = JsonFormat.printer().print(builder.build()); // protobuf转json
JSON.parseObject(json, Class.class); // json转bean
OrikaTest.Builder builderProto = OrikaTest.newBuilder();
SerializeConfig config = new SerializeConfig();
config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
String json = JSON.toJSONString(orikaDTO, config)
JsonFormat.parser().merge(json, builderProto); // json转protbuf
protobuf3的推荐用法:
/* com.google.protobuf.util.JsonFormat */
List<FieldDescriptor> fields = builder.getDescriptorForType().getFields();
Set<FieldDescriptor> fieldSet = new TreeSet<>();
for (FieldDescriptor field : fields) {
// 设置需要转换的默认值字段集合
if ("disable".equals(field.getName())) {
fieldSet.add(field);
}
}
String json = JsonFormat.printer()
.includingDefaultValueFields(fieldSet) // 设置需要转换的默认值字段,无参情况下会转换所有字段 可选
.printingEnumsAsInts() // 枚举转成int不转成枚举值字符串 可选
.print(builder); // protobuf转json
JSON.parseObject(json, Class.class); // json转bean
String json = JSON.toJSONString(Object); // bean转json
OrikaTest3.Builder builderProto3 = OrikaTest3.newBuilder();
JsonFormat.parser().merge(json, builderProto3); // json转protobuf
参考链接
protobuf文档
protobuf-java-api
fastjson