有很多人給我私信要具體的實現,在加上之前的版本有一點過時了,於是重新寫了一版本。網上很多人給的方案用了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()方法是無法返回所有字段的