Protobuf 3.0 任意类型的打包/解包

25

我想知道如何将 Protobuf 的 Any 类型转换为原始的 Protobuf 消息类型,反之亦然。在 Java 中,从消息到 Any 很容易:

Any.Builder anyBuilder = Any.newBuilder().mergeFrom(protoMess.build());

但是我该如何将那个Any解析回原始消息(例如“protoMess”类型)?我可以尝试在流上解析所有内容,然后再读取它们,但这不是我的想法。我希望进行某些转换,就像这样:

ProtoMess.MessData.Builder protoMessBuilder = (ProtoMess.MessData.Builder) transformToMessageBuilder(anyBuilder)

我该怎么做才能实现呢? Java已经实现了吗? Protobuf Language Guide中提到了pack和unpack方法,但Java中没有这些方法。

谢谢您的帮助 :)

4个回答

35
答案可能有点晚,但是也许仍然对某些人有帮助。
在当前版本的 Protocol Buffers 3 中,packunpack 在 Java 中是可用的
在您的示例中,打包可以像这样完成:
Any anyMessage = Any.pack(protoMess.build()));

并且像这样解包:

ProtoMess protoMess = anyMessage.unpack(ProtoMess.class);

以下是一个处理嵌套 Any 消息的 Protocol Buffers 消息的完整示例:

ProtocolBuffers 文件

一个包含嵌套 Any 消息的简单 Protocol Buffers 文件可能如下所示:

syntax = "proto3";

import "google/protobuf/any.proto";

message ParentMessage {
  string text = 1;
  google.protobuf.Any childMessage = 2;
}

一个可能的嵌套信息如下:

syntax = "proto3";

message ChildMessage {
  string text = 1;
}

打包

为了构建完整的消息,可以使用以下函数:

public ParentMessage createMessage() {
    // Create child message
    ChildMessage.Builder childMessageBuilder = ChildMessage.newBuilder();
    childMessageBuilder.setText("Child Text");
    // Create parent message
    ParentMessage.Builder parentMessageBuilder = ParentMessage.newBuilder();
    parentMessageBuilder.setText("Parent Text");
    parentMessageBuilder.setChildMessage(Any.pack(childMessageBuilder.build()));
    // Return message
    return parentMessageBuilder.build();
}

解包

要从父消息中读取子消息,可以使用以下函数:

public ChildMessage readChildMessage(ParentMessage parentMessage) {
    try {
        return parentMessage.getChildMessage().unpack(ChildMessage.class);
    } catch (InvalidProtocolBufferException e) {
        e.printStackTrace();
        return null;
    }
}

编辑:

如果你的打包消息可以有不同的类型,你可以读取typeUrl并使用反射来解包消息。假设你有子消息ChildMessage1ChildMessage2,你可以这样做:

@SuppressWarnings("unchecked")
public Message readChildMessage(ParentMessage parentMessage) {
    try {
        Any childMessage = parentMessage.getChildMessage();
        String clazzName = childMessage.getTypeUrl().split("/")[1];
        String clazzPackage = String.format("package.%s", clazzName);
        Class<Message> clazz = (Class<Message>) Class.forName(clazzPackage);
        return childMessage.unpack(clazz);
    } catch (ClassNotFoundException | InvalidProtocolBufferException e) {
        e.printStackTrace();
        return null;
    }
}

为了进一步处理,您可以使用instanceof确定消息的类型,但这并不是非常高效的。如果您想获取特定类型的消息,则应直接比较typeUrl

public ChildMessage1 readChildMessage(ParentMessage parentMessage) {
    try {
        Any childMessage = parentMessage.getChildMessage();
        String clazzName = childMessage.getTypeUrl().split("/")[1];
        if (clazzName.equals("ChildMessage1")) {
            return childMessage.unpack("ChildMessage1.class");
        }
        return null
    } catch (InvalidProtocolBufferException e) {
        e.printStackTrace();
        return null;
    }
}

2
除了readChildMessage,没有其他方法吗?如果可能会有数十种不同的消息,该怎么办?只需添加新的try-catch块吗?即便使用switch-case等也绝对不可接受。 - user4063815
好问题,我忘记了你可以通过typeURL获取打包消息的名称。这允许通过反射解包任何消息或直接决定如何处理消息。我在我的答案中添加了两个示例,希望这有所帮助。 - sundance
太好了!它帮助了我! - Fisher Coder
childMessage.getTypeUrl().split("/")[1]; 不会返回Java包名,而是返回Proto包名,因此无法正常工作。 - Aditya Joshee
@AdityaJoshee 没错。理想情况下,我想要获取类的规范名称,但你只能得到proto文件的包名。这真的很烦人。 - Capitano Giovarco
我的问题是我有成百上千种可能的类型,而我不能为每个类型编写像(X == Y)这样的条件。 - Capitano Giovarco

2

如果有人遇到同样的问题,我想补充一些信息...当下在解压时需要执行以下操作(使用c# .netcore 3.1 和Google.Protobuf 3.11.4)

Foo myobject = anyMessage.Unpack<Foo>();

1
我知道这个问题很老,但当我在寻找答案时它仍然出现了。使用@sundance的答案,我必须稍微不同地回答这个问题。问题在于实际消息是实际类的子类。因此,它需要一个$符号。
    for(Any x : in.getDetailsList()){
            try{
                String clazzName = x.getTypeUrl().split("/")[1];
                String[] split_name = clazzName.split("\\.");
                String nameClass = String.join(".", Arrays.copyOfRange(split_name, 0, split_name.length - 1)) + "$" + split_name[split_name.length-1];
                Class<Message> clazz = (Class<Message>) Class.forName(nameClass);

                System.out.println(x.unpack(clazz));

            } catch (Exception e){
                e.printStackTrace();
            }
        } 

这是我的原型消息的定义。

    syntax = "proto3";
    package cb_grpc.msg.Main;

    service QueryService {
        rpc anyService (AnyID) returns (QueryResponse) {}
    }

    enum Buckets {
        main = 0;
        txn = 1;
        hxn = 2;
       }

    message QueryResponse{
        string content = 1;
        string code = 2;
    }

    message AnyID {
        Buckets bucket = 1;
        string docID = 2;
        repeated google.protobuf.Any details = 3;
    }

and


    syntax = "proto3";
    package org.querc.cb_grpc.msg.database;

    option java_package = "org.querc.cb_grpc.msg";
    option java_outer_classname = "database";

    message TxnLog {
        string doc_id = 1;
        repeated string changes = 2;
    } 


0

这是我在Android Java项目中作为打包功能所做的事情:

String name = message.getClass().getSimpleName();
String packageName = message.getClass().getPackage().getName();   //needs to be the same as your proto package
String fullName = packageName + '.'+ name;
String typeURL = GetTypeUrl(fullName);
    
Any any = Any.newBuilder()
            .setValue(message.toByteString())
            .setTypeUrl(typeURL)
            .build();

private String GetTypeUrl(String fullName) {
    String prefix = "type.googleapis.com";
   return prefix + "/" + fullName;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接