實現本身的Protobuf Any

前言

在一種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;
}

Protobuf的any: google.protobuf.Any

google.protobuf.Any 也是由 proto 文件定義的

去掉全部的註釋,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

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_app

typeUrl_ 保存的值爲 Message類的描述類型,原proto文件的message帶上package的值,如any的typeUrl爲type.googleapis.com/google.protobuf.Anyvalue_ 爲 保存到Any對象中的Message對象的ByteString,經過調用方法toByteString()獲得。知道這些信息以後,就能夠本身從新定一個了。工具

自定義AnyData

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 的編碼和解析

自定義的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));
    }
}

參考

相關文章

相關文章
相關標籤/搜索