一起寫一個 JSON 解析器


原文地址:http://www.cnblogs.com/absfree/p/5502705.html


【本篇博文會介紹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解析的正確性,業務邏輯上也儘量使用直接的方式,許多地方沒使用更加高效的實現,另外在拋出異常方面也比較隨便,“一言不合”就拋異常...由於個人水平有限,代碼中難免存在謬誤,希望大家多多包涵,更希望可以指出不足之處,謝謝大家:)

 

三、參考資料

1. http://www.liaoxuefeng.com/article/0014211269349633dda29ee3f29413c91fa65c372585f23000?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

2. https://www.zhihu.com/question/24640264/answer/80500016

3. http://docs.oracle.com/javase/specs/jls/se8/jls8.pdf

4. 《Java核心技術(卷一)》



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章