Java解析JSON大文件解决方案之JsonReader

一,使用背景

之前遇到一个需求,是需要将一个json文件解析存储到数据库中。一开始测试的时候,json文件的大小都在几兆以内,所以直接将json文件转化为字符串,再转化成JSONObject对象进行处理时不会出现问题,如下所示:

File file = new File("")
try(FileInputStream fileInputStream = new FileInputStream(file)) {
    int size = fileInputStream.available();
    byte[] buffer = new byte[size];
    fileInputStream.read(buffer);
    String jsonString = new String(buffer, StandardCharsets.UTF_8);
    jsonString.replaceAll("\n", "");
    jsonString.replaceAll("\r", "");
    JSONObject json = JSON.parseObject(jsonString);
}

但是,当出现几十兆文件的时候,这时候就会报出内存溢出的错误

java.lang.OutOfMemoryError: Java heap space

虽然稍微大一点的文件,可以通过调整JVM参数来解决,如下所示

-Xms512m -Xmx2048m

但是这毕竟不是最合理的方法,因为当文件大到一定程度后,字节数组和字符串类型都存在接收不了的情况。因此,只能选择另外的方式,此时,Google的JsonReader是一个不错的解决方案。

二,JsonReader的使用

maven依赖如下:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

JsonReader读取 JSON (RFC 7159) 编码值作为令牌流。 此流包括文字 值(字符串、数字、布尔值和空值)以及开始和 对象和数组的结束分隔符。 令牌被遍历 深度优先顺序,与它们在 JSON 文档中出现的顺序相同。 在 JSON 对象中,名称/值对由单个标记表示。

解析json

创建递归下降解析器 JSON ,首先创建 创建一个入口点方法 JsonReader.

每个对象类型和每个数组类型都需要一个方法。

  • 数组处理 方法中,首先调用 beginArray()消耗数组的左括号。 然后创建一个累积值的while循环,在何时终止 hasNext()为false。 最后,通过调用读取数组的右括号 endArray()
  • 对象处理 方法中,首先调用 beginObject()消耗对象的左大括号。 然后创建一个while循环根据局部变量的名称为其赋值。 这个循环应该在什么时候终止 hasNext()为false。 最后,通过调用读取对象的右括号 endObject().

当遇到嵌套对象或数组时,委托给对应的处理方法。

当遇到未知名称时,严格的解析器应该失败并返回。 但宽松的解析器应该调用 skipValue()递归地 跳过值的嵌套标记,否则可能会发生冲突。

如果一个值可能为空,应该首先检查使用 peek(). 空字面量可以使用 nextNull()或者 skipValue().

例如,我之前要解析的json文件格式如下:

{
    "INFO": {
        "NAME": "",
        "Result": "",
        "Config": "",
        ...
    },
    "ATTR": {
        "key01": "val01",
        "key02": "val02",
        ...
    },
    "Parms": [
        {
            "k": "",
            "v": "",
            "p": "",
            "m": "",
            "l": ""
        },
        {
            "k": "",
            "v": "",
            "p": "",
            "m": "",
            "l": ""
        },
        ...
    ],
    "List": ["xxx", "xxxx", ...]
}

那按照JsonReader解析的思路,我应该先消费整体对象的{,再逐个对INFO,ATTR,Parms,List进行处理,总而言之,就是

String fileName = "";
FileReader in = new FileReader(fileName);
JsonReader reader = new JsonReader(in);
reader.beginObject();
String rootName = null;
while (reader.hasNext()) {
    rootName = reader.nextName();
    if("INFO".equals(rootName)) {
        reader.beginObject();
        while (reader.hasNext()) {
            System.out.println(reader.nextName() + ":" + reader.nextString())
        }
        reader.endObject();
    }else if("ATTR".equals(rootName)) {
        reader.beginObject();
        while (reader.hasNext()) {
            System.out.println(reader.nextName() + ":" + reader.nextString())
        }
        reader.endObject();
    }else if("Parms".equals(rootName)) {
        reader.beginArray();
        while (reader.hasNext()) {
            reader.beginObject();
            String k = null;
            while (reader.hasNext()) {
                k = reader.nextName();
                switch (k) {
                    case "k":
                        xxx;
                        break;
                    case "v":
                        xxx;
                        break;
                    case "p":
                        xxx;
                        break;
                    case "m":
                        xxx;
                        break;
                    case "l":
                        xxx;
                        break;
                    default:
                        reader.nextString();
                        break;
                }
            }
            reader.endObject();
        }
        reader.endArray();
    }else if("List".equals(rootName)) {
        reader.beginArray();
        while (reader.hasNext()) {
            System.out.println(reader.nextString());
        }
        reader.endArray();
    }else {
        reader.skipValue();
    }
}

常用方法如下所示:

方法名

返回值

描述

beginArray()

void

使用JSON流中的下一个令牌,并断言它是新数组的开始。

endArray()

void

使用JSON流中的下一个令牌,并断言它是当前数组的结尾。

beginObject()

void

使用JSON流中的下一个令牌,并断言它是新对象的开始。

endObject()

void

使用JSON流中的下一个令牌,并断言它是当前对象的结尾。

close()

void

关闭此 JSON阅读器 和底层 Reader.

getPath()

String

返回JSON值中当前位置的JsonPath。

hasNext()

Boolean

如果当前数组或对象有其他元素,则返回true。

isLenient()

Boolean

如果此解析器在接受的内容上是宽松的,则返回true。

setLenient(boolean lenient)

void

将此解析器配置为在其接受的内容上宽松。

nextBoolean()

boolean

返回boolean下一个令牌的值,并使用它。

nextDouble()

double

返回double下一个令牌的值,并使用它。

nextInt()

int

返回int下一个令牌的值,并使用它。

nextLong()

long

返回long下一个令牌的值,并使用它。

nextName()

String

返回下一个标记,即属性名,并使用它。

nextNull()

void

使用JSON流中的下一个令牌,并断言它是文本null。

nextString()

String

返回使用下一个标记的字符串值。

peek()

JsonToken

返回下一个令牌的类型,而不使用它

skipValue()

void

递归跳过下一个值。

通过使用JsonReader,现在我解析几十兆的文件基本没有问题(上百兆的还没尝试过),一个44.5M的JSON文件在4秒就能够处理完。