java版本的Protobuf反序列化实现--支持Message、Enum和基本类型的嵌套结构

本文重新编写了Java版本的Protobuf反序列化实现,不涉及业务数据结构,专注将数据反序列化为Message对象。实现中解决了支持不同字段类型、嵌套、分隔符转换等问题,还解决了字段值为默认值解析结果字段消失及ClassNotFoundException问题,最后指出protobuf-java-format包的坑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有很多人给我私信要具体的实现,在加上之前的版本有一点过时了,于是重新写了一版本。网上很多人给的方案用了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()方法是无法返回所有字段的

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值