30

实现自己的Protobuf Any

 4 years ago
source link: https://www.tuicool.com/articles/ZbuU7jm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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 <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 有两个字段: 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 {
    // 值为 <package>.<messageName>,如 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 <T extends com.google.protobuf.Message> AnyDataProto.AnyData pack(T message) {
        final String typeUrl = getTypeUrl(message.getDescriptorForType());

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

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

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

    public <T extends Message> boolean is(Class<T> 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 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(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<? extends Message> messageClass = messageTypeLookup.lookup(anyData.getTypeUrl());
        return anyDataPacker.unpack(messageClass);
    }

    public static <T extends Message> T unpack(AnyDataProto.AnyData anyData, Class<T> 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<String, Class<? extends Message>> TYPE_MESSAGE_CLASS_MAP;

    private MessageTypeLookup(Map<String, Class<? extends Message>> typeMessageClassMap) {
        this.TYPE_MESSAGE_CLASS_MAP = typeMessageClassMap;
    }

    public Class<? extends Message> 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<String, Class<? extends Message>> TYPE_MESSAGE_CLASS_BUILDER_MAP;

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

        public Builder addMessageTypeMapping(final Descriptors.Descriptor descriptor,
            final Class<? extends Message> 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 注解。类似的,这里提供了两种扫描方式,一个是包名前缀,另一是指定类所在的包作为扫描的包。这两种方式均允许提供多个路径。

<!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>

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 <T> Set<Class<? extends T>> lookupClasses(Class<T> subType, String... basePackages) {
        Reflections reflections = new Reflections(basePackages);
        return reflections.getSubTypesOf(subType);
    }

    public static <T> Set<Class<? extends T>> lookupClasses(Class<T> 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<Class<? extends GeneratedMessageV3>>
            klasses = ClassScanner.lookupClasses(GeneratedMessageV3.class, messageBasePackages);

        return generateMessageTypeLookup(klasses);
    }

    private static MessageTypeLookup generateMessageTypeLookup(Set<Class<? extends GeneratedMessageV3>> klasses) {
        MessageTypeLookup.Builder messageTypeLookupBuilder = MessageTypeLookup.newBuilder();
        try {
            for (Class<? extends GeneratedMessageV3> 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<Class<? extends GeneratedMessageV3>>
            klasses = ClassScanner.lookupClasses(GeneratedMessageV3.class, messageBasePackageClasses);
        return generateMessageTypeLookup(klasses);
    }
}

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK