Jackson复杂Json的快速解析
背景
在面对复杂Json字符串,但只解析部分字段场景时。
如何优化jackson反序列化字符串的过程来减少不必要的耗时操作(对象重复创建、无用路径解析),实现一种性能更高,资源消耗更少的json解析能力?
使用方式
JsonRowConverter类的构造参数为要解析json字符串中各个字段的路径,嵌套路径以 . 分割,对json字符串各个路径的解析结果会转换为string字符串,并按照构造参数的路径顺序返回String数组,示例代码如下。
// 用法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);
原理
jackson解析思路
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组合;
- 将调用DefaultSerializationContext的readRootValue方法。
- 将调用DefaultSerializationContext的readRootValue方法。
jackson定义了一系列json反序列化类,不同的反序列化类会将json反序列化为不同的类型。比如MapDeserializer类会将jsontoken集合解析为Map类型,而JsonNode deserializer类会将jsontoken集合解析为JsonNode类型。
但是jackson提供的官方解析方法为了保证扩展性,存在会将大量的无用字段递归解析,并且会在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. 对象重复创建:将对象存储到节点树上,复用对象,不需要重复创建对象。
-
构建节点树
构建节点树.png -
词法解析json字符串,生成JsonToken集合。
-
深度遍历JsonToken,赋值节点树,返回结果
代码实现
引入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
定义pojo
public abstract class AbstractNode<K, V, C extends AbstractNode> {
private long version = 0;
private boolean isLeaf = false;
private K name;
private V value = null;
private C parent = null;
private HashMap<K, C> children;
public AbstractNode(K name) {
this.name = name;
}
public abstract void _init();
public boolean isLeaf() {
return isLeaf;
}
public void setLeaf(boolean leaf) {
isLeaf = leaf;
}
public HashMap<K, C> getChildren() {
return children;
}
public void setChildren(HashMap<K, C> children) {
this.children = children;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
public K getName() {
return name;
}
public void setName(K name) {
this.name = name;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
public C getParent() {
return parent;
}
public void setParent(C parent) {
this.parent = parent;
}
}
public class JsonNode extends AbstractNode<String, String, JsonNode> {
private int start = -1;
private int end = -1;
public JsonNode(String name) {
super(name);
}
public void _init() {
setChildren(new HashMap<String, JsonNode>());
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
}
定义解析器
import java.io.IOException;
import org.protojson.pojo.JsonNode;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonRowConverter {
private static final ObjectMapper MAPPER = new ObjectMapper();
private String _json;
private JsonNode root = new JsonNode("root");
private JsonNode _ptr = root;
private JsonNode[] row;
private String[] result;
private static String[] EMPTY_RESULT;
private final int NUM;
private int currentVersion = 0;
public JsonRowConverter(String... args) {
NUM = args.length;
row = new JsonNode[NUM];
result = new String[NUM];
EMPTY_RESULT = new String[NUM];
for (int i = 0; i < args.length; i++) {
EMPTY_RESULT[i] = null;
}
for (int i = 0; i < NUM; i++) {
JsonNode cur = root;
String[] paths = args[i].split("\\.");
for (int j = 0; j < paths.length; j++) {
if (cur.getChildren() == null) {
cur._init();
}
if (!cur.getChildren().containsKey(paths[j])) {
JsonNode child = new JsonNode(paths[j]);
cur.getChildren().put(paths[j], child);
child.setParent(cur);
}
cur = cur.getChildren().get(paths[j]);
}
cur.setLeaf(true);
row[i] = cur;
}
}
public String[] process(String json) throws IOException {
currentVersion++;
_ptr = root;
_json = json;
if (json == null || json.length() == 0) {
return EMPTY_RESULT;
}
JsonParser parser = MAPPER.createParser(json);
parser.nextToken();
_recursion(_ptr, parser, null);
return getResult(_json);
}
private void _recursion(JsonNode ptr, JsonParser parser, JsonToken skipArray) {
try {
while (parser.currentToken() != null) {
// System.out.println("parser name:" + parser.currentName());
// System.out.println("parser token:" + parser.getCurrentToken().name());
// System.out.println("node name:" + ptr.getName());
switch (parser.currentToken()) {
case START_OBJECT:
ptr.setVersion(currentVersion);
ptr.setStart(parser.currentLocation().getColumnNr() - 2);
parser.nextToken();
break;
case END_OBJECT:
ptr.setEnd(parser.getCurrentLocation().getColumnNr() - 1);
ptr = ptr.getParent();
parser.nextToken();
if (skipArray == JsonToken.END_OBJECT) return;
break;
case START_ARRAY:
ptr.setVersion(currentVersion);
ptr.setStart(parser.getCurrentLocation().getColumnNr() - 2);
int i = 0;
while (parser.currentToken() != JsonToken.END_ARRAY) {
parser.nextToken();
if (ptr.getChildren().containsKey("" + i)) {
ptr = ptr.getChildren().get("" + i);
if (parser.currentToken() == JsonToken.START_OBJECT) {
_recursion(ptr, parser, JsonToken.END_OBJECT);
} else if (parser.currentToken() == JsonToken.START_ARRAY) {
_recursion(ptr, parser, JsonToken.END_ARRAY);
} else {
_recursion(ptr, parser, JsonToken.NOT_AVAILABLE);
}
ptr = ptr.getParent();
} else {
skip(parser);
}
++i;
}
break;
case END_ARRAY:
ptr.setEnd(parser.getCurrentLocation().getColumnNr() - 1);
ptr = ptr.getParent();
parser.nextToken();
if (skipArray == JsonToken.END_ARRAY) return;
break;
case FIELD_NAME:
if (ptr.getChildren() != null && ptr.getChildren().containsKey(parser.getCurrentName())) {
ptr = ptr.getChildren().get(parser.getCurrentName());
ptr.setVersion(currentVersion);
if (ptr.getChildren() == null) {
parser.nextToken();
ptr.setValue(skip(parser,true));
ptr = ptr.getParent();
}
} else {
parser.nextToken();
skip(parser);
}
parser.nextToken();
break;
case VALUE_STRING:
case VALUE_NUMBER_FLOAT:
case VALUE_NULL:
case VALUE_NUMBER_INT:
case VALUE_TRUE:
case VALUE_FALSE:
ptr.setVersion(currentVersion);
ptr.setValue(parser.getValueAsString());
ptr = ptr.getParent();
if (skipArray == JsonToken.NOT_AVAILABLE) return;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private String[] getResult(String json) {
for (int i = 0; i < NUM; i++) {
if (row[i].getVersion() < currentVersion) {
result[i] = null;
continue;
}
if (row[i].getChildren() != null) {
result[i] = json.substring(row[i].getStart(), row[i].getEnd());
} else {
result[i] = row[i].getValue();
}
}
return result;
}
private String skip(JsonParser parser, boolean flag) throws IOException {
_ptr.setVersion(currentVersion);
int i = 0, start;
start = flag ? parser.getCurrentLocation().getColumnNr() - 2 : 0;
switch (parser.currentToken()) {
case START_OBJECT:
i++;
while (i > 0 && parser.nextToken() != null) {
if (parser.currentToken() == JsonToken.START_OBJECT) i++;
else if (parser.currentToken() == JsonToken.END_OBJECT) i--;
}
return flag ? _json.substring(start, parser.getCurrentLocation().getColumnNr() - 1): null;
case START_ARRAY:
i++;
while (i > 0 && parser.nextToken() != null) {
if (parser.currentToken() == JsonToken.START_ARRAY) i++;
else if (parser.currentToken() == JsonToken.END_ARRAY) i--;
}
return flag ? _json.substring(start, parser.getCurrentLocation().getColumnNr() - 1): null;
default:
return flag ? parser.getValueAsString() : null;
}
}
private void skip(JsonParser parser) throws IOException {
skip(parser, false);
}
}