最近的工作中开始使用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.donespeak.javatool.toolprotobuf.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; }
可选择的工具
com.google.protobuf/protobuf-java-utilcom.googlecode.protobuf-java-format/protobuf-java-formatcom.google.protobuf/protobuf-java-utilprotobuf-java-formatJsonFormat{"key": "", "value": ""}protobuf-java-utilJsonFormat
<!-- 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>
代码实现
import com.google.gson.Gson; import com.google.protobuf.Message; import com.google.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
AnyOneofOneof
而对于Any的处理,则会比较特别。如果直接转化,会得到类似如下的异常,无法找到typeUrl指定的类型。
com.google.protobuf.InvalidProtocolBufferException: Cannot find type for url: type.googleapis.com/data.proto.BaseData at com.google.protobuf.util.JsonFormat$PrinterImpl.printAny(JsonFormat.java:807) at com.google.protobuf.util.JsonFormat$PrinterImpl.access$900(JsonFormat.java:639) at com.google.protobuf.util.JsonFormat$PrinterImpl$1.print(JsonFormat.java:709) at com.google.protobuf.util.JsonFormat$PrinterImpl.print(JsonFormat.java:688) at com.google.protobuf.util.JsonFormat$PrinterImpl.printSingleFieldValue(JsonFormat.java:1183) at com.google.protobuf.util.JsonFormat$PrinterImpl.printSingleFieldValue(JsonFormat.java:1048) at com.google.protobuf.util.JsonFormat$PrinterImpl.printField(JsonFormat.java:972) at com.google.protobuf.util.JsonFormat$PrinterImpl.print(JsonFormat.java:950) at com.google.protobuf.util.JsonFormat$PrinterImpl.print(JsonFormat.java:691) at com.google.protobuf.util.JsonFormat$Printer.appendTo(JsonFormat.java:332) at com.google.protobuf.util.JsonFormat$Printer.print(JsonFormat.java:342) at io.gitlab.donespeak.javatool.toolprotobuf.ProtoJsonUtil.toJson(ProtoJsonUtil.java:12) at io.gitlab.donespeak.javatool.toolprotobuf.ProtoJsonUtilTest.toJson2(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.
AnyTypeRegirstry
@Test public void toJson() throws IOException { // 可以为 TypeRegistry 添加多个不同的Descriptor JsonFormat.TypeRegistry typeRegistry = JsonFormat.TypeRegistry.newBuilder() .add(DataTypeProto.BaseData.getDescriptor()) .build(); // usingTypeRegistry 方法会重新构建一个Printer JsonFormat.Printer printer = JsonFormat.printer() .usingTypeRegistry(typeRegistry); String json = printer.print(DataTypeProto.DataWithAny.newBuilder() .setAnyVal( Any.pack( DataTypeProto.BaseData.newBuilder().setInt32Val(1235).build())) .build()); System.out.println(json); }
从上面的实现中,很容易会想到一个问题:对于一个Any类型的字段,必须先注册所有相关的Message类型,才能够正常地进行转化为Json。同理,当我们使用JsonFormat.parser().merge(json, targetBuilder);
时候,也必须先给Printer添加相关的Message,这必然导致整个代码出现很多重复。
MessageAnyDescriptorPrinterrepeatedmap
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) { return typeUrlPrefix.endsWith("/") ? typeUrlPrefix + descriptor.getFullName() : typeUrlPrefix + "/" + descriptor.getFullName(); } public static <T extends com.google.protobuf.Message> Any pack(T message) { return Any.newBuilder() .setTypeUrl(getTypeUrl("type.googleapis.com", message.getDescriptorForType())) .setValue(message.toByteString()) .build(); } public static <T extends Message> Any pack(T message, String typeUrlPrefix) { return Any.newBuilder() .setTypeUrl(getTypeUrl(typeUrlPrefix, message.getDescriptorForType())) .setValue(message.toByteString()) .build(); } public <T extends Message> boolean is(Class<T> clazz) { T defaultInstance = com.google.protobuf.Internal.getDefaultInstance(clazz); return getTypeNameFromTypeUrl(getTypeUrl()).equals( defaultInstance.getDescriptorForType().getFullName()); } 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 = com.google.protobuf.Internal.getDefaultInstance(clazz); T result = (T) defaultInstance.getParserForType().parseFrom(getValue()); cachedUnpackValue = result; return result; } ... }
value_typeUrl_typeUrl_unpackparseFromAny
写到最后,还是没有办法按照想法中那样,写出一个直接将Message转化为json的通用方法。虽然没法那么智能,那就手动将所有能够的Message都注册进去吧。
package io.gitlab.donespeak.javatool.toolprotobuf; import com.google.protobuf.Descriptors; import com.google.protobuf.Message; import com.google.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 = JsonFormat.TypeRegistry.newBuilder().add(anyFieldDescriptor).build(); printer = JsonFormat.printer().usingTypeRegistry(typeRegistry); parser = JsonFormat.parser().usingTypeRegistry(typeRegistry); } public String toJson(Message sourceMessage) throws IOException { String json = printer.print(sourceMessage); return json; } public Message toProto(Message.Builder targetBuilder, String json) throws IOException { parser.merge(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
package io.gitlab.donespeak.javatool.toolprotobuf; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParser; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; import io.gitlab.donespeak.javatool.toolprotobuf.proto.DataTypeProto; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * @author Yang Guanrong * @date 2019/08/31 17:23 */ public class ProtoGsonUtil { public static String toJson(Message message) { return getGson(message.getClass()).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 = gsonBuilder.registerTypeAdapter(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) { this.messageClass = 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 = messageClass.getMethod("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 = DataTypeProto.OnlyInt32.newBuilder() .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包中的Message.getAllFields()方法不会返回与默认值相等的字段。
因此,调用Message.getAllFields()方法是无法返回所有字段的