java版本的Protobuf反序列化實現--支持Message、Enum和基本類型的嵌套結構

有很多人給我私信要具體的實現,在加上之前的版本有一點過時了,於是重新寫了一版本。網上很多人給的方案用了protobuf-java-format,這個包是有坑的,在解析json會導致如果字段值是默認值,字段就會消失。參見最後的附錄瞭解這個問題的原因和解決方案。                                                                                                                                                                  2020-05-12

如果你的數據上游使用protobuf 做了數據序列化,那麼可能就需要你對protobuf做反序列化。無論上游數據源是日誌文件還是kafka,最終我們要處理的數據是一行行有規則的數據。一般每一行都會對應多個protobuf文件,有的文件定義通用字段,有的文件定義特殊字段。本文實現了java版本的Protobuf反序列化,但不涉及業務相關的數據結構,專注於如何根據已知的類名將數據反序列化成protobuf的Message對象。在實現過程中遇到和解決的問題:

  • 支持不同的字段類型:Message、Enum、基本數據類型
  • 支持各種數據類型嵌套和Repeated
  • 支持各種括號中分隔符的轉換
  • 解決字段值是默認值導致解析結果字段消失問題
  • 解決引用其它文件Message導致的ClassNotFoundException(新增)
    Message actionMessage = getMessageByClass(actionClassName, actionRaw, false);

    /**
     * @param className 要轉換的PB類名;
     * @param rawData 要解析的數據;
     * @param isChildField 是否是嵌套Message
     * @return 反序列化的Message結果
     * */
    public static Message getMessageByClass(String className, String rawData, boolean isChildField) throws Exception {
        ……………………
    }

現在來解釋一下getMessageByClass()函數的各個參數的作用

  • rawData: protobuf序列化後的數據,也就是我們要解析的數據
  • className: 格式爲 package_name.class_name$inner_class_name 比如protoLog.UserAction$AddCart, 其中inner_class_name是rawData真正對應的pb Message對象
  • isChildField:是否是嵌套Message。 2|3|{5;7}|8 這樣的整條數據就是非嵌套Message,其中的{5;7}屬於嵌套Message,在解析的時候轉換爲 5|7

下面是具體實現,各部分功能都加上了註釋。

    public static Message getMessageByClass(String className, String rawData, boolean isChildField) throws Exception {
        Class cl = Class.forName(className);
        // newBuilder 爲靜態變量,即使沒有 message 的具體實例也可以 invoke!
        Method method = cl.getMethod("newBuilder");
        Object obj = method.invoke(null, null);
        Message.Builder msgBuilder = (Message.Builder)obj;
        Descriptors.Descriptor descriptor = msgBuilder.getDescriptorForType();
        String[] dataFields;
        if(rawData.isEmpty())
            return null;
        if(isChildField) {
            rawData = convertFieldSeparator(rawData, "braces", ';', '|');
        }
        //拆分字段,各字段由"\\|"分隔
        dataFields = rawData.split(ConstantHelper.REG_VERTICAL_BAR_MARK, -1);

        //校驗數據字段和描述符中是否一致,不支持比描述符中字段多的情形
        if(dataFields.length != descriptor.getFields().size()) {
            log.warn(className + " fields number not consistent! Raw data {" + rawData + "} fields num: " + dataFields.length
                    + "  Descriptor fields num: " + descriptor.getFields().size());

            if(dataFields.length > descriptor.getFields().size()) {
                System.out.println(className + " fields number not consistent! Raw data {" + rawData + "} fields num: " + dataFields.length
                        + "  Descriptor fields num: " + descriptor.getFields().size());
                return null;
            }
        }
        //處理每個字段
        for (int i = 0; i < dataFields.length; i++) {
            String fieldValue = dataFields[i];
            Descriptors.FieldDescriptor fieldDescriptor = descriptor.getFields().get(i);

            if(fieldDescriptor == null) {
                log.info("findFieldByNumber 結果爲NULL");
                continue;
            }

            boolean isRepeated = fieldDescriptor.isRepeated();
            if (isRepeated) {
                //如果字段是Repeat類型,先轉換分隔符,然後轉換成數組,最後根據類型取值
                if(fieldValue.length() > 2) {
                    String fieldValueRepeated = convertFieldSeparator(fieldValue, "brackets", ',', '|');
                    String[] valueArray = fieldValueRepeated.split(ConstantHelper.REG_VERTICAL_BAR_MARK, -1);
                    List<Object> repeatedFields = new ArrayList<>();
                    for(String value : valueArray) {
                        Object jsonValue = getFieldJsonVal(fieldDescriptor, className, value);
                        if (jsonValue == null) {
                            continue;
                        }
                        msgBuilder.addRepeatedField(fieldDescriptor, jsonValue);
                    }

                }
            } else {
                //根據類型取值
                Object jsonValue = getFieldJsonVal(fieldDescriptor, className, fieldValue);
                if (jsonValue == null) {
                    continue;
                }
                msgBuilder.setField(fieldDescriptor, jsonValue);
            }
        }
        Message msg = msgBuilder.build();

        return msg;
    }

這裏有兩個重要的函數,轉換分隔符的convertFieldSeparator和根據數據類型獲取值的getFieldJsonVal。

 /*
    * 根據括號類型對解析數據,strip外層括號,並將原來的字段分隔符轉換爲"\\|"
    * @param raw {1;2;3;{4;5;6;7};{8;7}}
    * @param bracketsType {}
    * @return 1|2|3|{4;5;6;7}|{8;7}
    *
    * @param raw [{1;2},{4;5},{6;7}]
    * @param bracketsType []
    * @return {1;2}|{4;5}|{6;7}
    * */
    public static String convertFieldSeparator(String raw, String bracketsType, char src, char des){
        if(raw.length()<=2) {
            return "";
        }

        char preBracket;
        char postBracket;
        switch (bracketsType) {
            case "parentheses":
                preBracket = '(';
                postBracket = ')';
                break;
            case "brackets":
                preBracket = '[';
                postBracket = ']';
                break;
            case "braces":
                preBracket = '{';
                postBracket = '}';
                break;
            default:
                return raw;
        }

        char[] strippedRaw = raw.substring(1, raw.length()-1).toCharArray();
        int nested = 0;
        for (int i = 0; i < strippedRaw.length; i++) {
            if(strippedRaw[i] == preBracket)
                nested++;
            if(strippedRaw[i] == postBracket)
                nested--;
            if(nested ==0 && strippedRaw[i] == src) {
                strippedRaw[i] = des;
            }
        }
        return new String(strippedRaw);
    }
    /**
     * @param fieldDescriptor 字段描述符;
     * @param className 字段父類名;
     * @param fieldValue 字段String類型值
     * @return Object類型的字段值
     * */
    public static Object getFieldJsonVal(Descriptors.FieldDescriptor fieldDescriptor, String className, String fieldValue) throws Exception {
        Object fieldVal;
        Descriptors.FieldDescriptor.JavaType type = fieldDescriptor.getJavaType();
        if(type.toString().equals("MESSAGE")){
            // MESSAGE類型
            String typeName = fieldDescriptor.toProto().getTypeName();
            String[] typeArr = typeName.split("\\.", -1);
            String classNamePrefix = className.split("\\$")[0];
            String fieldClassName = classNamePrefix + ConstantHelper.DOLLER_MARK + typeArr[typeArr.length - 1];
            // 遞歸調用getMessageByClass方法獲取Message數據
            try{
                fieldVal = getMessageByClass(fieldClassName, fieldValue, true);
            } catch (ClassNotFoundException e){
                //解決引用其它文件Message導致的ClassNotFoundException
                //如果這個Message是從其它文件引入的,那麼需要通過文件依賴獲取Message全路徑類名
                List<Descriptors.FileDescriptor> fileDescriptorList =  fieldDescriptor.getFile().getDependencies();
                if(fileDescriptorList.size() > 0) {
                    for(Descriptors.FileDescriptor fileDescriptor : fileDescriptorList){
                        String dependName = fileDescriptor.toProto().getName();
                        String dependClassName = lineToHump(dependName.split("\\.")[0]);
                        fieldClassName = typeArr[1] + "." + dependClassName + "$" + typeArr[2];
                        fieldVal = getMessageByClass(fieldClassName, fieldValue, true);
                    }
                } else {
                    throw e;
                }

            }
        } else if(type.toString().equals("ENUM")){
            // ENUM類型
			fieldVal = fieldDescriptor.getEnumType().findValueByNumber(Integer.valueOf(fieldValue));
		} else {
            // 基本類型
            fieldVal = getObject(fieldValue, type);
        }
        return fieldVal;
    }


    private static Object getObject(String rawString, Descriptors.FieldDescriptor.JavaType type) {
        try {
            switch (type) {
                case INT:
                    return rawString.equals("") ? 0 : Integer.valueOf(rawString);
                case LONG:
                    return rawString.equals("") ? 0 : Long.valueOf(rawString);
                case FLOAT:
                    return rawString.equals("") ? 0 : Float.valueOf(rawString);
                case DOUBLE:
                    return rawString.equals("") ? 0 : Double.valueOf(rawString);
                case BOOLEAN:
                    return rawString.equals("") ? false : Boolean.valueOf(rawString);
                case STRING:
                    return rawString;
                default:
                    // BYTE_STRING, ENUM, MESSAGE
                    return null;
            }
        } catch (Exception e) {
            log.error( e.getMessage(), e);
            e.printStackTrace();
        }
        return null;
    }

在得到Messge類型數據後,我們就可以開心的將其轉換成json了,注意,這裏的JsonFormat屬於protobuf-java-util包!

    public String MessageToJsonString(Message message) throws Exception {
        String jsonStr = "";
        if(message != null) {
            jsonStr = JsonFormat.printer().includingDefaultValueFields().preservingProtoFieldNames().print(message);
            jsonStr = JSON.parseObject(jsonStr).toJSONString();
        }
        return jsonStr;
    }
<!--protobuf-->
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.7.1</version>
</dependency>
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java-util</artifactId>
    <version>3.6.1</version>
</dependency>

附錄:protobuf-java-format包的坑,貌似這個包已經不維護了

使用了protobuf-java-format包將message對象轉換成json串。但最後發現轉換結果中值爲0的字段全都不見了,排查了很久發現是protobuf-java包中的Message.getAllFields()方法不會返回與默認值相等的字段。

因此,調用Message.getAllFields()方法是無法返回所有字段的

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章