JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。相对于另外一种数据交换格式 XML,JSON 有着诸多优势。好比易读性更好,占用空间更少等。在 web 应用开发领域内,得益于 JavaScript 对 JSON 提供的良好支持,JSON 要比 XML 更受开发人员青睐。因此做为开发人员,若是有兴趣的话,仍是应该深刻了解一下 JSON 相关的知识。本着探究 JSON 原理的目的,我将会在这篇文章中详细向你们介绍一个简单的JSON解析器的解析流程和实现细节。因为 JSON 自己比较简单,解析起来也并不复杂。因此若是你们感兴趣的话,在看完本文后,不妨本身动手实现一个 JSON 解析器。好了,其余的话就很少说了,接下来让咱们移步到重点章节吧。html
JSON 解析器从本质上来讲就是根据 JSON 文法规则建立的状态机,输入是一个 JSON 字符串,输出是一个 JSON 对象。通常来讲,解析过程包括词法分析和语法分析两个阶段。词法分析阶段的目标是按照构词规则将 JSON 字符串解析成 Token 流,好比有以下的 JSON 字符串:java
{ "name" : "小明", "age": 18 }
结果词法分析后,获得一组 Token,以下:{
、 name
、 :
、 小明
、 ,
、 age
、 :
、 18
、 }
git
图1 词法分析器输入输出github
词法分析解析出 Token 序列后,接下来要进行语法分析。语法分析的目的是根据 JSON 文法检查上面 Token 序列所构成的 JSON 结构是否合法。好比 JSON 文法要求非空 JSON 对象以键值对的形式出现,形如 object = {string : value}
。若是传入了一个格式错误的字符串,好比web
{ "name", "小明" }
那么在语法分析阶段,语法分析器分析完 Token name
后,认为它是一个符合规则的 Token,而且认为它是一个键。接下来,语法分析器读取下一个 Token,指望这个 Token 是 :
。但当它读取了这个 Token,发现这个 Token 是 ,
,并不是其指望的:
,因而文法分析器就会报错误。json
图2 语法分析器输入输出app
这里简单总结一下上面两个流程,词法分析是将字符串解析成一组 Token 序列,而语法分析则是检查输入的 Token 序列所构成的 JSON 格式是否合法。这里你们对 JSON 的解析流程有个印象就好,接下来我会详细分析每一个流程。ide
在本章开始,我说了词法解析的目的,即按照“构词规则”将 JSON 字符串解析成 Token 流。请注意双引号引发来词--构词规则,所谓构词规则是指词法分析模块在将字符串解析成 Token 时所参考的规则。在 JSON 中,构词规则对应于几种数据类型,当词法解析器读入某个词,且这个词类型符合 JSON 所规定的数据类型时,词法分析器认为这个词符合构词规则,就会生成相应的 Token。这里咱们能够参考http://www.json.org/对 JSON 的定义,罗列一下 JSON 所规定的数据类型:测试
当词法分析器读取的词是上面类型中的一种时,便可将其解析成一个 Token。咱们能够定义一个枚举类来表示上面的数据类型,以下:ui
public enum TokenType { BEGIN_OBJECT(1), END_OBJECT(2), BEGIN_ARRAY(4), END_ARRAY(8), NULL(16), NUMBER(32), STRING(64), BOOLEAN(128), SEP_COLON(256), SEP_COMMA(512), END_DOCUMENT(1024); TokenType(int code) { this.code = code; } private int code; public int getTokenCode() { return code; } }
在解析过程当中,仅有 TokenType 类型还不行。咱们除了要将某个词的类型保存起来,还须要保存这个词的字面量。因此,因此这里还须要定义一个 Token 类。用于封装词类型和字面量,以下:
public class Token { private TokenType tokenType; private String value; // 省略不重要的代码 }
定义好了 Token 类,接下来再来定义一个读取字符串的类。以下:
public CharReader(Reader reader) { this.reader = reader; buffer = new char[BUFFER_SIZE]; } /** * 返回 pos 下标处的字符,并返回 * @return * @throws IOException */ public char peek() throws IOException { if (pos - 1 >= size) { return (char) -1; } return buffer[Math.max(0, pos - 1)]; } /** * 返回 pos 下标处的字符,并将 pos + 1,最后返回字符 * @return * @throws IOException */ public char next() throws IOException { if (!hasMore()) { return (char) -1; } return buffer[pos++]; } public void back() { pos = Math.max(0, --pos); } public boolean hasMore() throws IOException { if (pos < size) { return true; } fillBuffer(); return pos < size; } void fillBuffer() throws IOException { int n = reader.read(buffer); if (n == -1) { return; } pos = 0; size = n; } }
有了 TokenType、Token 和 CharReader 这三个辅助类,接下来咱们就能够实现词法解析器了。
public class Tokenizer { private CharReader charReader; private TokenList tokens; public TokenList tokenize(CharReader charReader) throws IOException { this.charReader = charReader; tokens = new TokenList(); tokenize(); return tokens; } private void tokenize() throws IOException { // 使用do-while处理空文件 Token token; do { token = start(); tokens.add(token); } while (token.getTokenType() != TokenType.END_DOCUMENT); } private Token start() throws IOException { char ch; for(;;) { if (!charReader.hasMore()) { return new Token(TokenType.END_DOCUMENT, null); } ch = charReader.next(); if (!isWhiteSpace(ch)) { break; } } switch (ch) { case '{': return new Token(TokenType.BEGIN_OBJECT, String.valueOf(ch)); case '}': return new Token(TokenType.END_OBJECT, String.valueOf(ch)); case '[': return new Token(TokenType.BEGIN_ARRAY, String.valueOf(ch)); case ']': return new Token(TokenType.END_ARRAY, String.valueOf(ch)); case ',': return new Token(TokenType.SEP_COMMA, String.valueOf(ch)); case ':': return new Token(TokenType.SEP_COLON, String.valueOf(ch)); case 'n': return readNull(); case 't': case 'f': return readBoolean(); case '"': return readString(); case '-': return readNumber(); } if (isDigit(ch)) { return readNumber(); } throw new JsonParseException("Illegal character"); } private Token readNull() {...} private Token readBoolean() {...} private Token readString() {...} private Token readNumber() {...} }
上面的代码是词法分析器的实现,部分代码这里没有贴出来,后面具体分析的时候再贴。先来看看词法分析器的核心方法 start,这个方法代码量很少,并不复杂。其经过一个死循环不停的读取字符,而后再根据字符的类型,执行不一样的解析逻辑。上面说过,JSON 的解析过程比较简单。缘由在于,在解析时,只需经过每一个词第一个字符便可判断出这个词的 Token Type。好比:
{
、}
、[
、]
、,
、:
,直接封装成相应的 Token 返回便可n
,指望这个词是null
,Token 类型是NULL
t
或f
,指望这个词是true
或者false
,Token 类型是 BOOLEAN
"
,指望这个词是字符串,Token 类型为String
0~9
或-
,指望这个词是数字,类型为NUMBER
正如上面所说,词法分析器只须要根据每一个词的第一个字符,便可知道接下来它所指望读取的到的内容是什么样的。若是知足指望了,则返回 Token,不然返回错误。下面就来看看词法解析器在碰到第一个字符是n
和"
时的处理过程。先看碰到字符n
的处理过程:
private Token readNull() throws IOException { if (!(charReader.next() == 'u' && charReader.next() == 'l' && charReader.next() == 'l')) { throw new JsonParseException("Invalid json string"); } return new Token(TokenType.NULL, "null"); }
上面的代码很简单,词法分析器在读取字符n
后,指望后面的三个字符分别是u
,l
,l
,与 n
组成词 null。若是知足指望,则返回类型为 NULL 的 Token,不然报异常。readNull 方法逻辑很简单,很少说了。接下来看看 string 类型的数据处理过程:
private Token readString() throws IOException { StringBuilder sb = new StringBuilder(); for (;;) { char ch = charReader.next(); // 处理转义字符 if (ch == '\\') { if (!isEscape()) { throw new JsonParseException("Invalid escape character"); } sb.append('\\'); ch = charReader.peek(); sb.append(ch); // 处理 Unicode 编码,形如 \u4e2d。且只支持 \u0000 ~ \uFFFF 范围内的编码 if (ch == 'u') { for (int i = 0; i < 4; i++) { ch = charReader.next(); if (isHex(ch)) { sb.append(ch); } else { throw new JsonParseException("Invalid character"); } } } } else if (ch == '"') { // 碰到另外一个双引号,则认为字符串解析结束,返回 Token return new Token(TokenType.STRING, sb.toString()); } else if (ch == '\r' || ch == '\n') { // 传入的 JSON 字符串不容许换行 throw new JsonParseException("Invalid character"); } else { sb.append(ch); } } } private boolean isEscape() throws IOException { char ch = charReader.next(); return (ch == '"' || ch == '\\' || ch == 'u' || ch == 'r' || ch == 'n' || ch == 'b' || ch == 't' || ch == 'f'); } private boolean isHex(char ch) { return ((ch >= '0' && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')); }
string 类型的数据解析起来要稍微复杂一些,主要是须要处理一些特殊类型的字符。JSON 所容许的特殊类型的字符以下:
\"
\\
\b
\f
\n
\r
\t
\u four-hex-digits
\/
最后一种特殊字符\/
代码中未作处理,其余字符均作了判断,判断逻辑在 isEscape 方法中。在传入 JSON 字符串中,仅容许字符串包含上面所列的转义字符。若是乱传转义字符,解析时会报错。对于 STRING 类型的词,解析过程始于字符"
,也终于"
。因此在解析的过程当中,当再次遇到字符"
,readString 方法会认为本次的字符串解析过程结束,并返回相应类型的 Token。
上面说了 null 类型和 string 类型的数据解析过程,过程并不复杂,理解起来应该不难。至于 boolean 和 number 类型的数据解析过程,你们有兴趣的话能够本身看源码,这里就不在说了。
当词法分析结束后,且分析过程当中没有抛出错误,那么接下来就能够进行语法分析了。语法分析过程以词法分析阶段解析出的 Token 序列做为输入,输出 JSON Object 或 JSON Array。语法分析器的实现的文法以下:
object = {} | { members } members = pair | pair , members pair = string : value array = [] | [ elements ] elements = value | value , elements value = string | number | object | array | true | false | null
语法分析器的实现须要借助两个辅助类,也就是语法分析器的输出类,分别是 JsonObject 和 JsonArray。代码以下:
public class JsonObject { private Map<String, Object> map = new HashMap<String, Object>(); public void put(String key, Object value) { map.put(key, value); } public Object get(String key) { return map.get(key); } public List<Map.Entry<String, Object>> getAllKeyValue() { return new ArrayList<>(map.entrySet()); } public JsonObject getJsonObject(String key) { if (!map.containsKey(key)) { throw new IllegalArgumentException("Invalid key"); } Object obj = map.get(key); if (!(obj instanceof JsonObject)) { throw new JsonTypeException("Type of value is not JsonObject"); } return (JsonObject) obj; } public JsonArray getJsonArray(String key) { if (!map.containsKey(key)) { throw new IllegalArgumentException("Invalid key"); } Object obj = map.get(key); if (!(obj instanceof JsonArray)) { throw new JsonTypeException("Type of value is not JsonArray"); } return (JsonArray) obj; } @Override public String toString() { return BeautifyJsonUtils.beautify(this); } } public class JsonArray implements Iterable { private List list = new ArrayList(); public void add(Object obj) { list.add(obj); } public Object get(int index) { return list.get(index); } public int size() { return list.size(); } public JsonObject getJsonObject(int index) { Object obj = list.get(index); if (!(obj instanceof JsonObject)) { throw new JsonTypeException("Type of value is not JsonObject"); } return (JsonObject) obj; } public JsonArray getJsonArray(int index) { Object obj = list.get(index); if (!(obj instanceof JsonArray)) { throw new JsonTypeException("Type of value is not JsonArray"); } return (JsonArray) obj; } @Override public String toString() { return BeautifyJsonUtils.beautify(this); } public Iterator iterator() { return list.iterator(); } }
语法解析器的核心逻辑封装在了 parseJsonObject 和 parseJsonArray 两个方法中,接下来我会详细分析 parseJsonObject 方法,parseJsonArray 方法你们本身分析吧。parseJsonObject 方法实现以下:
private JsonObject parseJsonObject() { JsonObject jsonObject = new JsonObject(); int expectToken = STRING_TOKEN | END_OBJECT_TOKEN; String key = null; Object value = null; while (tokens.hasMore()) { Token token = tokens.next(); TokenType tokenType = token.getTokenType(); String tokenValue = token.getValue(); switch (tokenType) { case BEGIN_OBJECT: checkExpectToken(tokenType, expectToken); jsonObject.put(key, parseJsonObject()); // 递归解析 json object expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case END_OBJECT: checkExpectToken(tokenType, expectToken); return jsonObject; case BEGIN_ARRAY: // 解析 json array checkExpectToken(tokenType, expectToken); jsonObject.put(key, parseJsonArray()); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case NULL: checkExpectToken(tokenType, expectToken); jsonObject.put(key, null); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case NUMBER: checkExpectToken(tokenType, expectToken); if (tokenValue.contains(".") || tokenValue.contains("e") || tokenValue.contains("E")) { jsonObject.put(key, Double.valueOf(tokenValue)); } else { Long num = Long.valueOf(tokenValue); if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE) { jsonObject.put(key, num); } else { jsonObject.put(key, num.intValue()); } } expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case BOOLEAN: checkExpectToken(tokenType, expectToken); jsonObject.put(key, Boolean.valueOf(token.getValue())); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case STRING: checkExpectToken(tokenType, expectToken); Token preToken = tokens.peekPrevious(); /* * 在 JSON 中,字符串既能够做为键,也可做为值。 * 做为键时,只期待下一个 Token 类型为 SEP_COLON。 * 做为值时,期待下一个 Token 类型为 SEP_COMMA 或 END_OBJECT */ if (preToken.getTokenType() == TokenType.SEP_COLON) { value = token.getValue(); jsonObject.put(key, value); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; } else { key = token.getValue(); expectToken = SEP_COLON_TOKEN; } break; case SEP_COLON: checkExpectToken(tokenType, expectToken); expectToken = NULL_TOKEN | NUMBER_TOKEN | BOOLEAN_TOKEN | STRING_TOKEN | BEGIN_OBJECT_TOKEN | BEGIN_ARRAY_TOKEN; break; case SEP_COMMA: checkExpectToken(tokenType, expectToken); expectToken = STRING_TOKEN; break; case END_DOCUMENT: checkExpectToken(tokenType, expectToken); return jsonObject; default: throw new JsonParseException("Unexpected Token."); } } throw new JsonParseException("Parse error, invalid Token."); } private void checkExpectToken(TokenType tokenType, int expectToken) { if ((tokenType.getTokenCode() & expectToken) == 0) { throw new JsonParseException("Parse error, invalid Token."); } }
parseJsonObject 方法解析流程大体以下:
上面的步骤并不复杂,但有可能很差理解。这里举个例子说明一下,有以下的 Token 序列:
{
、 id
、 :
、 1
、 }
parseJsonObject 解析完 {
Token 后,接下来它将期待 STRING 类型的 Token 或者 END_OBJECT 类型的 Token 出现。因而 parseJsonObject 读取了一个新的 Token,发现这个 Token 的类型是 STRING 类型,知足指望。因而 parseJsonObject 更新指望Token 类型为 SEL_COLON,即:
。如此循环下去,直至 Token 序列解析结束或者抛出异常退出。
上面的解析流程虽然不是很复杂,但在具体实现的过程当中,仍是须要注意一些细节问题。好比:
:
,那么此处的字符串只能做为值了。不然,则只能作为键。[Integer.MIN_VALUE, Integer.MAX_VALUE]
范围内的整数来讲,解析成 Integer 更为合适,因此解析的过程当中也须要注意一下。为了验证代码的正确性,这里对代码进行了简单的测试。测试数据来自网易音乐,大约有4.5W个字符。为了不每次下载数据,因数据发生变化而致使测试不经过的问题。我将某一次下载的数据保存在了 music.json 文件中,后面每次测试都会从文件中读取数据。关于测试部分,这里就不贴代码和截图了。你们有兴趣的话,能够本身下载源码测试玩玩。
测试就很少说了,接下来看看 JSON 美化效果展现。这里随便模拟点数据,就模拟王者荣耀里的狄仁杰英雄信息吧(对,这个英雄我常常用)。以下图:
图3 JSON 美化结果
关于 JSON 美化的代码这里也不讲解了,并不是重点,只算一个彩蛋吧。
到此,本文差很少要结束了。本文对应的代码已经放到了 github 上,须要的话,你们可自行下载。传送门 -> JSONParser。这里须要声明一下,本文对应的代码实现了一个比较简陋的 JSON 解析器,实现的目的是探究 JSON 的解析原理。JSONParser 只算是一个练习性质的项目,代码实现的并不优美,并且缺少充足的测试。同时,限于本人的能力(编译原理基础基本能够忽略),我并没有法保证本文以及对应的代码中不出现错误。若是你们在阅读代码的过程当中,发现了一些错误,或者写的很差的地方,能够提出来,我来修改。若是这些错误对你形成了困扰,这里先说一声很抱歉。最后,本文及实现主要参考了一块儿写一个JSON解析器和如何编写一个JSON解析器两篇文章及两篇文章对应的实现代码,在这里向着两篇博文的做者表示感谢。好了,本文到此结束,祝你们生生活愉快!再见。
本文在知识共享许可协议 4.0 下发布,转载请注明出处
做者:coolblog
为了得到更好的分类阅读体验,
请移步至本人的我的博客: http://www.coolblog.xyz
本做品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。