实现自己的Protobuf Any

前言

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);
    }
}

参考

前言

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);
    }
}

参考

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript中使用Protobuf的Any类型需要进行一些特定的操作。Any类型允许在消息中存储任意类型的值,但在JavaScript中没有原生支持。下面是一种实现Any类型的方法: 1. 首先,确保你已经安装了Googleprotobuf库。可以使用npm命令来安装: ```shell npm install google-protobuf ``` 2. 在你的JavaScript文件中引入所需的protobuf库: ```javascript const protobuf = require("google-protobuf"); ``` 3. 创建一个新的Any类型对象并设置其值: ```javascript const anyValue = new protobuf.Any(); anyValue.pack(MessageType, message); ``` 在上述代码中,`MessageType` 是你要存储的消息类型,`message` 是一个具体的消息实例。通过调用`pack()`方法,将消息类型和实例打包到Any对象中。 4. 将Any对象序列化为字节流: ```javascript const bytes = anyValue.serializeBinary(); ``` 通过调用`serializeBinary()`方法,将Any对象转换为字节流。 5. 反序列化字节流为Any对象: ```javascript const anyValue = new protobuf.Any(); anyValue.deserializeBinary(bytes); ``` 通过调用`deserializeBinary()`方法,将字节流转换为Any对象。 6. 获取Any对象中存储的消息类型和实例: ```javascript const messageType = anyValue.getTypeName(); const message = anyValue.unpack(MessageType); ``` 通过调用`getTypeName()`方法,获取消息类型的名称。通过调用`unpack()`方法,将Any对象解包为具体的消息实例。 这是一个简单的示例,用于在JavaScript中使用Protobuf的Any类型。请根据你的实际需求进行相应的调整和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值