前言
JSON是常用的数据编码格式,在从海量JSON格式字符串数据中解析出所需值常常是计算的性能瓶颈,在大数据实时离线场景尤为常见。本文阐述一种高效解析JSON的方案和实现,相比较于jackson,在公司场景应用中,性能平均提升50%+。
基于JAVA的JSON处理源码参考github: https://github.com/lunar-ye/ProtoJson
参考测试用例:
// 用法JsonRowConverter构造方法为变长参数,传入要解析的多个路径
// 对下面例子,路径a返回:"1", 路径b.c返回 "xx", 路径b返回 {"c":"xx","d":[1,2,3],"e":[[1,2,3]]}
// 路径b.d返回[1,2,3],路径b.d.1返回2,路径b.e.0.0返回1
String json = "{\"a\":1,\"b\":{\"c\":\"xx\",\"d\":[1,2,3],\"e\":[[1,2,3]]}}";
JsonRowConverter converter = new JsonRowConverter("a","b","b.c", 'b.d.1');
String[] result = converter.process(json);
// 返回结果
// 1, {"c":"xx","d":[1,2,3],"e":[[1,2,3]]}, xx
基于该方案实现的HIVE JSON处理UDF源码:https://github.com/lunar-ye/ProtoJson/tree/kson_tuple
UDF用法参考:
-- kson_tuple用法类似于json_tuple,第一个参数为要解析的json字段,后面参数为要解析的多个路径
add jar viewfs://***.jar;
create temporary function kson_tuple as '***.video.KsonTuple';
select
custom_key,
custom_biz,
custom_value,
stat,
stat_status,
err_msg,
sdk_ver,
max_retry_cnt
FROM
table1 lateral view outer kson_tuple(
custom_value,
'stat',
'stat.status',
'stat.error_message',
'sdk_ver',
'config.max_retry_count'
) t as stat,stat_status, err_msg,sdk_ver,max_retry_cnt
where
p_date = '{{ ds_nodash }}'AND p_hourmin = '{{hourmin}}'AND custom_key in ('VIDEO_IMAGE')
jackson是一种开源主流的json解析工具,详情可以参考:https://github.com/FasterXML/jackson。
jackson常见有两种解析场景,一种为将json解析为JsonNode tree,另一种将json字符串解析为java类
ObjectMapper mapper = new ObjectMapper();
// 1. 解析成jsonnode tree
JsonNode node = mapper.readTree(json);
// 2. 解析成java对象
HashMap<String, String> map = (HashMap<String, String>) mapper.readValue(json, Map.class);
两种解析json的方法逻辑相似,可以分为下面几个部分。
首先,将json字符串进行词法解析,解析成JsonToken组合。
public JsonParser createParser(String content) throws IOException, JsonParseException {
int strLen = content.length();
if (this._inputDecorator == null && strLen <= 32768 && this.canUseCharArrays()) {
IOContext ctxt = this._createContext(this._createContentReference(content), true);
char[] buf = ctxt.allocTokenBuffer(strLen);
content.getChars(0, strLen, buf, 0);
return this._createParser(buf, 0, strLen, ctxt, true);
} else {
return this.createParser((Reader)(new StringReader(content)));
}
}
然后,将调用DefaultSerializationContext的readRootValue方法。
public Object readRootValue(JsonParser p, JavaType valueType,
JsonDeserializer<Object> deser, Object valueToUpdate)throws IOException
{
if (_config.useRootWrapping()) {
return _unwrapAndDeserialize(p, valueType, deser, valueToUpdate);
}
if (valueToUpdate == null) {
return deser.deserialize(p, this);
}
return deser.deserialize(p, this, valueToUpdate);
}
最终会递归调用JsonDeserialize方法进解析JsonToken组合。
public abstract T deserialize(JsonParser p, DeserializationContext ctxt)throws IOException, JacksonException;
jackson定义了一系列json反序列化类,不同的反序列化类会将json反序列化为不同的类型。比如MapDeserializer类会将jsontoken集合解析为Map类型,而JsonNode deserializer类会将jsontoken集合解析为JsonNode类型。
但是jackson提供的官方解析方法为了保证易用性(把全量json构建成一棵树,用户按需取),存在会将大量的无用字段递归解析,并且会在json每个路径节点创建不同的对象。
比如:对于json字符串:"{\"a\":1,\"b\":{\"c\":\"xx\",\"d\":[1,2,3],\"e\":[[1,2,3]]}}"
哪怕我们只想解析"a"这个字段的值,当调用jackson的官方解析方法时候(比如readTree),也会将b、b.c、b.d等等字段全部解析出来,并且每个节点构造jsonnode的对象。
针对jackson官方解析方案存在的两点问题,分别给出解决方案:
a. 无效字段解析:常见的数据清洗场景,用户需要的字段都是固定的,所以可以只解析需要的字段,不需要的字段可以快速跳过
b. 对象重复创建:将结果存储到节点树上,复用对象,不需要重复创建对象。
以示例JSON为例, a, b, b.c, b.d[1] 解析流程如下
{
"a": 1,
"b": {
"c": "xx",
"d": [1, 2, 3],
"e": [
[1, 2, 3]
]
}
}
a. 构造节点树,
b. 词法解析json字符串,生成JsonToken集合。
c. 深度遍历JsonToken,赋值节点树,返回结果。
构造了一个简单的case,测试快速json解析方案(protojson)和jackson通用的json解析方案性能。可以自行测试看看。
测试代码可以参考:https://github.com/lunar-ye/ProtoJson/blob/main/Demo/src/test/java/org/protojson/test/JsonRowConverterTest.java
2022-11-01 首发于快手技术博客
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。