在一种API的设计中,有以下的设计,这也是网上常常看到的。java
@Data public class ApiResult { private int code; private String error; private Object data; }
若是要等价替换的话,能够有以下的设计:git
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类进行替代。 github
咱们自定义一个donespeak.protobuf.AnyData
,则能够有以下的结构:golang
message ApiResult { int32 code = 1; string error = 2; donespeak.protobuf.AnyData data = 3; }
去掉全部的注释,google/protobuf/any.proto
也就只有以下的内容,彻底能够自定义一个。json
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类有什么不一样。api
简单地讲一下Any
,Any的源码不是不少,删除GeneratedMessageV3
和Builder
相关的代码,大概还有以下代码:缓存
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_
。 app
typeUrl_
保存的值为 Message类的描述类型,原proto文件的message带上package的值,如any的typeUrl为type.googleapis.com/google.protobuf.Any
。value_
为 保存到Any对象中的Message对象的ByteString,经过调用方法toByteString()
获得。知道这些信息以后,就能够本身从新定一个了。工具
common/any_data.protogitlab
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只是一个普通的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会很管用。
按照前面的描述,这里独立提供一个解包工具,提供更多的解包方法。该工具类有一个静态的解包方法,无需实例化直接调用。另外一个方法则须要借助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中”的需求就变得很是容易了。
有了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)); } }