写点什么

实现自己的 Protobuf Any

用户头像
DoneSpeak
关注
发布于: 1 小时前
实现自己的Protobuf Any

前言

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


@Datapublic 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 <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.protomessage 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); }}
复制代码


这里添加一个单元测试,以提供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<? extends Message> anyDataMessage = messageTypeLookup.lookup(AnyDataProto.AnyData.getDescriptor().getFullName()); // AnyDataProto 在包下 assertNotNull(anyDataMessage); assertTrue(AnyDataProto.AnyData.class.equals(anyDataMessage));
Class<? extends Message> 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<? extends Message> anyDataMessage = messageTypeLookup.lookup(AnyDataProto.AnyData.getDescriptor().getFullName()); // AnyDataProto 在 io.gitlab.donespeak.proto.common 下 assertNotNull(anyDataMessage); assertTrue(AnyDataProto.AnyData.class.equals(anyDataMessage));
Class<? extends Message> 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<? extends Message> anyDataMessage = messageTypeLookup.lookup(AnyDataProto.AnyData.getDescriptor().getFullName()); // AnyDataProto 与 ApiResultProto 同包 assertNotNull(anyDataMessage); assertTrue(AnyDataProto.AnyData.class.equals(anyDataMessage));
Class<? extends Message> studentMessage = messageTypeLookup.lookup(StudentProto.Student.getDescriptor().getFullName()); // StudentProto 与 DataTypeProto 同包 assertNotNull(studentMessage); assertTrue(StudentProto.Student.class.equals(studentMessage)); }}
复制代码

参考

相关文章

发布于: 1 小时前阅读数: 2
用户头像

DoneSpeak

关注

Let the Work That I've Done Speak for Me 2018.05.10 加入

Java后端开发

评论

发布
暂无评论
实现自己的Protobuf Any