当前位置 博文首页 > 文章内容

    Protobuf与Json的相互转化

    作者: 栏目:未分类 时间:2020-08-04 18:01:45

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    前言

    最近的工作中开始使用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;
    }

    可选择的工具

    可以将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类型的字符串。

    Any 以及 Oneof

    Any 和 Oneof 是protobuf中比较特别的两个类型,如果尝试将含有Oneof字段转化为json,是可以正常转化的,字段名为被赋值的oneof字段的名称。

    而对于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.

    Class JsonFormat.TypeRegistry @JavaDoc

    上面的实现无法处理得了 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进行实现

    在查找资料的过程中,还发现了一种通过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));
        }
    }
    参考