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 國際許可協議進行許可。