Protobuf与Json的相互转化
前⾔
最近的⼯作中开始使⽤Google的Protobuf构建REST API,按照现在使⽤的感觉,除了应为Protobuf的特性,接⼝被严格确定下来之外,暂时还么有感受到其他特别的好处。说是Protobuf⽐Json的序列化更⼩更快,但按照⽬前的需求,估计很就都没有还不会有这个性能的需要。既然是全新的技术,我⾮常地乐意学习。
在MVC的代码架构中,Protbuf是Controller层⽤到的技术,为了能够将每个层进⾏划分,使得Service层的实现不依赖于Protobuf,需要将Protobuf的实体类,这⾥称之为ProtoBean吧,转化为POJO。在实现的过程中,有涉及到了Protobuf转Json的实现,因为有了这篇⽂章。⽽ProtoBean转POJO的讲解我会在另⼀篇,或者是⼏篇⽂章中进⾏讲解,因为会⽐较复杂。
这篇⽂章已经放了很久很久了,⼀直希望去看两个JsonFormat的实现。想看完了再写的,但还是先写出来吧,拖着挺累的。
为了读者可以顺畅地阅读,⽂章中涉及到地链接都会在最后给出,⽽不会在⾏⽂中间给出。
测试使⽤的Protobuf⽂件如下:
syntax = "proto3";
import "google/protobuf/any.proto";
option java_package = "io.gitlab.lprotobuf.proto";
package data.proto;
message OnlyInt32 {
int32 int_val = 1;
}
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;
repeated string re_str_val = 17;
map<string, BaseData> map_val = 18;
}
message DataWithAny {
double double_val = 1;
float float_val = 2;
int32 int32_val = 3;
int64 int64_val = 4;
bool bool_val = 13;
string string_val = 14;
bytes bytes_val = 15;
repeated string re_str_val = 17;
map<string, BaseData> map_val = 18;
google.protobuf.Any anyVal = 102;
}
可选择的⼯具
可以将ProtoBean转化为Json的⼯具有两个,⼀个是le.protobuf/protobuf-java-util,另⼀个是lecode.protobuf-java-format/protobuf-java-format,两个的性能和效果还有待对⽐。这⾥使⽤的是le.protobuf/protobuf-java-util,原因在于protobuf-java-format中的JsonFormat会将Map格式化为{"key": "", "value": ""}的对象列表,⽽protobuf-java-util中的JsonFormat能够序列化为理想的key-value的结构。
<!-- mvnrepository/le.protobuf/protobuf-java-util -->
<dependency>
<groupId&le.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.7.1</version>
</dependency>
<!-- mvnrepository/lecode.protobuf-java-format/protobuf-java-format -->
<dependency>
<groupId&lecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.4</version>
</dependency>
代码实现
le.gson.Gson;
le.protobuf.Message;
le.protobuf.util.JsonFormat;
import java.io.IOException;
/**
* 特别主要:
* <ul>
* <li>该实现⽆法处理含有Any类型字段的Message</li>
* <li>enum类型数据会转化为enum的字符串名</li>
* <li>bytes会转化为utf8编码的字符串</li>
* </ul>
* @author Yang Guanrong
* @date 2019/08/20 17:11
*/
public class ProtoJsonUtils {
public static String toJson(Message sourceMessage)
throws IOException {
String json = JsonFormat.printer().print(sourceMessage);
return json;
}
public static Message toProtoBean(Message.Builder targetBuilder, String json) throws IOException {
JsonFormat.parser().merge(json, targetBuilder);
return targetBuilder.build();
}
}
对于⼀般的数据类型,如int,double,float,long,string都能够按照理想的⽅式进⾏转化。对于protobuf中的enum类型字段,会被按照enum的名称转化为string。对于bytes类型的字
段,则会转化为utf8类型的字符串。
Any 以及 Oneof
Any和Oneof是protobuf中⽐较特别的两个类型,如果尝试将含有Oneof字段转化为json,是可以正常转化的,字段名为被赋值的oneof字段的名称。
⽽对于Any的处理,则会⽐较特别。如果直接转化,会得到类似如下的异常,⽆法到typeUrl指定的类型。
le.protobuf.util.JsonFormat$PrinterImpl.printAny(JsonFormat.java:807)
le.protobuf.util.JsonFormat$PrinterImpl.access$900(JsonFormat.java:639)
le.protobuf.util.JsonFormat$PrinterImpl$1.print(JsonFormat.java:709)
le.protobuf.util.JsonFormat$PrinterImpl.print(JsonFormat.java:688)
le.protobuf.util.JsonFormat$PrinterImpl.printSingleFieldValue(JsonFormat.java:1183)
le.protobuf.util.JsonFormat$PrinterImpl.printSingleFieldValue(JsonFormat.java:1048)
le.protobuf.util.JsonFormat$PrinterImpl.printField(JsonFormat.java:972)
le.protobuf.util.JsonFormat$PrinterImpl.print(JsonFormat.java:950)
le.protobuf.util.JsonFormat$PrinterImpl.print(JsonFormat.java:691)
le.protobuf.util.JsonFormat$Printer.appendTo(JsonFormat.java:332)
le.protobuf.util.JsonFormat$Printer.print(JsonFormat.java:342)
at io.gitlab.Json(ProtoJsonUtil.java:12)
at io.gitlab.Json2(ProtoJsonUtilTest.java:72)
...
为了解决这个问题,我们需要⼿动添加typeUrl对应的类型,我是从Tomer Rothschild的⽂章《Protocol Buffers, Part 3 — JSON Format》到的答案。到之前可是苦恼了很
久。事实上,在print⽅法的上⽅就显赫地写着该⽅法会因为没有any的types⽽抛出异常。
/**
* Converts a protobuf message to JSON format. Throws exceptions if there
* are unknown Any types in the message.
*/
public String print(MessageOrBuilder message) throws InvalidProtocolBufferException {
...
}
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.
上⾯的实现⽆法处理得了Any类型的数据。需要⾃⼰添加TypeRegirstry才能进⾏转化。
@Test
public void toJson() throws IOException {
// 可以为 TypeRegistry 添加多个不同的Descriptor
JsonFormat.TypeRegistry typeRegistry = wBuilder()
.add(Descriptor())
.build();
// usingTypeRegistry ⽅法会重新构建⼀个Printer
JsonFormat.Printer printer = JsonFormat.printer()
.usingTypeRegistry(typeRegistry);
String json = printer.print(wBuilder()
.setAnyVal(
Any.pack(
wBuilder().setInt32Val(1235).build()))
.build());
System.out.println(json);
}
从上⾯的实现中,很容易会想到⼀个问题:对于⼀个Any类型的字段,必须先注册所有相关的Message类型,才能够正常地进⾏转化为Json。同理,当我们使⽤JsonFormat.parser().merge(json, targetBuilder);时候,也必须先给Pri
为了解决这个问题,我尝试直接从Message中取出所有的Any字段中值的Message的Descriptor,然后再创建Printer,这样就可以得到⼀个通⽤的转化⽅法了。最后还是失败了。原本
以为会卡在repeated或者map的范型中,但最后发现这些都不是问题,⾄少在从protoBean转化为json中不会是问题。问题出在Any的设计本⾝⽆法实现这个需求。
简单地讲⼀下Any,Any的源码不是很多,可以⼤概抽取部分代码如下:
public final class Any
extends GeneratedMessageV3 implements AnyOrBuilder {
// typeUrl_ 会是⼀个 java.lang.String 值
private volatile Object typeUrl_;
private ByteString value_;
private static String getTypeUrl(String typeUrlPrefix, Descriptors.Descriptor descriptor) {
dsWith("/")
? typeUrlPrefix + FullName()
: typeUrlPrefix + "/" + FullName();
}
public static <T le.protobuf.Message> Any pack(T message) {
wBuilder()
.setTypeUrl(getTypeUrl("leapis",
.ByteString())
.build();
}
public static <T extends Message> Any pack(T message, String typeUrlPrefix) {
wBuilder()
.setTypeUrl(getTypeUrl(typeUrlPrefix,
.ByteString())
.build();
}
public <T extends Message> boolean is(Class<T> clazz) {
T defaultInstance = le.DefaultInstance(clazz);
return getTypeNameFromTypeUrl(getTypeUrl()).equals(
}
private volatile Message cachedUnpackValue;
@java.lang.SuppressWarnings("unchecked")
public <T extends Message> T unpack(Class<T> clazz) throws InvalidProtocolBufferException {
if (!is(clazz)) {
throw new InvalidProtocolBufferException("Type of the Any message does not match the given class.");
}
if (cachedUnpackValue != null) {
return (T) cachedUnpackValue;
}
T defaultInstance = le.DefaultInstance(clazz);
T result = (T) ParserForType().parseFrom(getValue());
cachedUnpackValue = result;
return result;
}
...
}
从上⾯的代码中,我们可以很容易地看出,Any类型的字段存储的是Any类型的Message,与原本的Message值没有关系。⽽保存为Any之后,Any会将其保存到ByteString
的value_中,并构建⼀个typeUrl_,所以从⼀个Any对象中,我们是⽆法得知原本⽤于构造该Any对象的Message对象的类型是什么(typeUrl_只是给出了⼀个描述,⽆法⽤反射等⽅法得到原本的类类型)。在unpack⽅法,实现⽤的⽅法是先⽤class构建出⼀个⽰例对象,在⽤parseFrom⽅法恢复原本的值。到
这⾥我就特别好奇,为什么Any这个类就不能保存value原本的类类型进去呢?或者直接将value定义为Message对象也好呀,这样处理起来就会⽅便很多,⽽且也不会影响到序列化才对吧。要能够渗透设计者的意图,还有很多需要学习了解的地⽅。
写到最后,还是没有办法按照想法中那样,写出⼀个直接将Message转化为json的通⽤⽅法。虽然没法那么智能,那就⼿动将所有能够的Message都注册进去吧。
package io.gitlab.lprotobuf;
le.protobuf.Descriptors;
le.protobuf.Message;
le.protobuf.util.JsonFormat;
import java.io.IOException;
import java.util.List;
public class ProtoJsonUtilV1 {
private final JsonFormat.Printer printer;
private final JsonFormat.Parser parser;
public ProtoJsonUtilV1() {
printer = JsonFormat.printer();
parser = JsonFormat.parser();
}
public ProtoJsonUtilV1(List<Descriptors.Descriptor> anyFieldDescriptor) {
JsonFormat.TypeRegistry typeRegistry = wBuilder().add(anyFieldDescriptor).build();
printer = JsonFormat.printer().usingTypeRegistry(typeRegistry);
parser = JsonFormat.parser().usingTypeRegistry(typeRegistry);
}
浏览器json格式化public String toJson(Message sourceMessage) throws IOException {
String json = printer.print(sourceMessage);
return json;
}
public Message toProto(Message.Builder targetBuilder, String json) throws IOException {
<(json, targetBuilder);
return targetBuilder.build();
}
}
通过Gson进⾏实现
在查资料的过程中,还发现了⼀种通过Gson完成的转化⽅法。来⾃Alexander Moses的《Converting
Protocol Buffers data to Json and back with Gson Type Adapters》。但我觉得他的这篇⽂章中有⼏点没有说对,⼀个是protbuf的插件现在还是有不错的,⽐如Idea就很容易到,vscode的也很容易搜到,eclipse的可以⽤protobuf-dt(这个dt会有点问题,有机会讲下)。⽂章写得很是清楚,我这⾥主要是将他的实现改成更加通⽤⼀点。
这个实现还是上⾯的JsonFormat,所以也没有⽀持Any的转化。如果想⽀持Any,可以按照上⾯的代码进⾏修改,这⾥就不多做修改了。
package io.gitlab.lprotobuf;
le.gson.Gson;
le.gson.GsonBuilder;
le.gson.JsonParser;
le.gson.TypeAdapter;
le.gson.stream.JsonReader;
le.gson.stream.JsonWriter;
le.protobuf.Message;
le.protobuf.util.JsonFormat;
import io.gitlab.lprotobuf.proto.DataTypeProto;
import java.io.IOException;
import flect.InvocationTargetException;
import flect.Method;
/**
* @author Yang Guanrong
* @date 2019/08/31 17:23
*/
public class ProtoGsonUtil {
public static String toJson(Message message) {
return Class()).toJson(message);
}
public static <T extends Message> Message toProto(Class<T> klass, String json) {
return getGson(klass).fromJson(json, klass);
}
/**
* 如果这个⽅法要设置为public⽅法,那么需要确定gson是否是⼀个不可变对象,否则就不应该开放出去
*
* @param messageClass
* @param <E>
* @return
*/
private static <E extends Message> Gson getGson(Class<E> messageClass) {
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = isterTypeAdapter(DataTypeProto.OnlyInt32.class, new MessageAdapter(messageClass)).create();
return gson;
}
private static class MessageAdapter<E extends Message> extends TypeAdapter<E> {
private Class<E> messageClass;
public MessageAdapter(Class<E> messageClass) {
}
@Override
public void write(JsonWriter jsonWriter, E value) throws IOException {
jsonWriter.jsonValue(JsonFormat.printer().print(value));
}
@Override
public E read(JsonReader jsonReader) throws IOException {
try {
// 这⾥必须⽤范型<E extends Message>,不能直接⽤ Message,否则将不到 newBuilder ⽅法
Method method = Method("newBuilder");
// 调⽤静态⽅法
E.Builder builder = (E.Builder)method.invoke(null);
JsonParser jsonParser = new JsonParser();
JsonFormat.parser().merge(jsonParser.parse(jsonReader).toString(), builder);
return (E)builder.build();
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
throw new ProtoJsonConversionException(e);
}
}
}
public static void main(String[] args) {
DataTypeProto.OnlyInt32 data = wBuilder()
.setIntVal(100)
.build();
String json = toJson(data);
System.out.println(json);
System.out.println(toProto(DataTypeProto.OnlyInt32.class, json));
}
}
参考
附:
protobuf-java-format包的坑,貌似这个包已经不维护了
使⽤了protobuf-java-format包将message对象转换成json串。但最后发现转换结果中值为0的字段全都不见了,排查了很久发现是protobuf-java包中的AllFields()⽅法不会返回与默认值相等的字段。
因此,调⽤AllFields()⽅法是⽆法返回所有字段的
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论