使用JsonFormat映射protobuf和javabean

使用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的结果为
使用JsonFormat映射protobuf和javabean

从中可以看出所有的复合字段都被转成下划线连接的格式,bytes类型若是传字符串会存在乱码,BigDecimal可以和protobuf的数值转换(也可以和string转换),枚举被转成枚举值字符串,id设了默认值是被转换了,其他没设值的都没有转换。

fastjson转换的beanJson结果
使用JsonFormat映射protobuf和javabean
从中可以看出所有的复合字段还是小驼峰格式,日期为时间戳,id的值也被覆盖了。

builderProto结果
使用JsonFormat映射protobuf和javabean
然而抛异常了。追异常栈可知在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*/

得到的结果
使用JsonFormat映射protobuf和javabean
可用发现如果设置了createTime的指定字段还是会报错,不是反而不报错但是没有值,因为这里是时间戳,并不支持转换成protobuf的string。
使用在转换json是要指定日期的转换格式

com.googlecode.protobuf.format.JsonFormat.merge(JSON.toJSONString(orikaDTO, SerializerFeature.UseISO8601DateFormat), builderProto);

使用JsonFormat映射protobuf和javabean
这样就可以正常转换了,而且这里的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的结果
使用JsonFormat映射protobuf和javabean
从结果可以看出:因为指定了“disable”默认值输出,“fixed64Count”并没有设置,所以结果中只有“disable”的值;和F.JsonFormat不同的是这里的字段即使是复合命名的也是小驼峰输出的,同时any类型也可以正常输出。

在使用fastjson转bean时就直接报错了,虽然fastjson可以使用@type进行任意类型的转换,但是@type的值并不符合fastjson源码中定义的解构(1.2.24前版本的fastjson会有因此而造成的漏洞)。
使用JsonFormat映射protobuf和javabean
要想使用any类型目前所知的方法只用使用Gson取代fastjson。使用fastjson的话建议设置@JSONField(serialize=false)、@JSONField(deserialize=false)不进行序列化,之后手动设置。

但是直接将bean用fastjson或Gson转成json再转成protobuf,但是转成json后 “detail”并不符合any类型本身定义的字符串格式,只会被当做是一个json对象,转换会失败。
使用JsonFormat映射protobuf和javabean
目前没有找到直接将对象类型转换成any类型的方法,所以要不就不用any类型,要不就手动设置。

使用U.JsonFormat时注意,如果protobuf的对象字段设置了,从json转到这对象上是不会覆盖值,而是报错。
使用JsonFormat映射protobuf和javabean

最后试着用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的结果
使用JsonFormat映射protobuf和javabean
builderProto的结果
使用JsonFormat映射protobuf和javabean

总结

当然不使用该方法,习惯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

上一篇:protobuf 与 gRPC相关


下一篇:JAVA 调用 com.google.protobuf