自己实现JSON解析器 JsonParser

自己实现json parser,只有一个类,不依赖任何第三方工具。

背景

为什么要实现json解析器呢?在我实现一个rpc框架的过程中,注册中心部分使用consul,而consul的api是通过restful http api来提供的,数据交互格式为json,此时就需要用到json解析工具。

让我们回顾一下java界较为通用的json处理库,常用的json处理库有jackson,gson,fastjson,其他还有许多json工具包,不过都不流行或已退出历史舞台。

java ee也有一个相关的jsr,jsr 353 json processing api,定义了通用的json 处理 api,一个参考实现是oracle 的glassfish jsonp。

jackson是最完善的json处理工具,实现的功能最多,并且支持jaxb注解,而且也有支持适配 jsr353 json processing api的模块。。也是我个人最喜欢的json处理工具包。其实fastjson的很多实现的常量定义都能看到jaxkson的影子。

本来决定使用jackson的,一般来讲现在spring是企业应用的标配,而spring mvc应用通常都会依赖jackson的包。因此依赖一下jackson的包也可以接受。但后来又考虑了几次,觉得依赖第三方的包毕竟不美,额外带来依赖总是会增加复杂性,对于基础组件,除了日志门面框架这样与实现无关的包,还是尽量少依赖其他库为妙。

因此决定自己写一个json解析工具,考虑了一下觉得也不是十分复杂,用递归的方式解析json串即可。具体实现思路接下来分析。

实现思路

json的结构分析

json的结构包含几种元素:

  • object(name value pair object),此处指狭义的对象,名值对形式。广义上任何元素都是对象。
  • array,由[]符号包裹,元素用英文逗号分隔。
  • 字面类型,字面类型最为简单,不能再嵌套
    • number
    • boolean,true or false
    • null

下面放几张json.org的图,以直观的形式展示json格式。

json object
json object
json object

object 内部的value和array内部的元素都可以是任意组成类型,可以存在任意层次的嵌套。因此用递归方式解析比较简单。

实现思路

a) 基本概念

  • <span style="color:red">trim</span> ,trim是把一个字符序列的头尾的不可见字符去掉,由于json允许在元素之间存在任意个tab、换行、空格。因此可能有许多地方需要用到trim

b) processObjcet: object解析

  1. 以"{"开始,正确找到对应的结束的"}",由于花括号可能存在多重嵌套,找到正确的结束符号是有技巧的。记下整个{}区块的位置。
  2. 脱去头尾的{},中间的部分是properties列表,以name:value,...的形式存储。解析properties列表。
  3. 标记nameStartMark,初始为0,遇到冒号":",从nameStartMark到冒号前都为nameToken(需要trim),从冒号后开始寻找nextValue。同样需要注意(1.)提及的花括号和中括号匹配,遇到逗号或结束表示value区块结束。(由findNextValue函数完成)
  4. 移动游标到找到的value区块后,并更新nameStartMark标记。
  5. 循环执行(3.)和(4.),直到不再有冒号。
  6. <span style="color:red">注意:</span>找到的value区块移交给另一个函数processValue处理,此处存在递归。

c) processArray: array解析

与object类似,但是比object简单

  1. 以"["开始,正确找到对应的结束的"]",由于方括号可能存在多重嵌套,找到正确的结束符号是有技巧的。记下整个[]区块的位置。
  2. 脱去头尾的[],中间的部分是elements列表,以element1,element2...的形式存储。
  3. 直接循环执行findNextValue即可,直到结束
  4. <span style="color:red">注意:</span>同上,找到的value区块移交给另一个函数processValue处理,此处存在递归。

d) processValue: value解析

此处是一个递归操作,value本身可能是一个字面量,或者是object,或者是array。

  1. 如果value区块以"{"开头,则是object,移交给processObjcet 做object解析,递归操作。
  2. 如果value区块以"["开头,则是array,移交给processArray 做array解析,递归操作。
  3. 字面量,如string,boolean,number,null直接解析。string可能有转义字符,这个目前没有考虑处理。

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

由于{}[]都可能存在多重嵌套,因此需要正确的找到一个开始的花括号对应的结束符号,方括号同理。

这个可以用这个原理:符号一定是成对出现的。

步骤如下:

  1. 对于已知的第一个左符号,定义symbolsScore=1index=1
  2. 遍历后续的字符,遇到左符号symbolsScore++,遇到右符号symbolsScore--
  3. 直到symbolsScore==0,则找到正确的结束符。
  4. 左边开始符号到右边结束符号之间的内容就是需要的内容。

代码实现

方法原型

public class JsonParser {

    private final String json;

    /**
     * 入口方法
     * @return 解析完成的对象
     */
    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;}

}


具体代码

此处当然要放具体的代码,只有一个类。

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 解析完成的对象
     */
    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();
        }
    }

}

功能测试

junit test

最后当然要做测试,不过我们这个东西是个简单的小东西,暂时不做性能测试,测试一下功能即可。
注意:所有用到的资源都在附件里,下载可直接使用。

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()));
    }

}

测试结果

[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}

结尾

任何功能,简单的实现总是很容易,但是要做到工程级别总是很复杂,一个完整的JSON解析程序会包含更多的特性,比如注解支持、容错性、语法错误提示等。因此我们写这个东西只是自我学习一下,如果真的追求性能和各种特性的支持,还是要用成熟的工具包。

还有,我们的程序没有处理转义字符,不过这个处理倒不是很复杂。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,673评论 18 139
  • 很久以前看过一句话“生活苦于无常,困于如常”,那时年轻气盛,总觉不以为然,后来,工作,买房,结婚,生子,换工作,…...
    二二书阅读 210评论 0 0
  • 丽芸是我在健身房认识的健身达人,面容姣好,身材凹凸有致。矮穷矬的我当时也是怀着对美丽的无限向往,一个激动就不知天高...
    文炜阅读 530评论 0 0
  • 选择器 ·简单选择器 标签选择器 p{color:blue;} 类选择器 .className .special{...
    cooore阅读 224评论 0 0