实现自己的Protobuf Any

前言

在一种API的设计中,有如下的设计,这也是网上经常看到的。

@Data
public class ApiResult {
    private int code;
    private String error;
    private Object data;
}

如果要等价替换的话,可以有如下的设计:

message ApiResult {
    int32 code = 1;
    string error = 2;
    google.protobuf.Any data = 3;
}

google.protobuf.Any 可以理解为Java中的Object,但又和Object有所不同。Any不是所有的Message的父类,而Object是所有类的父类。在某些情况下使用的并不是那么方便,希望有更加方便的设计。从protobuf的源码中,我们很容易地知道,google.protobuf.Any 也是一个 proto 的类罢了,完全可以用自己定义的proto类进行替代。

我们自定义一个donespeak.protobuf.AnyData,则可以有如下的结构:

message ApiResult {
    int32 code = 1;
    string error = 2;
    donespeak.protobuf.AnyData data = 3;
}

Protobuf的any: google.protobuf.Any

google.protobuf.Any 也是由 proto 文件定义的

去掉所有的注释,google/protobuf/any.proto 也就只有如下的内容,完全可以自定义一个。

syntax = "proto3";

package google.protobuf;

option csharp_namespace = "Google.Protobuf.WellKnownTypes";
option go_package = "github.com/golang/protobuf/ptypes/any";
option java_package = "com.google.protobuf";
option java_outer_classname = "AnyProto";
option java_multiple_files = true;
option objc_class_prefix = "GPB";

message Any {
    string type_url = 1;
    bytes value = 2;
}

any.proto 编译之后可以得到一个Message类,而 protobuf 还为any添加了一些必要的方法。我们可以从下面的,any.proto 编译出来的类的源码中可以看出 Any.java 与 其他的Message类有什么不同。

google.protobuf.Any 本身也是一个 GeneratedMessageV3

简单地讲一下Any,Any的源码不是很多,删除GeneratedMessageV3Builder相关的代码,大概还有如下代码:

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  Any pack(T message) {
        return Any.newBuilder()
            .setTypeUrl(getTypeUrl("type.googleapis.com",
                                message.getDescriptorForType()))
            .setValue(message.toByteString())
            .build();
    }

    public static  Any pack(T message, String typeUrlPrefix) {
        return Any.newBuilder()
            .setTypeUrl(getTypeUrl(typeUrlPrefix,
                                message.getDescriptorForType()))
            .setValue(message.toByteString())
            .build();
    }

    public  boolean is(Class 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 unpack(Class 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 有两个字段:typeUrl_value_

typeUrl_ 保存的值为 Message类的描述类型,原proto文件的message带上package的值,如any的typeUrl为type.googleapis.com/google.protobuf.Anyvalue_ 为 保存到Any对象中的Message对象的ByteString,通过调用方法toByteString()得到。知道这些信息之后,就可以自己重新定一个了。

自定义AnyData

common/any_data.proto

syntax = "proto3";

package donespeak.protobuf;

option java_package = "io.gitlab.donespeak.proto.common";
option java_outer_classname = "AnyDataProto";

// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto
message AnyData {
    // 值为 .,如 api.donespeak.cn/data.proto.DataTypeProto
    string type_url = 1;
    // 值为 message.toByteString();
    bytes value = 2;
}

AnyData 的编码和解析

自定义的AnyData只是一个普通的Message类,需要另外实现一个Pack和Unpack的工具类。

package io.gitlab.donespeak.javatool.toolprotobuf.anydata;

import com.google.protobuf.Descriptors;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.gitlab.donespeak.proto.common.AnyDataProto;

public class AnyDataPacker {
    private static final String COMPANY_TYPE_URL_PREFIX = "type.donespeakapi.cn";

    private final AnyDataProto.AnyData anyData;

    public AnyDataPacker(AnyDataProto.AnyData anyData) {
        this.anyData = anyData;
    }

    public static  AnyDataProto.AnyData pack(T message) {
        final String typeUrl = getTypeUrl(message.getDescriptorForType());

        return AnyDataProto.AnyData.newBuilder()
            .setTypeUrl(typeUrl)
            .setValue(message.toByteString())
            .build();
    }

    public static  AnyDataProto.AnyData pack(T message, String typeUrlPrefix) {
        String typeUrl = getTypeUrl(typeUrlPrefix, message.getDescriptorForType());

        return AnyDataProto.AnyData.newBuilder()
            .setTypeUrl(typeUrl)
            .setValue(message.toByteString())
            .build();
    }

    public  boolean is(Class clazz) {
        T defaultInstance = com.google.protobuf.Internal.getDefaultInstance(clazz);
        return getTypeNameFromTypeUrl(anyData.getTypeUrl()).equals(
            defaultInstance.getDescriptorForType().getFullName());
    }

    private static String getTypeNameFromTypeUrl(String typeUrl) {
        int pos = typeUrl.lastIndexOf('/');
        return pos == -1 ? "" : typeUrl.substring(pos + 1);
    }

    private volatile Message cachedUnpackValue;

    public  T unpack(Class 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(anyData.getValue());
        cachedUnpackValue = result;
        return result;
    }

    private static String getTypeUrl(final Descriptors.Descriptor descriptor) {
        return getTypeUrl(COMPANY_TYPE_URL_PREFIX, descriptor);
    }

    private static String getTypeUrl(String typeUrlPrefix, Descriptors.Descriptor descriptor) {
        return typeUrlPrefix.endsWith("/")
            ? typeUrlPrefix + descriptor.getFullName()
            : typeUrlPrefix + "/" + descriptor.getFullName();
    }
}

很容易可以看出,这个类和google.protobuf.Any中的实现基本是一样的。是的,这个类其实就是直接从Any类中抽取出来的。你也可以将unpack方式设计成static的,这样的话,这个工具类就是一个完全的静态工具类了。而这里保留原来的实现是为了在unpack的时候可以做一个缓存。因为Message类都是不变类,所以这样的策略对于多次unpack会很管用。

定义一个将typeUrl和Class映射的lookup工具类

按照前面的描述,这里独立提供一个解包工具,提供更多的解包方法。该工具类有一个静态的解包方法,无需实例化直接调用。另一个方法则需要借助MessageTypeLookup类。MessageTypeLookup类是一个注册类,保存类Message的Descriptor和Class的映射关系。该类的存在,允许了将所有可能的Message类进行注册,然后进行通用的解包,而无需再设法找到AnyData.value的数据对应的类。

MessageTypeUnpacker.java

package io.gitlab.donespeak.javatool.toolprotobuf.anydata;

import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.gitlab.donespeak.proto.common.AnyDataProto;

public class MessageTypeUnpacker {
    private final MessageTypeLookup messageTypeLookup;

    public MessageTypeUnpacker(MessageTypeLookup messageTypeLookup) {
        this.messageTypeLookup = messageTypeLookup;
    }

    public Message unpack(AnyDataProto.AnyData anyData) throws InvalidProtocolBufferException {
        AnyDataPacker anyDataPacker = new AnyDataPacker(anyData);
        Class messageClass = messageTypeLookup.lookup(anyData.getTypeUrl());
        return anyDataPacker.unpack(messageClass);
    }

    public static  T unpack(AnyDataProto.AnyData anyData, Class messageClass)
        throws InvalidProtocolBufferException {
        AnyDataPacker anyDataPacker = new AnyDataPacker(anyData);
        return anyDataPacker.unpack(messageClass);
    }
}

MessageTypeLookup 用于注册typeUrl和Message的Class的映射关系,以方便通过typeUrl查找相应的Class。

MessageTypeLookup.java

package io.gitlab.donespeak.javatool.toolprotobuf.anydata;

import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;

import java.util.HashMap;
import java.util.Map;

public class MessageTypeLookup {

    private final Map> TYPE_MESSAGE_CLASS_MAP;

    private MessageTypeLookup(Map> typeMessageClassMap) {
        this.TYPE_MESSAGE_CLASS_MAP = typeMessageClassMap;
    }

    public Class lookup(final String typeUrl) {
        String type = typeUrl;
        if(type.contains("/")) {
            type = getTypeUrlSuffix(type);
        }
        return TYPE_MESSAGE_CLASS_MAP.get(type);
    }

    public static Builder newBuilder() {
        return new Builder();
    }

    private static String getTypeUrlSuffix(String fullTypeUrl) {
        String[] parts = fullTypeUrl.split("/");
        return parts[parts.length - 1];
    }

    public static class Builder {

        private final Map> TYPE_MESSAGE_CLASS_BUILDER_MAP;

        public Builder() {
            TYPE_MESSAGE_CLASS_BUILDER_MAP = new HashMap<>();
        }

        public Builder addMessageTypeMapping(final Descriptors.Descriptor descriptor,
            final Class messageClass) {
            TYPE_MESSAGE_CLASS_BUILDER_MAP.put(descriptor.getFullName(), messageClass);
            return this;
        }

        public MessageTypeLookup build() {
            return new MessageTypeLookup(TYPE_MESSAGE_CLASS_BUILDER_MAP);
        }
    }
}

有了MessageTypeLookup之后,可以将所有可能用到的Message都预先注册到这个类中,再借助该类进行解包这样基本就可以实现一个通用的AnyData的打包解包的实现了。但这个类的注册会非常的麻烦,需要手动将所有的Message都添加进来,费力而且容易出错,以后每次添加新的类还要进行添加,很麻烦。

查找指定路径下的类及其内部类

为了解决上面的MessageTypeLookup的不足,可以添加一个按照包的路径查找符合条件的类的方法。在开发中,一般会将所有的Proto都放在一个统一的包名下,所以只需要知道这个包名,然后扫描这个包下的所有类,找到GeneratedMessageV3的子类。再将得到的结果注册到MessageTypeLookup即可。这样实现之后,即使添加新的Message类,也不需要手动添加到MessageTypeLookup中也可以自动实现注册了。

找到一个包下的所有类

为了实现找到一个包下的所有类,这借助了Reflection库,该库提供了很多有用的反射方法。如果想要自己实现一个这样的反射方法,其实挺麻烦的,而且还会有很多坑。之后有时间再进一步讲解反射和类的加载相关的内容吧,感觉会很有趣。

这部分的灵感是来自于Spring@ComponentScan注解。类似的,这里提供了两种扫描方式,一个是包名前缀,另一是指定类所在的包作为扫描的包。这两种方式均允许提供多个路径。



    org.reflections
    reflections
    0.9.11

ClassScanner.java

package io.gitlab.donespeak.javatool.toolprotobuf.anydata;

import java.util.Set;
import com.google.protobuf.GeneratedMessageV3;
import org.reflections.Reflections;

public class ClassScanner {

    public static  Set> lookupClasses(Class subType, String... basePackages) {
        Reflections reflections = new Reflections(basePackages);
        return reflections.getSubTypesOf(subType);
    }

    public static  Set> lookupClasses(Class subType, Class... basePackageClasses) {

        String[] basePackages = new String[basePackageClasses.length];
        for(int i = 0; i < basePackageClasses.length; i ++) {
            basePackages[i] = basePackageClasses[i].getPackage().getName();
        }
        return lookupClasses(subType, basePackages);
    }
}

将一个包下的GeneratedMessageV3的子类注册到MessageTypeLookup中

当我们有了类的扫描工具类之后,“将一个包下的GeneratedMessageV3的子类注册到MessageTypeLookup中”的需求就变得非常容易了。

有了ClassScanner,我们可以得到所有的GeneratedMessageV3类的类对象,还需要获取typeUrl。因为 Message#getDescriptorForType() 方式是一个对象的方法,所以在得到所需要的类的类对象之后需要用反射的方法得到一个实例,再调用getDescriptorForType()方法以获取typeUrl。又知道Message类都是不可变类,而且所有的构造方法都是私有的,因而只能通过Builder类创建。这里先通过反射调用静态方法Message#newBuilder()创建一个Builder,再通过Builder得到Message实例。到这里,所有需要的工作都完成了。

MessageTypeLookupUtil.java

package io.gitlab.donespeak.javatool.toolprotobuf.anydata;

import com.google.protobuf.GeneratedMessageV3;
import com.google.protobuf.Message;

import java.lang.reflect.InvocationTargetException;
import java.util.Set;

public class MessageTypeLookupUtil {

    public static MessageTypeLookup getMessageTypeLookup(String... messageBasePackages) {

        // 这里使用 GeneratedMessageV3作为父类查找,防止类似com.google.protobuf.AbstractMessage的类出现
        Set>
            klasses = ClassScanner.lookupClasses(GeneratedMessageV3.class, messageBasePackages);

        return generateMessageTypeLookup(klasses);
    }

    private static MessageTypeLookup generateMessageTypeLookup(Set> klasses) {
        MessageTypeLookup.Builder messageTypeLookupBuilder = MessageTypeLookup.newBuilder();
        try {
            for (Class klass : klasses) {
                Message.Builder builder = (Message.Builder)klass.getMethod("newBuilder").invoke(null);
                Message messageV3 = builder.build();
                messageTypeLookupBuilder.addMessageTypeMapping(messageV3.getDescriptorForType(), klass);
            }
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            // will never happen
            throw new RuntimeException(e.getMessage(), e);
        }
        return messageTypeLookupBuilder.build();
    }

    public static MessageTypeLookup getMessageTypeLookup(Class... messageBasePackageClasses) {

        // 这里使用 GeneratedMessageV3作为父类查找,防止类似com.google.protobuf.AbstractMessage的类出现
        Set>
            klasses = ClassScanner.lookupClasses(GeneratedMessageV3.class, messageBasePackageClasses);
        return generateMessageTypeLookup(klasses);
    }
}

这里添加一个单元测试,以提供MessageTypeLookupUtil类的使用方法。

这里增加一个多个不同的proto类,生成的代码位置大概如下,其中的$表示内部类。

io.gitlab.donespeak.proto.common
    .AnyDataProto.class$AnyData.class
    .ApiResultProto.class$ApiResult.class

io.gitlab.donespeak.javatool.toolprotobuf.proto
    .DataTypeProto.class$BaseData.class
    .StudentProto.class$Student.class

测试类实现:MessageTypeLookupUtilTest.java

package io.gitlab.donespeak.javatool.toolprotobuf.anydata;

import com.google.protobuf.Message;
import io.gitlab.donespeak.javatool.toolprotobuf.proto.DataTypeProto;
import io.gitlab.donespeak.javatool.toolprotobuf.proto.StudentProto;
import io.gitlab.donespeak.proto.common.AnyDataProto;
import io.gitlab.donespeak.proto.common.ApiResultProto;
import org.junit.Test;

import static org.junit.Assert.*;

public class MessageTypeLookupUtilTest {

    @Test
    public void getMessageTypeLookup1() {
        MessageTypeLookup messageTypeLookup = MessageTypeLookupUtil.getMessageTypeLookup(
            "io.gitlab.donespeak.proto.common");

        Class anyDataMessage =
            messageTypeLookup.lookup(AnyDataProto.AnyData.getDescriptor().getFullName());
        // AnyDataProto 在包下
        assertNotNull(anyDataMessage);
        assertTrue(AnyDataProto.AnyData.class.equals(anyDataMessage));

        Class studentMessage =
            messageTypeLookup.lookup(StudentProto.Student.getDescriptor().getFullName());
        // StudentProto 不在指定包下
        assertNull(studentMessage);
    }

    @Test
    public void getMessageTypeLookup2() {
        MessageTypeLookup messageTypeLookup = MessageTypeLookupUtil.getMessageTypeLookup(
            "io.gitlab.donespeak.proto.common", "io.gitlab.donespeak.javatool.toolprotobuf.proto");

        Class anyDataMessage =
            messageTypeLookup.lookup(AnyDataProto.AnyData.getDescriptor().getFullName());
        // AnyDataProto 在 io.gitlab.donespeak.proto.common 下
        assertNotNull(anyDataMessage);
        assertTrue(AnyDataProto.AnyData.class.equals(anyDataMessage));

        Class studentMessage =
            messageTypeLookup.lookup(StudentProto.Student.getDescriptor().getFullName());
        // StudentProto 在 io.gitlab.donespeak.javatool.toolprotobuf.proto 下
        assertNotNull(studentMessage);
        assertTrue(StudentProto.Student.class.equals(studentMessage));
    }

    @Test
    public void getMessageTypeLookup3() {
        MessageTypeLookup messageTypeLookup =
            MessageTypeLookupUtil.getMessageTypeLookup(ApiResultProto.ApiResult.class, DataTypeProto.BaseData.class);

        Class anyDataMessage =
            messageTypeLookup.lookup(AnyDataProto.AnyData.getDescriptor().getFullName());
        // AnyDataProto 与 ApiResultProto 同包
        assertNotNull(anyDataMessage);
        assertTrue(AnyDataProto.AnyData.class.equals(anyDataMessage));

        Class studentMessage =
            messageTypeLookup.lookup(StudentProto.Student.getDescriptor().getFullName());
        // StudentProto 与 DataTypeProto 同包
        assertNotNull(studentMessage);
        assertTrue(StudentProto.Student.class.equals(studentMessage));
    }
}

参考

  • protocolbuffers/protobuf/src/google/protobuf/any.proto @Github
  • ronmamo/reflections @Github
  • ronmamo/reflections#UseCases.md @Github
  • Protocol Buffers, Part 3 — JSON Format @codeburst

相关文章

  • Protobuf与POJO的相互转化 - 通过Json
  • Protobuf与Json的相互转化

你可能感兴趣的:(protobuf,java,protocol-buffer)