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