自己實(shí)現(xiàn)JSON解析器 JsonParser

自己實(shí)現(xiàn)json parser,只有一個(gè)類(lèi)费就,不依賴(lài)任何第三方工具。

背景

為什么要實(shí)現(xiàn)json解析器呢川队?在我實(shí)現(xiàn)一個(gè)rpc框架的過(guò)程中力细,注冊(cè)中心部分使用consul,而consul的api是通過(guò)restful http api來(lái)提供的固额,數(shù)據(jù)交互格式為json眠蚂,此時(shí)就需要用到j(luò)son解析工具。

讓我們回顧一下java界較為通用的json處理庫(kù)斗躏,常用的json處理庫(kù)有jackson逝慧,gson,fastjson啄糙,其他還有許多json工具包笛臣,不過(guò)都不流行或已退出歷史舞臺(tái)。

java ee也有一個(gè)相關(guān)的jsr隧饼,jsr 353 json processing api沈堡,定義了通用的json 處理 api,一個(gè)參考實(shí)現(xiàn)是oracle 的glassfish jsonp燕雁。

jackson是最完善的json處理工具诞丽,實(shí)現(xiàn)的功能最多,并且支持jaxb注解拐格,而且也有支持適配 jsr353 json processing api的模塊僧免。。也是我個(gè)人最喜歡的json處理工具包捏浊。其實(shí)fastjson的很多實(shí)現(xiàn)的常量定義都能看到j(luò)axkson的影子懂衩。

本來(lái)決定使用jackson的,一般來(lái)講現(xiàn)在spring是企業(yè)應(yīng)用的標(biāo)配,而spring mvc應(yīng)用通常都會(huì)依賴(lài)jackson的包勃痴。因此依賴(lài)一下jackson的包也可以接受谒所。但后來(lái)又考慮了幾次,覺(jué)得依賴(lài)第三方的包畢竟不美沛申,額外帶來(lái)依賴(lài)總是會(huì)增加復(fù)雜性劣领,對(duì)于基礎(chǔ)組件,除了日志門(mén)面框架這樣與實(shí)現(xiàn)無(wú)關(guān)的包铁材,還是盡量少依賴(lài)其他庫(kù)為妙尖淘。

因此決定自己寫(xiě)一個(gè)json解析工具,考慮了一下覺(jué)得也不是十分復(fù)雜著觉,用遞歸的方式解析json串即可村生。具體實(shí)現(xiàn)思路接下來(lái)分析。

實(shí)現(xiàn)思路

json的結(jié)構(gòu)分析

json的結(jié)構(gòu)包含幾種元素:

  • object(name value pair object)饼丘,此處指狹義的對(duì)象趁桃,名值對(duì)形式。廣義上任何元素都是對(duì)象肄鸽。
  • array卫病,由[]符號(hào)包裹,元素用英文逗號(hào)分隔典徘。
  • 字面類(lèi)型蟀苛,字面類(lèi)型最為簡(jiǎn)單,不能再嵌套
    • number
    • boolean逮诲,true or false
    • null

下面放幾張json.org的圖帜平,以直觀的形式展示json格式。

json object
json object
json object

object 內(nèi)部的value和array內(nèi)部的元素都可以是任意組成類(lèi)型梅鹦,可以存在任意層次的嵌套裆甩。因此用遞歸方式解析比較簡(jiǎn)單。

實(shí)現(xiàn)思路

a) 基本概念

  • <span style="color:red">trim</span> 帘瞭,trim是把一個(gè)字符序列的頭尾的不可見(jiàn)字符去掉淑掌,由于json允許在元素之間存在任意個(gè)tab、換行蝶念、空格抛腕。因此可能有許多地方需要用到trim

b) processObjcet: object解析

  1. 以"{"開(kāi)始媒殉,正確找到對(duì)應(yīng)的結(jié)束的"}"担敌,由于花括號(hào)可能存在多重嵌套,找到正確的結(jié)束符號(hào)是有技巧的廷蓉。記下整個(gè){}區(qū)塊的位置全封。
  2. 脫去頭尾的{}马昙,中間的部分是properties列表,以name:value,...的形式存儲(chǔ)刹悴。解析properties列表行楞。
  3. 標(biāo)記nameStartMark,初始為0土匀,遇到冒號(hào)":"子房,從nameStartMark到冒號(hào)前都為nameToken(需要trim),從冒號(hào)后開(kāi)始尋找nextValue就轧。同樣需要注意(1.)提及的花括號(hào)和中括號(hào)匹配证杭,遇到逗號(hào)或結(jié)束表示value區(qū)塊結(jié)束。(由findNextValue函數(shù)完成)
  4. 移動(dòng)游標(biāo)到找到的value區(qū)塊后妒御,并更新nameStartMark標(biāo)記解愤。
  5. 循環(huán)執(zhí)行(3.)和(4.),直到不再有冒號(hào)乎莉。
  6. <span style="color:red">注意:</span>找到的value區(qū)塊移交給另一個(gè)函數(shù)processValue處理送讲,此處存在遞歸。

c) processArray: array解析

與object類(lèi)似惋啃,但是比object簡(jiǎn)單

  1. 以"["開(kāi)始李茫,正確找到對(duì)應(yīng)的結(jié)束的"]",由于方括號(hào)可能存在多重嵌套肥橙,找到正確的結(jié)束符號(hào)是有技巧的。記下整個(gè)[]區(qū)塊的位置秸侣。
  2. 脫去頭尾的[]存筏,中間的部分是elements列表,以element1,element2...的形式存儲(chǔ)味榛。
  3. 直接循環(huán)執(zhí)行findNextValue即可椭坚,直到結(jié)束
  4. <span style="color:red">注意:</span>同上,找到的value區(qū)塊移交給另一個(gè)函數(shù)processValue處理搏色,此處存在遞歸善茎。

d) processValue: value解析

此處是一個(gè)遞歸操作,value本身可能是一個(gè)字面量频轿,或者是object垂涯,或者是array。

  1. 如果value區(qū)塊以"{"開(kāi)頭航邢,則是object耕赘,移交給processObjcet 做object解析,遞歸操作膳殷。
  2. 如果value區(qū)塊以"["開(kāi)頭操骡,則是array,移交給processArray 做array解析,遞歸操作册招。
  3. 字面量岔激,如string,boolean是掰,number虑鼎,null直接解析。string可能有轉(zhuǎn)義字符冀惭,這個(gè)目前沒(méi)有考慮處理震叙。

e) completeSymbolPair: 尋找匹配的{}[]

由于{}[]都可能存在多重嵌套,因此需要正確的找到一個(gè)開(kāi)始的花括號(hào)對(duì)應(yīng)的結(jié)束符號(hào)散休,方括號(hào)同理媒楼。

這個(gè)可以用這個(gè)原理:符號(hào)一定是成對(duì)出現(xiàn)的。

步驟如下:

  1. 對(duì)于已知的第一個(gè)左符號(hào)戚丸,定義symbolsScore=1划址,index=1
  2. 遍歷后續(xù)的字符限府,遇到左符號(hào)symbolsScore++夺颤,遇到右符號(hào)symbolsScore--
  3. 直到symbolsScore==0胁勺,則找到正確的結(jié)束符世澜。
  4. 左邊開(kāi)始符號(hào)到右邊結(jié)束符號(hào)之間的內(nèi)容就是需要的內(nèi)容。

代碼實(shí)現(xiàn)

方法原型

public class JsonParser {

    private final String json;

    /**
     * 入口方法
     * @return 解析完成的對(duì)象
     */
    public Object parse() {
        CharsRange trimmedJson = newRange(0, json.length()).trim();
        return processValue(trimmedJson);
    }

    private Object processPlainObject(CharsRange range) {}

    private List<Property> processProperties(CharsRange range) {}

    private List<?> processArray(CharsRange range) {}

    /**
     * @param chars
     * @return value segment trimmed.
     */
    private CharsRange findNextValue(CharsRange chars, AtomicInteger readCursor) {}

    private CharsRange completeSymbolPair(CharsRange trimChars, AtomicInteger readCursor, String symbolPair) {}

    private Object processValue(CharsRange valueSegment) {}

    static class Property { final String name, value;}

    class CharsRange { final int start, end;}

}


具體代碼

此處當(dāng)然要放具體的代碼署穗,只有一個(gè)類(lèi)寥裂。

package io.destinyshine.storks.utils.json;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import lombok.extern.slf4j.Slf4j;

/**
 * @author liujianyu
 * @date 2017/10/17
 */
@Slf4j
public class JsonParser {

    private final String json;

    public JsonParser(String json) {
        this.json = json;
    }

    /**
     * 入口方法
     * @return 解析完成的對(duì)象
     */
    public Object parse() {
        CharsRange trimmedJson = newRange(0, json.length()).trim();
        return processValue(trimmedJson);
    }

    private Object processPlainObject(CharsRange range) {
        List<Property> properties = processProperties(newRange(range.start + 1, range.end - 1));
        Map<String, Object> object = new HashMap<>();
        properties.forEach(prop -> object.put(prop.name, prop.value));
        return object;
    }

    private List<Property> processProperties(CharsRange range) {
        List<Property> properties = new ArrayList<>();
        int nameStartMark = range.start;
        for (int i = range.start; i < range.end; i++) {
            char ch = json.charAt(i);
            if (ch == ':') {
                CharsRange nameToken = newRange(nameStartMark, i).trim();
                AtomicInteger readCursor = new AtomicInteger();
                CharsRange valueSegment = findNextValue(newRange(++i, range.end), readCursor);
                i = readCursor.intValue() + 1;
                nameStartMark = i;
                logger.info("nameToken:{},\nvalueSegment:{}", nameToken, valueSegment);
                //TODO::valid nameToken is start and end with '"'
                final String name = newRange(nameToken.start + 1, nameToken.end - 1).toString();
                final Object value = processValue(valueSegment);
                properties.add(Property.of(name, value));
            }
        }
        return properties;
    }

    private List<?> processArray(CharsRange range) {
        return processElements(newRange(range.start + 1, range.end - 1));
    }

    private List<?> processElements(CharsRange range) {
        List<Object> array = new ArrayList<>();
        int elementStartMark = range.start;
        for (int i = range.start; i < range.end; i++) {
            AtomicInteger readCursor = new AtomicInteger();
            CharsRange elementSegment = findNextValue(newRange(elementStartMark, range.end), readCursor);
            Object elementValue = processValue(elementSegment);
            array.add(elementValue);
            i = readCursor.intValue();
            elementStartMark = i + 1;
        }
        return array;
    }

    /**
     * @param chars
     * @return value segment trimmed.
     */
    private CharsRange findNextValue(CharsRange chars, AtomicInteger readCursor) {
        CharsRange trimChars = chars.trimLeft();
        if (trimChars.relativeChar(0) == '{') {
            return completeSymbolPair(trimChars, readCursor, "{}");
        } else if (trimChars.relativeChar(0) == '[') {
            return completeSymbolPair(trimChars, readCursor, "[]");
        } else {
            int i;
            for (i = trimChars.start + 1; i < trimChars.end; i++) {
                char ch = json.charAt(i);
                if (ch == ',') {
                    break;
                }
            }
            readCursor.set(i);
            return newRange(trimChars.start, i).trim();
        }
    }

    private CharsRange completeSymbolPair(CharsRange trimChars, AtomicInteger readCursor, String symbolPair) {
        int leftSymbol = symbolPair.charAt(0);
        int rightSymbol = symbolPair.charAt(1);
        int symbolsScore = 1;
        //nested object
        int i;
        CharsRange valueSegment = null;
        for (i = trimChars.start + 1; i < trimChars.end; i++) {
            char ch = json.charAt(i);
            if (ch == leftSymbol) {
                symbolsScore++;
            } else if (ch == rightSymbol) {
                symbolsScore--;
            }
            if (symbolsScore == 0) {
                valueSegment = newRange(trimChars.start, i + 1);
                break;
            }
        }

        for (; i < trimChars.end; i++) {
            char chx = json.charAt(i);
            if (chx == ',') {
                break;
            }
        }

        readCursor.set(i);
        return valueSegment;
    }

    private Object processValue(CharsRange valueSegment) {
        final Object value;
        if (valueSegment.relativeChar(0) == '"') {
            value = newRange(valueSegment.start + 1, valueSegment.end - 1).toString();
        } else if (valueSegment.relativeChar(0) == '{') {
            value = processPlainObject(valueSegment);
        } else if (valueSegment.relativeChar(0) == '[') {
            value = processArray(valueSegment);
        } else if (valueSegment.equalsString("true")) {
            value = true;
        } else if (valueSegment.equalsString("false")) {
            value = false;
        } else if (valueSegment.equalsString("null")) {
            value = null;
        } else {
            value = Double.parseDouble(valueSegment.toString());
        }
        return value;
    }

    static class Property {
        final String name;
        final Object value;

        Property(String name, Object value) {
            this.name = name;
            this.value = value;
        }

        static Property of(String name, Object value) {
            return new Property(name, value);
        }
    }

    CharsRange newRange(int start, int end) {
        return new CharsRange(start, end);
    }

    class CharsRange {
        final int start;
        final int end;

        CharsRange(int start, int end) {
            this.start = start;
            this.end = end;
        }

        CharsRange trimLeft() {
            int newStart = -1;
            for (int i = start; i < end; i++) {
                if (!Character.isWhitespace(json.charAt(i))) {
                    newStart = i;
                    break;
                }
            }

            if (newStart == -1) {
                throw new IllegalArgumentException("illegal blank string!");
            }
            return newRange(newStart, end);
        }

        CharsRange trimRight() {
            int newEnd = -1;

            for (int i = end - 1; i >= start; i--) {
                if (!Character.isWhitespace(json.charAt(i))) {
                    newEnd = i + 1;
                    break;
                }
            }
            if (newEnd == -1) {
                throw new IllegalArgumentException("illegal blank string!");
            }
            return newRange(start, newEnd);
        }

        CharsRange trim() {
            return this.trimLeft().trimRight();
        }

        char relativeChar(int index) {
            return json.charAt(start + index);
        }

        public boolean equalsString(String str) {
            return json.regionMatches(true, start, str, 0, str.length());
        }

        @Override
        public String toString() {
            return json.subSequence(start, end).toString();
        }
    }

}

功能測(cè)試

junit test

最后當(dāng)然要做測(cè)試,不過(guò)我們這個(gè)東西是個(gè)簡(jiǎn)單的小東西案疲,暫時(shí)不做性能測(cè)試封恰,測(cè)試一下功能即可。
注意:所有用到的資源都在附件里褐啡,下載可直接使用诺舔。

package jsonparse;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;

import io.destinyshine.storks.utils.json.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.junit.Test;

/**
 * @author liujianyu
 * @date 2017/10/17
 */
@Slf4j
public class JsonParserTest {

    @Test
    public void parseComplexObject() throws IOException, URISyntaxException {
        String json = readFile("/json/nested.json");
        logger.info("origin json content:{}", json);
        JsonParser parser = new JsonParser(json);
        Object object = parser.parse();
        logger.info("parsed object:{}", object);
    }

    @Test
    public void parseEmptyObject() throws IOException, URISyntaxException {
        String json = readFile("/json/empty.json");
        logger.info("origin json content:{}", json);
        JsonParser parser = new JsonParser(json);
        Object object = parser.parse();
        logger.info("parsed object:{}", object);
    }

    @Test
    public void parseArray() throws IOException, URISyntaxException {
        String json = readFile("/json/array.json");
        logger.info("origin json content:{}", json);
        JsonParser parser = new JsonParser(json);
        Object object = parser.parse();
        logger.info("parsed object:{}", object);
    }

    private String readFile(String resource) throws URISyntaxException, IOException {
        return FileUtils.readFileToString(
            new File(JsonParserTest.class.getResource(resource).toURI()));
    }

}

測(cè)試結(jié)果

[main] INFO jsonparse.JsonParserTest - parsed array:[{area=12.0, color=green, shape=circle}, {nested={area=12.0, color=green, shape=circle}}]
[main] INFO jsonparse.JsonParserTest - parsed emptyObject:{}
[main] INFO jsonparse.JsonParserTest - parsed complexObject:{parent={address=null, array=[1.0, 3.0, {}], name=jerry, adult=true, age=45.4}, name=tom, adult=false, age=5.0}

結(jié)尾

任何功能,簡(jiǎn)單的實(shí)現(xiàn)總是很容易备畦,但是要做到工程級(jí)別總是很復(fù)雜低飒,一個(gè)完整的JSON解析程序會(huì)包含更多的特性,比如注解支持萍恕、容錯(cuò)性逸嘀、語(yǔ)法錯(cuò)誤提示等。因此我們寫(xiě)這個(gè)東西只是自我學(xué)習(xí)一下允粤,如果真的追求性能和各種特性的支持崭倘,還是要用成熟的工具包翼岁。

還有,我們的程序沒(méi)有處理轉(zhuǎn)義字符司光,不過(guò)這個(gè)處理倒不是很復(fù)雜琅坡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市残家,隨后出現(xiàn)的幾起案子榆俺,更是在濱河造成了極大的恐慌,老刑警劉巖坞淮,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茴晋,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡回窘,警方通過(guò)查閱死者的電腦和手機(jī)诺擅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)啡直,“玉大人烁涌,你說(shuō)我怎么就攤上這事【泼伲” “怎么了撮执?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)舷丹。 經(jīng)常有香客問(wèn)我抒钱,道長(zhǎng),這世上最難降的妖魔是什么颜凯? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任继效,我火速辦了婚禮,結(jié)果婚禮上装获,老公的妹妹穿的比我還像新娘。我一直安慰自己厉颤,他們只是感情好穴豫,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著逼友,像睡著了一般精肃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上帜乞,一...
    開(kāi)封第一講書(shū)人閱讀 52,337評(píng)論 1 310
  • 那天司抱,我揣著相機(jī)與錄音,去河邊找鬼黎烈。 笑死习柠,一個(gè)胖子當(dāng)著我的面吹牛匀谣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播资溃,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼武翎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了溶锭?” 一聲冷哼從身側(cè)響起宝恶,我...
    開(kāi)封第一講書(shū)人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趴捅,沒(méi)想到半個(gè)月后垫毙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拱绑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年综芥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欺栗。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毫痕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迟几,到底是詐尸還是另有隱情消请,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布类腮,位于F島的核電站臊泰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蚜枢。R本人自食惡果不足惜缸逃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厂抽。 院中可真熱鬧需频,春花似錦、人聲如沸筷凤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)藐守。三九已至挪丢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卢厂,已是汗流浹背乾蓬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慎恒,地道東北人任内。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓撵渡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親族奢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姥闭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)越走,斷路器棚品,智...
    卡卡羅2017閱讀 134,702評(píng)論 18 139
  • 很久以前看過(guò)一句話“生活苦于無(wú)常,困于如忱鹊校”铜跑,那時(shí)年輕氣盛,總覺(jué)不以為然骡澈,后來(lái)锅纺,工作,買(mǎi)房肋殴,結(jié)婚囤锉,生子,換工作护锤,…...
    二二書(shū)閱讀 212評(píng)論 0 0
  • 麗蕓是我在健身房認(rèn)識(shí)的健身達(dá)人官地,面容姣好,身材凹凸有致烙懦。矮窮矬的我當(dāng)時(shí)也是懷著對(duì)美麗的無(wú)限向往驱入,一個(gè)激動(dòng)就不知天高...
    文煒閱讀 531評(píng)論 0 0
  • 選擇器 ·簡(jiǎn)單選擇器 標(biāo)簽選擇器 p{color:blue;} 類(lèi)選擇器 .className .special{...
    cooore閱讀 226評(píng)論 0 0