乐趣区

实现自己的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);
    }
}

参考

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