最近的工做中開始使用Google的Protobuf構建REST API,按照如今使用的感受,除了應爲Protobuf的特性,接口被嚴格肯定下來以外,暫時還麼有感覺到其餘特別的好處。說是Protobuf比Json的序列化更小更快,但按照目前的需求,估計很就都沒有還不會有這個性能的須要。既然是全新的技術,我很是地樂意學習。 java
在MVC的代碼架構中,Protbuf是Controller層用到的技術,爲了可以將每一個層進行劃分,使得Service層的實現不依賴於Protobuf,須要將Protobuf的實體類,這裏稱之爲ProtoBean吧,轉化爲POJO。在實現的過程當中,有涉及到了Protobuf轉Json的實現,由於有了這篇文章。而ProtoBean轉POJO的講解我會在另外一篇,或者是幾篇文章中進行講解,由於會比較複雜。 git
這篇文章已經放了好久好久了,一直但願去看兩個JsonFormat的實現。想看完了再寫的,但仍是先寫出來吧,拖着挺累的。 github
爲了讀者能夠順暢地閱讀,文章中涉及到地連接都會在最後給出,而不會在行文中間給出。 json
測試使用的Protobuf文件以下:api
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; }
能夠將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的結構。架構
<!-- 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類型的字符串。app
Any
和 Oneof
是protobuf中比較特別的兩個類型,若是嘗試將含有Oneof
字段轉化爲json,是能夠正常轉化的,字段名爲被賦值的oneof字段的名稱。 eclipse
而對於Any的處理,則會比較特別。若是直接轉化,會獲得相似以下的異常,沒法找到typeUrl指定的類型。ide
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.
上面的實現沒法處理得了 Any
類型的數據。須要本身添加 TypeRegirstry
才能進行轉化。
@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,這必然致使整個代碼出現不少重複。
爲了解決這個問題,我嘗試直接從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) { 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; } ... }
從上面的代碼中,咱們能夠很容易地看出,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.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完成的轉化方法。來自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.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)); } }