【本篇博文會介紹JSON解析的原理與實現,並一步一步寫出來一個簡單但實用的JSON解析器,項目地址:SimpleJSON。希望通過這篇博文,能讓我們以后與JSON打交道時更加得心應手。由於個人水平有限,敘述中難免存在不准確或是不清晰的地方,希望大家可以指正:)】
一、JSON解析器介紹
相信大家在平時的開發中沒少與JSON打交道,那么我們平常使用的一些JSON解析庫都為我們做了哪些工作呢?這里我們以知乎日報API返回的JSON數據來介紹一下兩個主流JSON解析庫的用法。我們對地址 http://news-at.zhihu.com/api/4/news/latest進行GET請求,返回的JSON響應的整體結構如下:
{
date: "20140523", stories: [ { images:["http:\/\/pic1.zhimg.com\/4e7ecded780717589609d950bddbf95c.jpg"] type: 0, id: 3930445, ga_prefix: "052321", title: "中國古代家具發展到今天有兩個高峰,一個兩宋一個明末(多圖)", }, ... ], top_stories: [ {
image:"http:\/\/pic4.zhimg.com\/8f209bcfb5b6e0625ca808e43c0a0a73.jpg",
type:0,
id:8314043,
ga_prefix:"051717",
title:"怎樣才能找到自己的興趣所在,發自內心地去工作?"
},
...
]
}
以上JSON響應表示的是某天的最新知乎日報內容。頂層的date的值表示的是日期;stories的值是一個數組,數組的每個元素又包含images、type、id等域;top_stories的值也是一個數組,數組元素的結構與stories類似。我們先把把以上返回的JSON數據表示為一個model類:
public class LatestNews { private String date; private List<TopStory> top_stories; private List<Story> stories; //省略LatestNews類的getter與setter public static class TopStory { private String image; private int type; private int id; private String title; //省略TopStory類的getter與setter } public static class Story implements Serializable { private List<String> images; private int type; private int id; private String title; //省略Story類的getter與setter } }
在以上的代碼中,我們定義的域與返回的JSON響應的鍵一一對應。那么接下來我們就來完成JSON響應的解析吧。首先我們使用org.json包來完成JSON的解析。相關代碼如下:
1 public class JSONParsingTest { 2 public static final String urlString = "http://news-at.zhihu.com/api/4/news/latest"; 3 public static void main(String[] args) throws Exception { 4 try { 5 String jsonString = new String(HttpUtil.get(urlString)); 6 JSONObject latestNewsJSON = new JSONObject(jsonString); 7 String date = latestNewsJSON.getString("date"); 8 JSONArray top_storiesJSON = latestNewsJSON.getJSONArray("top_stories"); 9 LatestNews latest = new LatestNews(); 10 11 12 List<LatestNews.TopStory> stories = new ArrayList<>(); 13 14 for (int i = 0; i < top_storiesJSON.length(); i++) { 15 LatestNews.TopStory story = new LatestNews.TopStory(); 16 story.setId(((JObject) top_storiesJSON.get(i)).getInt("id")); 17 story.setType(((JObject) top_storiesJSON.get(i)).getInt("type")); 18 story.setImage(((JObject) top_storiesJSON.get(i)).getString("image")); 19 story.setTitle(((JObject) top_storiesJSON.get(i)).getString("title")); 20 stories.add(story); 21 } 22 latest.setDate(date); 23 24 System.out.println("date: " + latest.getDate()); 25 for (int i = 0; i < stories.size(); i++) { 26 System.out.println(stories.get(i)); 27 } 28 29 } catch (JSONException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 }
相信Android開發的小伙伴對org.json都不陌生,因為Android SDK中提供的JSON解析類庫就是org.json,要是使用別的開發環境我們可能就需要手動導入org.json包。
第5行我們調用了HttpUtil.get方法來獲取JSON格式的響應字符串,HttpUtil是我們封裝的一個用於網絡請求的靜態代碼庫,代碼見這里:
接着在第6行,我們以JSON字符串為參數構造了一個JSONObject對象;在第7行我們調用JSONObject的實例方法getString根據鍵名“date”獲取了date對應的值並保存在了一個String變量中。
在第8行我們調用了JSONObject的getJSONArray方法來從JSONObject對象中獲取一個JSON數組,這個JSON數組的每個元素均為JSONObject(代表了一個TopStory),每個JSONObject都可以通過在其上調用getInt、getString等方法獲取type、title等鍵的值。正如我們在第14到21行所做的,我們通過一個循環讀取JSONArray的每個JSONObject中的title、id、type、image域的值,並把他們寫入TopStory對象的對應實例域。
我們可以看到,當返回的JSON響應結構比較復雜時,使用org.json包來解析響應比較繁瑣。那么我們看看如何使用gson(Google出品的JSON解析庫,被廣泛應用於Android開發中)來完成相同的工作:
public class GsonTest { public static final String urlString = "http://news-at.zhihu.com/api/4/news/latest"; public static void main(String[] args) { LatestNews latest = new LatestNews(); String jsonString = new String(HttpUtil.get(urlString)); latest = (new Gson()).fromJson(jsonString, LatestNews.class); System.out.println(latest.getDate()); for (int i = 0; i < latest.getTop_stories().size(); i++) { System.out.println(latest.getTop_stories().get(i)); } } }
我們可以看到,使用gson完成同樣的工作只需要一行代碼。那么讓我們一起來看一下gson是如何做到的。在上面的代碼中,我們調用了Gson對象的fromJson方法,傳入了返回的JSON字符串和Latest.class作為參數。看到Latest.class,我們就大概能夠知道fromJson方法的內部工作機制了。可以通過反射獲取到LatestNews的各個實例域,然后幫助我們讀取並填充這些實例域。那么fromJson怎么知道我們要填充LatestNews的哪些實例域呢?實際上我們必須保證LatestNews的域的名字與JSON字符串中對應的鍵的名字相同,這樣gson就能夠把我們的model類與JSON字符串“一一對應“起來,也就是說我們要保證我們的model類與JSON字符串具有相同的層級結構,這樣gson就可以根據名稱從JSON字符串中為我們的實例域尋找一個對應的值。我們可以做個小實驗:把LatestNews中TopStory的title實例域的名字改為title1,這時再只執行以上程序,會發現每個story的title1域均變為null了。
通過上面的介紹,我們感受到了JSON解析庫帶給我們的便利,接下來我們一起來實現org.json包提供給我們的基本JSON解析功能,然后再進一步嘗試實現gson提供給我們的更方便快捷的JSON解析功能。
二、JSON解析基本原理
現在,假設我們沒有任何現成的JSON解析庫可用,我們要自己完成JSON的解析工作。JSON解析的工作主要分一下幾步:
- 詞法分析:這個過程把輸入的JSON字符串分解為一系列詞法單元(token)。比如以下JSON字符串:
{ "date" : 20160517, "id" : 1 }
經過詞法分析后,會被分解為以下token:“{”、 ”date“、 “:”、 “20160517”、 “,"、 “id”、 “:”、 “1”、 “}”。
- 語法分析:這一過程的輸入是上一步得到的token序列。語法分析這一階段完成的工作是把token構造成抽象語法單元。對於JSON的解析,這里的抽象語法對象就類似於org.json包中的JSONObject和JSONArray等。有了抽象語法對象,我們就可以進一步把它“映射到”Java數據類型。
實際上,在進行詞法分析之前,JSON數據對計算機來說只是一個沒有意義的字符串而已。詞法分析的目的是把這些無意義的字符串變成一個一個的token,而這些token有着自己的類型和值,所以計算機能夠區分不同的token,還能以token為單位解讀JSON數據。接下來,語法分析的目的就是進一步處理token,把token構造成一棵抽象語法樹(Abstract Syntax Tree)(這棵樹的結點是我們上面所說的抽象語法對象)。比如上面的JSON數據我們經過詞法分析后得到了一系列token,然后我們把這些token作為語法分析的輸入,就可以構造出一個JSONObject對象(即只有一個結點的抽象語法樹),這個JSONObject對象有date和id兩個實例域。下面我們來分別介紹詞法分析與語法分析的原理和實現。
1. 詞法分析
JSON字符串中,一共有幾種token呢?根據http://www.json.org/對JSON格式的相關定義,我們可以把token分為以下類型:
- STRING(字符串字面量)
- NUMBER(數字字面量)
- NULL(null)
- START_ARRAY([)
- END_ARRAY(])
- START_OBJ({)
- END_OBJ(})
- COMMA(,)
- COLON(:)
- BOOLEAN(true或者false)
- END_DOC(表示JSON數據的結束)
我們可以定義一個枚舉類型來表示不同的token類型:
public enum TokenType {
START_OBJ, END_OBJ, START_ARRAY, END_ARRAY, NULL, NUMBER, STRING, BOOLEAN, COLON, COMMA, END_DOC
}
然后,我們還需要定義一個Token類用於表示token:
public class Token { private TokenType type; private String value; public Token(TokenType type, String value) { this.type = type; this.value = value; } public TokenType getType() { return type; } public String getValue() { return value; } public String toString() { return getValue(); } }
在這之后,我們就可以開始寫詞法分析器了,詞法分析器通常被稱為lexer或是tokenizer。我們可以使用DFA(確定有限狀態自動機)來實現tokenizer,也可以直接使用使用Java的regex包。這里我們使用DFA來實現tokenizer。
實現詞法分析器(tokenizer)和語法分析器(parser)的依據都是JSON文法,完整的JSON文法如下(來自https://www.zhihu.com/question/24640264/answer/80500016):
object = {} | { members } members = pair | pair , members pair = string : value array = [] | [ elements ] elements = value | value , elements value = string | number | object | array | true | false | null string = "" | " chars " chars = char | char chars char = any-Unicode-character-except-"-or-\-or- control-character | \" | \\ | \/ | \b | \f | \n | \r | \t | \u four-hex-digits number = int | int frac | int exp | int frac exp int = digit | digit1-9 digits | - digit | - digit1-9 digits frac = . digits exp = e digits digits = digit | digit digits e = e | e+ | e- | E | E+ | E-
現在,我們就可以根據JSON的文法來構造DFA了,核心代碼如下:
1 private Token start() throws Exception { 2 c = '?'; 3 Token token = null; 4 do { //先讀一個字符,若為空白符(ASCII碼在[0, 20H]上)則接着讀,直到剛讀的字符非空白符 5 c = read(); 6 } while (isSpace(c)); 7 if (isNull(c)) { 8 return new Token(TokenType.NULL, null); 9 } else if (c == ',') { 10 return new Token(TokenType.COMMA, ","); 11 } else if (c == ':') { 12 return new Token(TokenType.COLON, ":"); 13 } else if (c == '{') { 14 return new Token(TokenType.START_OBJ, "{"); 15 } else if (c == '[') { 16 return new Token(TokenType.START_ARRAY, "["); 17 } else if (c == ']') { 18 return new Token(TokenType.END_ARRAY, "]"); 19 } else if (c == '}') { 20 return new Token(TokenType.END_OBJ, "}"); 21 } else if (isTrue(c)) { 22 return new Token(TokenType.BOOLEAN, "true"); //the value of TRUE is not null 23 } else if (isFalse(c)) { 24 return new Token(TokenType.BOOLEAN, "false"); //the value of FALSE is null 25 } else if (c == '"') { 26 return readString(); 27 } else if (isNum(c)) { 28 unread(); 29 return readNum(); 30 } else if (c == -1) { 31 return new Token(TokenType.END_DOC, "EOF"); 32 } else { 33 throw new JsonParseException("Invalid JSON input."); 34 } 35 }
我們可以看到,tokenizer的核心代碼十分簡潔,因為我們把一些稍繁雜的處理邏輯都封裝在了一個個方法中,比如上面的readNum方法、readString方法等。
以上代碼的第4到第6行的功能是消耗掉開頭的所有空白字符(如space、tab等),直到讀取到一個非空白字符,isSpace方法用於判斷一個字符是否屬於空白字符。也就是說,DFA從起始狀態開始,若讀到一個空字符,會在起始狀態不斷循環,直到遇到非空字符,狀態轉移情況如下:
接下來我們可以看到從代碼的第7行到第33行是一個if語句塊,外層的所有if分支覆蓋了DFA的所有可能狀態。在第7行我們會判斷讀入的是不是“null”,isNull方法的代碼如下:
private boolean isNull(int c) throws IOException { if (c == 'n') { c = read(); if (c == 'u') { c = read(); if (c == 'l') { c = read(); if (c == 'l') { return true; } else { throw new JsonParseException("Invalid JSON input."); } } else { throw new JsonParseException("Invalid JSON input."); } } else { throw new JsonParseException("Invalid JSON input."); } } else { return false; } }
也就是說,當第一個非空字符為'n'時,我們會判斷下一個是否為‘u',接着判斷下面的是不是'u'、’l',這中間任何一步的判斷結果為否,就說明我們遇到了一個非法關鍵字(比如null拼寫錯誤,拼成了noll,這就是非法關鍵字),就會拋出異常,只有我們依次讀取的4個字符分別為'n'、'u'、'l'、'l'時,isNull方法才會返回true。下面出現的isTrue、isFalse分別用來判斷“true”和“false”,具體實現與isNull類似。
現在讓我們回到以上的代碼,接着看從第9行到第20行,我們會根據下一個字符的不同轉移到不同的狀態。若下一個字符為’{'、 '}'、 '['、 ']'、 ':'、 ','等6種中的一個,則DFA運行停止,此時我們構造一個新的相應類型的Token對象,並直接返回這個token,作為DFA本次運行的結果。這幾個狀態轉移的示意圖如下:
上圖中圓圈中的數字僅僅表示狀態的標號,我們僅畫出了下一個字符分別為'{'、'['、':'時的狀態轉移(省略了3種情況)。
接下來,讓我們看第25行到第26行的代碼。這部分代碼的主要作用是讀取一個由雙引號包裹的字符串字面量並構造一個TokenType為STRING的Token對象。若剛讀取到的字符為雙引號,意味着接下來的是一個字符串字面量,所以我們調用readString方法來讀入一個字符串變量。readString方法的代碼如下:
1 private Token readString() throws IOException { 2 StringBuilder sb = new StringBuilder(); 3 while (true) { 4 c = read(); 5 if (isEscape()) { //判斷是否為\", \\, \/, \b, \f, \n, \t, \r. 6 if (c == 'u') { 7 sb.append('\\' + (char) c); 8 for (int i = 0; i < 4; i++) { 9 c = read(); 10 if (isHex(c)) { 11 sb.append((char) c); 12 } else { 13 throw new JsonParseException("Invalid Json input."); 14 } 15 } 16 } else { 17 sb.append("\\" + (char) c); 18 } 19 } else if (c == '"') { 20 return new Token(TokenType.STRING, sb.toString()); 21 } else if (c == '\r' || c == '\n'){ 22 throw new JsonParseException("Invalid JSON input."); 23 } else { 24 sb.append((char) c); 25 } 26 } 27 }
我們來看一下readString方法的代碼。第3到26行是一個無限循環,退出循環的條件有兩個:一個是又讀取到一個雙引號(意味着字符串的結束),第二個條件是讀取到了非法字符('\r'或’、'\n')。第5行的功能是判斷剛讀取的字符是否是轉義字符的開始,isEscape方法的代碼如下:
private boolean isEscape() throws IOException { if (c == '\\') { c = read(); if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || c == 't' || c == 'r' || c == 'u') { return true; } else { throw new JsonParseException("Invalid JSON input."); } } else { return false; } }
我們可以看到這個方法是用來判斷接下來的輸入流中是否為以下字符組合:\", \\, \/, \b, \f, \n, \t, \r, \uhhhh(hhhh表示四位十六進制數)。若是以上幾種中的一個,我們會接着判斷是不是“\uhhhh“,並對他進行特殊處理,如readString方法的第7到15行所示,實際上就是先把'\u'添加到StringBuilder對象中,在依次讀取它后面的4個字符,若是十六進制數字,則append,否則拋出異常。
現在讓我們回到start方法,接着看第27到29行的代碼,這兩行代碼用於讀入一個數字字面量。isNum方法用於判斷輸入流中接下來的內容是否是數字字面量,這個方法的源碼如下:
private boolean isNum(int c) { return isDigit(c) || c == '-'; }
根據上面我們貼出的JSON文法,只有下一個字符為數字0~9或是'-',接下來的內容才可能是一個數字字面量,isDigit方法用於判斷下一個字符是否是0~9這10個數字中的一個。
我們注意到第28行有一個unread方法調用,意味着我們下回調用read方法還是返回上回調用read方法返回的那個字符,為什么這么做我們看一下readNum方法的代碼就知道了:
1 private Token readNum() throws IOException { 2 StringBuilder sb = new StringBuilder(); 3 int c = read(); 4 if (c == '-') { //- 5 sb.append((char) c); 6 c = read(); 7 if (c == '0') { //-0 8 sb.append((char) c); 9 numAppend(sb); 10 11 } else if (isDigitOne2Nine(c)) { //-digit1-9 12 do { 13 sb.append((char) c); 14 c = read(); 15 } while (isDigit(c)); 16 unread(); 17 numAppend(sb); 18 } else { 19 throw new JsonParseException("- not followed by digit"); 20 } 21 } else if (c == '0') { //0 22 sb.append((char) c); 23 numAppend(sb); 24 } else if (isDigitOne2Nine(c)) { //digit1-9 25 do { 26 sb.append((char) c); 27 c = read(); 28 } while (isDigit(c)); 29 unread(); 30 numAppend(sb); 31 } 32 return new Token(TokenType.NUMBER, sb.toString()); //the value of 0 is null 33 }
我們來看一下第4到31行,外層的if語句有三種情況:分別對應着剛讀取的字符為'-'、'0'和數字1~9中的一個。我們來看一下第5到9行的代碼,對應了剛讀取到的字符為'-'這種情況。這種情況表示這個數字字面量是個負數。然后我們再看這種情況下的內層if語句,共有兩種情況,一是負號后面的字符為0,另一個是負號后面的字符為數字1~9中的一個。前者表示本次讀取的數字字面量為-0(后面可以跟着frac或是exp),后者表示本次讀取的字面量為負整數(后面也可以跟着frac或exp)。然后我們看第9行調用的numAppend方法,它的源碼如下:
private void numAppend(StringBuilder sb) throws IOException { c = read(); if (c == '.') { //int frac sb.append((char) c); //apppend '.' appendFrac(sb); if (isExp(c)) { //int frac exp sb.append((char) c); //append 'e' or 'E'; appendExp(sb); } } else if (isExp(c)) { // int exp sb.append((char) c); //append 'e' or 'E' appendExp(sb); } else { unread(); } }
我們上面貼的JSON文法中對數字字面量的定義如下:
number = int | int frac | int exp | int frac exp
numAppend方法的功能就是在我們讀取了數字字面量的int部分后,接着讀取后面可能還有的frac或exp部分,上面的appendFrac方法用於讀取frac部分,appendExp方法用於讀取exp部分。具體的邏輯比較直接,大家直接看代碼就可以了。( 這部分的處理邏輯是否正確未經過嚴格測試,如有錯誤希望大家可以指出,謝謝:) )
到了這里,tokenizer的核心——start()方法我們已經介紹的差不多了,tokenizer的完整代碼請參考文章開頭給出的鏈接,接下來讓我們看一下如何實現JSON parser。
2. 語法分析
經過前一步的詞法分析,我們已經得到了一個token序列,現在讓我們來用這個序列構造出類似於org.json包的JSONObject與JSONArray對象。現在我們的任務就是編寫一個語法分析器(parser),以詞法分析得到的token序列為輸入,產生JSONObject或是JSONArray抽象語法對象。語法分析的依據同樣是上面我們貼出的JSON文法。
語法分析器依據JSON文法的以下部分實現:
object = {} | { members }
members = pair | pair , members pair = string : value array = [] | [ elements ] elements = value | value , elements value = string | number | object | array | true | false | null
具體代碼如下:
1 public class Parser { 2 private Tokenizer tokenizer; 3 4 public Parser(Tokenizer tokenizer) { 5 this.tokenizer = tokenizer; 6 } 7 8 private JObject object() { 9 tokenizer.next(); //consume '{' 10 Map<String, Value> map = new HashMap<>(); 11 if (isToken(TokenType.END_OBJ)) { 12 tokenizer.next(); //consume '}' 13 return new JObject(map); 14 } else if (isToken(TokenType.STRING)) { 15 map = key(map); 16 } 17 return new JObject(map); 18 } 19 20 private Map<String, Value> key(Map<String, Value> map) { 21 String key = tokenizer.next().getValue(); 22 if (!isToken(TokenType.COLON)) { 23 throw new JsonParseException("Invalid JSON input."); 24 } else { 25 tokenizer.next(); //consume ':' 26 if (isPrimary()) { 27 Value primary = new Primary(tokenizer.next().getValue()); 28 map.put(key, primary); 29 } else if (isToken(TokenType.START_ARRAY)) { 30 Value array = array(); 31 map.put(key, array); 32 } 33 if (isToken(TokenType.COMMA)) { 34 tokenizer.next(); //consume ',' 35 if (isToken(TokenType.STRING)) { 36 map = key(map); 37 } 38 } else if (isToken(TokenType.END_OBJ)) { 39 tokenizer.next(); //consume '}' 40 return map; 41 } else { 42 throw new JsonParseException("Invalid JSON input."); 43 } 44 } 45 return map; 46 } 47 48 private JArray array() { 49 tokenizer.next(); //consume '[' 50 List<Json> list = new ArrayList<>(); 51 JArray array = null; 52 if (isToken(TokenType.START_ARRAY)) { 53 array = array(); 54 list.add(array); 55 if (isToken(TokenType.COMMA)) { 56 tokenizer.next(); //consume ',' 57 list = element(list); 58 } 59 } else if (isPrimary()) { 60 list = element(list); 61 } else if (isToken(TokenType.START_OBJ)) { 62 list.add(object()); 63 while (isToken(TokenType.COMMA)) { 64 tokenizer.next(); //consume ',' 65 list.add(object()); 66 } 67 } else if (isToken(TokenType.END_ARRAY)) { 68 tokenizer.next(); //consume ']' 69 array = new JArray(list); 70 return array; 71 } 72 tokenizer.next(); //consume ']' 73 array = new JArray(list); 74 return array; 75 } 76 77 private List<Json> element(List<Json> list) { 78 list.add(new Primary(tokenizer.next().getValue())); 79 if (isToken(TokenType.COMMA)) { 80 tokenizer.next(); //consume ',' 81 if (isPrimary()) { 82 list = element(list); 83 } else if (isToken(TokenType.START_OBJ)) { 84 list.add(object()); 85 } else if (isToken(TokenType.START_ARRAY)) { 86 list.add(array()); 87 } else { 88 throw new JsonParseException("Invalid JSON input."); 89 } 90 } else if (isToken(TokenType.END_ARRAY)) { 91 return list; 92 } else { 93 throw new JsonParseException("Invalid JSON input."); 94 } 95 return list; 96 } 97 98 private Json json() { 99 TokenType type = tokenizer.peek(0).getType(); 100 if (type == TokenType.START_ARRAY) { 101 return array(); 102 } else if (type == TokenType.START_OBJ) { 103 return object(); 104 } else { 105 throw new JsonParseException("Invalid JSON input."); 106 } 107 } 108 109 private boolean isToken(TokenType tokenType) { 110 Token t = tokenizer.peek(0); 111 return t.getType() == tokenType; 112 } 113 114 private boolean isToken(String name) { 115 Token t = tokenizer.peek(0); 116 return t.getValue().equals(name); 117 } 118 119 private boolean isPrimary() { 120 TokenType type = tokenizer.peek(0).getType(); 121 return type == TokenType.BOOLEAN || type == TokenType.NULL || 122 type == TokenType.NUMBER || type == TokenType.STRING; 123 } 124 125 public Json parse() throws Exception { 126 Json result = json(); 127 return result; 128 } 129 130 }
我們先來看以上代碼的第98到107行的json方法,這個方法可以作為語法分析的起點。它會根據第一個Token的類型是START_OBJ或START_ARRAY而選擇調用object方法或是array方法。object方法會返回一個JObject對象(JSONObject),array方法會返回一個JArray對象(JSONArray)。JArray與JObject的定義如下:
public class JArray extends Json implements Value { private List<Json> list = new ArrayList<>(); public JArray(List<Json> list) { this.list = list; } public int length() { return list.size(); } public void add(Json element) { list.add(element); } public Json get(int i) { return list.get(i); } @Override public Object value() { return this; } public String toString() { . . . } } public class JObject extends Json { private Map<String, Value> map = new HashMap<>(); public JObject(Map<String, Value> map) { this.map = map; } public int getInt(String key) { return Integer.parseInt((String) map.get(key).value()); } public String getString(String key) { return (String) map.get(key).value(); } public boolean getBoolean(String key) { return Boolean.parseBoolean((String) map.get(key).value()); } public JArray getJArray(String key) { return (JArray) map.get(key).value(); } public String toString() { . . . } }
JSON parser的邏輯也沒有太復雜的地方,如果哪位同學不太理解,可以寫一個test case跟着走幾遍。
接下來,我們要進入有意思的部分了——實現類似org.json包的根據JSON字符串直接構造JSONObject與JSONArray。
3. parseJSONObject方法與parseJSONArray方法
基於以上的tokenizer與parser,我們可以實現兩個實用的JSON解析方法,有了這兩個方法,可以說我們就完成了一個基本的JSON解析庫。
(1)parseJSONObject方法
該方法以一個JSON字符串為輸入,返回一個JObject,代碼如下:
public static JObject parseJSONObject(String jsonString) throws Exception { Tokenizer tokenizer = new Tokenizer(new BufferedReader(new StringReader(jsonString))); tokenizer.tokenize(); Parser parser = new Parser(tokenizer); return parser.object(); }
(2)parseJSONArray方法
該方法以一個JSON字符串為輸入,返回一個JArray,代碼如下:
public static JObject parseJSONArray(String jsonString) throws Exception { Tokenizer tokenizer = new Tokenizer(new BufferedReader(new StringReader(jsonString))); tokenizer.tokenize(); Parser parser = new Parser(tokenizer); return parser.array(); }
接下來,我們來測試以下這兩個放究竟能不能用,test case如下:
public static void main(String[] args) throws Exception { try { String jsonString = new String(HttpUtil.get(urlString)); JObject latestNewsJSON = parseJSONObject(jsonString); String date = latestNewsJSON.getString("date"); JArray top_storiesJSON = latestNewsJSON.getJArray("top_stories"); LatestNews latest = new LatestNews(); List<LatestNews.TopStory> stories = new ArrayList<>(); for (int i = 0; i < top_storiesJSON.length(); i++) { LatestNews.TopStory story = new LatestNews.TopStory(); story.setId(((JObject) top_storiesJSON.get(i)).getInt("id")); story.setType(((JObject) top_storiesJSON.get(i)).getInt("type")); story.setImage(((JObject) top_storiesJSON.get(i)).getString("image")); story.setTitle(((JObject) top_storiesJSON.get(i)).getString("title")); stories.add(story); } latest.setDate(date); System.out.println("date: " + latest.getDate()); for (int i = 0; i < stories.size(); i++) { System.out.println(stories.get(i)); } } catch (JSONException e) { e.printStackTrace(); } }
實際上,上面的代碼只是把我們使用org.json包的代碼稍作修改。然后我們可以得到了同使用org.json包一樣的輸出,這說明我們的JSON解析器工作正常。以上代碼中的getInt方法與getString方法定義在JObject中,只需要根據要取得的值的類型做類型轉換即可,具體實現可以參考開頭給出的項目地址。接下來,讓我們更上一層樓,實現一個類似與gson中fromJson方法的便捷方法。
4. fromJson方法的實現
這個方法的核心思想是:根據給定的JSON字符串和model類的class對象,通過反射獲取model類的各個實例域的類型及名稱。然后用java.lang.reflect包提供給我們的方法在運行時創建一個model類的對象,然后根據它的實例域的名稱從JObject中獲取相應的值並為model類對象的對應實例域賦值。若實例域為List<T>,我們需要特殊進行處理,這里我們實現了一個inflateList方法來處理這種情況。fromJson方法的代碼如下:
1 public static <T> T fromJson(String jsonString, Class<T> classOfT) throws Exception { 2 Tokenizer tokenizer = new Tokenizer(new BufferedReader(new StringReader(jsonString))); 3 tokenizer.tokenize(); 4 Parser parser = new Parser(tokenizer); 5 JObject result = parser.object(); 6 7 Constructor<T> constructor = classOfT.getConstructor(); 8 Object latestNews = constructor.newInstance(); 9 Field[] fields = classOfT.getDeclaredFields(); 10 int numField = fields.length; 11 String[] fieldNames = new String[numField]; 12 String[] fieldTypes = new String[numField]; 13 for (int i = 0; i < numField; i++) { 14 String type = fields[i].getType().getTypeName(); 15 String name = fields[i].getName(); 16 fieldTypes[i] = type; 17 fieldNames[i] = name; 18 } 19 for (int i = 0; i < numField; i++) { 20 if (fieldTypes[i].equals("java.lang.String")) { 21 fields[i].setAccessible(true); 22 fields[i].set(latestNews, result.getString(fieldNames[i])); 23 } else if (fieldTypes[i].equals("java.util.List")) { 24 fields[i].setAccessible(true); 25 JArray array = result.getJArray(fieldNames[i]); 26 ParameterizedType pt = (ParameterizedType) fields[i].getGenericType(); 27 Type elementType = pt.getActualTypeArguments()[0]; 28 String elementTypeName = elementType.getTypeName(); 29 Class<?> elementClass = Class.forName(elementTypeName); 30 fields[i].set(latestNews, inflateList(array, elementClass));//類型捕獲 31 32 } else if (fieldTypes[i].equals("int")) { 33 fields[i].setAccessible(true); 34 fields[i].set(latestNews, result.getInt(fieldNames[i])); 35 } 36 } 37 return (T) latestNews; 38 }
在第8行,我們構造了一個LatestNews對象。在第9到18行,我們獲取了LatestNews類的所有實例域,並把它們的名稱存在了String數組fieldNames中,把它們的類型存在了String數組fieldTypes中。然后在第19到36行,我們遍歷Field數組fields,對每個實例域進行賦值。若實例域的類型為int或是String或是primitive types(int、double等基本類型),則直接調用set方法對相應實例域賦值(簡單起見,上面只實現了對String類型實例域的處理,對於primitive types的處理與之類似,感興趣的同學可以自己嘗試實現下);若實例域的類型為List,則我們需要為這個List中的每個元素賦值。在第26到29行,我們獲取了List中存儲的元素的類型名稱,然后根據這個名稱獲取了對應的class對象。在第30行,我們調用了inflateList方法來“填充“這個List,這里存在一個”類型捕獲“,具體來說,就是inflateList方法接收的第2個參數Class<T>中的類型參數T捕獲了List中存儲元素的實際類型(第29行我們獲取了這個實際類型並用類型通配符接收了它)。inflateList方法的代碼如下:
1 public static <T> List<T> inflateList(JArray array, Class<T> clz) throws Exception { 2 int size = array.length(); 3 4 List<T> list = new ArrayList<T>(); 5 Constructor<T> constructor = clz.getConstructor(); 6 String className = clz.getName(); 7 if (className.equals("java.lang.String")) { 8 for (int i = 0; i < size; i++) { 9 String element = (String) ((Primary) array.get(i)).value(); 10 list.add((T) element); 11 return list; 12 } 13 } 14 Field[] fields = clz.getDeclaredFields(); 15 int numField = fields.length; 16 String[] fieldNames = new String[numField]; 17 String[] fieldTypes = new String[numField]; 18 19 for (int i = 0; i < numField; i++) { 20 String type = fields[i].getType().getTypeName(); 21 String name = fields[i].getName(); 22 fieldTypes[i] = type; 23 fieldNames[i] = name; 24 } 25 for (int i = 0; i < size; i++) { 26 T element = constructor.newInstance(); 27 JObject object = (JObject) array.get(i); 28 for (int j = 0; j < numField; j++) { 29 if (fieldTypes[j].equals("java.lang.String")) { 30 fields[j].setAccessible(true); 31 fields[j].set(element, (object.getString(fieldNames[j]))); 32 } else if (fieldTypes[j].equals("java.util.List")) { 33 fields[j].setAccessible(true); 34 JArray nestArray = object.getJArray(fieldNames[j]); 35 ParameterizedType pt = (ParameterizedType) fields[j].getGenericType(); 36 Type elementType = pt.getActualTypeArguments()[0]; 37 String elementTypeName = elementType.getTypeName(); 38 Class<?> elementClass = Class.forName(elementTypeName); 39 String value = null; 40 41 fields[j].set(element, inflateList(nestArray, elementClass));//Type Capture 42 } else if (fieldTypes[j].equals("int")) { 43 fields[j].setAccessible(true); 44 fields[j].set(element, object.getInt(fieldNames[j])); 45 } 46 47 } 48 list.add(element); 49 } 50 return list; 51 }
在這個方法中,我們會根據對JSON解析獲取的JArray所含的元素個數,以及我們之前獲取到的元素的類型,構造相應數目的對象,並添加到list中去。具體的執行過程大家可以參考代碼,邏輯比較直接。
需要注意的是以上代碼的第7到13行,它的意思是若列表的元素類型為String,我們就應直接從相應的JArray中獲取元素並添加到list中,然后直接返回list。實際上,對於primitive types我們都應該做相似處理,簡單起見,這里只對String類型做了處理,其他primitive types的處理方式類似。
接下來測試一下我們實現的fromJson方法是否能如我們預期那樣工作,test case還是解析上面的知乎日報API返回的數據:
public class SimpleJSONTest { public static final String urlString = "http://news-at.zhihu.com/api/4/news/latest"; public static void main(String[] args) throws Exception { LatestNews latest = new LatestNews(); String jsonString = new String(HttpUtil.get(urlString)); latest = Parser.fromJson(jsonString, LatestNews.class); System.out.println(latest.getDate()); for (int i = 0; i < latest.getTop_stories().size(); i++) { System.out.println(latest.getTop_stories().get(i)); } } }
我們還可以對比一下我們的實現與gson的實現的性能,我這里測試的結果是SimpleJSON的速度大約是gson速度的三倍,考慮到我們的SimpleJSON在不少地方”偷懶“了,這個測試結果並不能說明我們的實現性能要優於gson,不過這或許可以說明我們的JSON解析庫還是具備一定的實用性...
由於本篇博文重點在介紹一個JSON解析器的實現思路,在具體實現上很多部分做的並不好。比如沒有做足夠多的測試來驗證JSON解析的正確性,業務邏輯上也盡量使用直接的方式,許多地方沒使用更加高效的實現,另外在拋出異常方面也比較隨便,“一言不合”就拋異常...由於個人水平有限,代碼中難免存在謬誤,希望大家多多包涵,更希望可以指出不足之處,謝謝大家:)
三、參考資料
2. https://www.zhihu.com/question/24640264/answer/80500016
3. http://docs.oracle.com/javase/specs/jls/se8/jls8.pdf
4. 《Java核心技術(卷一)》