Java大联盟

致力于最高效的Java学习

关注

作者 | 田小波

cnblogs.com/nullllun/p/8358146.html1、背景JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。相对于另一种数据交换格式 XML,JSON 有着诸多优点。比如易读性更好,占用空间更少等。在 web 应用开发领域内,得益于 JavaScript 对 JSON 提供的良好支持,JSON 要比 XML 更受开发人员青睐。所以作为开发人员,如果有兴趣的话,还是应该深入了解一下 JSON 相关的知识。本着探究 JSON 原理的目的,我将会在这篇文章中详细向大家介绍一个简单的JSON解析器的解析流程和实现细节。由于 JSON 本身比较简单,解析起来也并不复杂。所以如果大家感兴趣的话,在看完本文后,不妨自己动手实现一个 JSON 解析器。好了,其他的话就不多说了,接下来让我们移步到重点章节吧。2. JSON 解析器实现原理JSON 解析器从本质上来说就是根据 JSON 文法规则创建的状态机,输入是一个 JSON 字符串,输出是一个 JSON 对象。一般来说,解析过程包括词法分析和语法分析两个阶段。词法分析阶段的目标是按照构词规则将 JSON 字符串解析成 Token 流,比如有如下的 JSON 字符串:

{    "name" : "小明",    "age": 18}

结果词法分析后,得到一组 Token,如下:

{、 name、 :、 小明、 ,、 age、 :、 18、 }

词法分析解析出 Token 序列后,接下来要进行语法分析。语法分析的目的是根据 JSON 文法检查上面 Token 序列所构成的 JSON 结构是否合法。比如 JSON 文法要求非空 JSON 对象以键值对的形式出现,形如 object = {string : value}。如果传入了一个格式错误的字符串,比如

{    "name", "小明"}

那么在语法分析阶段,语法分析器分析完 Token name后,认为它是一个符合规则的 Token,并且认为它是一个键。接下来,语法分析器读取下一个 Token,期望这个 Token 是 :。但当它读取了这个 Token,发现这个 Token 是,,并非其期望的:,于是文法分析器就会报错误。

这里简单总结一下上面两个流程,词法分析是将字符串解析成一组 Token 序列,而语法分析则是检查输入的 Token 序列所构成的 JSON 格式是否合法。这里大家对 JSON 的解析流程有个印象就好,接下来我会详细分析每个流程。2.1 词法分析在本章开始,我说了词法解析的目的,即按照“构词规则”将 JSON 字符串解析成 Token 流。请注意双引号引起来词--构词规则,所谓构词规则是指词法分析模块在将字符串解析成 Token 时所参考的规则。在 JSON 中,构词规则对应于几种数据类型,当词法解析器读入某个词,且这个词类型符合 JSON 所规定的数据类型时,词法分析器认为这个词符合构词规则,就会生成相应的 Token。这里我们可以参考http://www.json.org/对 JSON 的定义,罗列一下 JSON 所规定的数据类型:

  • BEGIN_OBJECT({)

  • END_OBJECT(})

  • BEGIN_ARRAY([)

  • END_ARRAY(])

  • NULL(null)

  • NUMBER(数字)

  • STRING(字符串)

  • BOOLEAN(true/false)

  • SEP_COLON(:)

  • SEP_COMMA(,)

当词法分析器读取的词是上面类型中的一种时,即可将其解析成一个 Token。我们可以定义一个枚举类来表示上面的数据类型,如下:

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             return true;        }

        fillBuffer();        return pos     }

    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 类型的数据解析过程,大家有兴趣的话可以自己看源码,这里就不在说了。2.2 语法分析当词法分析结束后,且分析过程中没有抛出错误,那么接下来就可以进行语法分析了。语法分析过程以词法分析阶段解析出的 Token 序列作为输入,输出 JSON Object 或 JSON Array。语法分析器的实现的文法如下:

object = {} | { members }members = pair | pair , memberspair = string : valuearray = [] | [ elements ]elements = value  | value , elementsvalue = string | number | object | array | true | false | null

语法分析器的实现需要借助两个辅助类,也就是语法分析器的输出类,分别是 JsonObject 和 JsonArray,代码如下:

public class JsonObject {

    private Map map = new HashMap();public void put(String key, Object value) {map.put(key, value);    }public Object get(String key) {return map.get(key);    }public List> 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;    }    @Overridepublic 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;    }    @Overridepublic 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                     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 方法解析流程大致如下:1、读取一个 Token,检查这个 Token 是否是其所期望的类型。2、如果是,更新期望的 Token 类型。否则,抛出异常,并退出。3、重复步骤1和2,直至所有的 Token 都解析完,或出现异常。上面的步骤并不复杂,但有可能不好理解。这里举个例子说明一下,有如下的 Token 序列:

{、 id、 :、 1、 }

parseJsonObject 解析完 { Token 后,接下来它将期待 STRING 类型的 Token 或者 END_OBJECT 类型的 Token 出现。于是 parseJsonObject 读取了一个新的 Token,发现这个 Token 的类型是 STRING 类型,满足期望。于是 parseJsonObject 更新期望Token 类型为 SEL_COLON,即:。如此循环下去,直至 Token 序列解析结束或者抛出异常退出。上面的解析流程虽然不是很复杂,但在具体实现的过程中,还是需要注意一些细节问题。比如:在 JSON 中,字符串既可以作为键,也可以作为值。作为键时,语法分析器期待下一个 Token 类型为 SEP_COLON。而作为值时,则期待下一个 Token 类型为 SEP_COMMA 或 END_OBJECT。所以这里要判断该字符串是作为键还是作为值,判断方法也比较简单,即判断上一个 Token 的类型即可。如果上一个 Token 是 SEP_COLON,即:,那么此处的字符串只能作为值了。否则,则只能做为键。对于整数类型的 Token 进行解析时,简单点处理,可以直接将该整数解析成 Long 类型。但考虑到空间占用问题,对于 [Integer.MIN_VALUE, Integer.MAX_VALUE]范围内的整数来说,解析成 Integer 更为合适,所以解析的过程中也需要注意一下。3. 测试及效果展示为了验证代码的正确性,这里对代码进行了简单的测试。测试数据来自网易音乐,大约有4.5W个字符。为了避免每次下载数据,因数据发生变化而导致测试不通过的问题。我将某一次下载的数据保存在了 music.json 文件中,后面每次测试都会从文件中读取数据。关于测试部分,这里就不贴代码和截图了。大家有兴趣的话,可以自己下载源码测试玩玩。测试就不多说了,接下来看看 JSON 美化效果展示。这里随便模拟点数据,就模拟王者荣耀里的狄仁杰英雄信息吧(对,这个英雄我经常用)。如下图:

关于 JSON 美化的代码这里也不讲解了,并非重点,只算一个彩蛋吧。4. 写作最后到此,本文差不多要结束了。本文对应的代码已经放到了 github 上,需要的话,大家可自行下载。

传送门:https://github.com/code4wt/JSONParser

这里需要声明一下,本文对应的代码实现了一个比较简陋的 JSON 解析器,实现的目的是探究 JSON 的解析原理。JSONParser 只算是一个练习性质的项目,代码实现的并不优美,而且缺乏充足的测试。同时,限于本人的能力(编译原理基础基本可以忽略),我并无法保证本文以及对应的代码中不出现错误。如果大家在阅读代码的过程中,发现了一些错误,或者写的不好的地方,可以提出来,我来修改。如果这些错误对你造成了困扰,这里先说一声很抱歉。最后,本文及实现主要参考了一起写一个JSON解析器和如何编写一个JSON解析器两篇文章及两篇文章对应的实现代码,在这里向着两篇博文的作者表示感谢。

推荐阅读

1、一次性把JVM讲清楚,别再被面试官问住了

2、axios异步请求数据的12种操作

3、一文搞懂前后端分离

4、快速上手Spring Boot+Vue前后端分离

json解析对应的value为null_徒手撸一个JSON解析器相关推荐

  1. spring boot 自动跳转登录页面_徒手撸一个扫码登录示例工程

    徒手撸一个扫码登录示例工程 不知道是不是微信的原因,现在出现扫码登录的场景越来越多了,作为一个有追求.有理想新四好码农,当然得紧跟时代的潮流,得徒手撸一个以儆效尤 本篇示例工程,主要用到以下技术栈 q ...

  2. kjb文件 解析_Python 之父再发文:构建一个 PEG 解析器

    花下猫语: Python 之父在 Medium 上开了博客,现在写了两篇文章,本文是第二篇的译文.前一篇的译文 在此 ,宣布了将要用 PEG 解析器来替换当前的 pgen 解析器. 本文主要介绍了构建 ...

  3. erlang xml 解析_用yecc(erlang)写一个json解析器

    昨天写了个json的解析器.其实yecc早看过了,只是那时对自己要求太高,想一下子写个小语言.然后大脑就陷入混乱... 后来注意力转移了.就不那么急着去开发些难道大的.今天回来一看,觉得都理解了,实践 ...

  4. js websocket同步等待_WebSocket硬核入门:200行代码,教你徒手撸一个WebSocket服务器...

    本文原题"Node.js - 200 多行代码实现 Websocket 协议",为了提升内容品质,有较大修订. 1.引言 最近正在研究 WebSocket 相关的知识,想着如何能自 ...

  5. diff算法_[VUE学习]徒手撸一个虚拟dom+diff算法

    首先先来聊一下,什么是虚拟dom? 为什么要使用虚拟dom? 其实答案很简单,虚拟dom就是位于js层和html层之间的一个层,使用js模拟出一个dom树,然后通过diff算法,来侦测到dom发生更改 ...

  6. 用Python徒手撸一个股票回测框架

    通过纯Python完成股票回测框架的搭建. 什么是回测框架? 无论是传统股票交易还是量化交易,无法避免的一个问题是我们需要检验自己的交易策略是否可行,而最简单的方式就是利用历史数据检验交易策略,而回测 ...

  7. 徒手撸一个简单的IOC

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:不学无数的程序员 juejin.im/post/5c4ac4 ...

  8. 徒手撸一个简单的 IOC

    Spring框架中最经典的两个就是IOC和AOP,其中IOC(Inversion of Control)是什么呢?控制反转,简单来说就是将控制实体Bean的动作交给了Spring容器进行管理.再简单点 ...

  9. 从零开始徒手撸一个vue的toast弹窗组件

    相信普通的vue组件大家都会写,定义 -> 引入 -> 注册 -> 使用,行云流水,一气呵成,但是如果我们今天是要自定义一个弹窗组件呢? 首先,我们来分析一下弹窗组件的特性(需求): ...

最新文章

  1. js解决iframe跨域问题
  2. java二分查找法_java算法之二分查找法的实例详解
  3. 基于OpenCV完成离散傅里叶变换
  4. 微信社交小程序服务器,Day12-微信小程序实战-交友小程序-搭建服务器与上传文件到后端...
  5. JS面向对象的程序设计之创建对象_工厂模式,构造函数模式,原型模式-1
  6. 双榜首!华为云擎天架构刷新进化计算大赛新纪录!
  7. 编写Oracle简单的存储过程
  8. 个位百位AS3实现经典算法(二) 水仙花数
  9. 要连plsql是不是要装oracle,PLSQL直接连接数据库,不装ORACLE
  10. 嵌入式应用软件开发的步骤流程
  11. pmos低电平驱动_详细讲解MOSFET管驱动电路
  12. PHP正则表达式基础必须掌握
  13. 蓝桥杯练习——摔手机测试次数
  14. win7主机 ubuntu10.04虚拟机 共享文件夹设置
  15. 维修技术论坛万能预装系统 v5.0
  16. 罗克韦尔PLC程序,水处理自动化最高程序
  17. LPC23XX CAN波特率的计算
  18. 《许渊冲西南联大求学日记》摘抄
  19. SSI通信C语言编程,SSI协议应用和编程.ppt
  20. python remi库官网_诺禾致源2020:诺禾分享:python的Remi库

热门文章

  1. rip协议中周期性广播路由信息的报文_技术实操||距离矢量路由协议-RIP
  2. 天涯明月刀7月4号服务器维护,7月8日服务器例行维护公告
  3. ES6 iterator 迭代器
  4. 基于AngularJS的Onsen UI --Onsen UI学习笔记
  5. 最大连续子序列----DP动态规划
  6. 题解 P2598 【[ZJOI2009]狼和羊的故事】
  7. 使用SDL打造游戏世界之入门篇 - 2
  8. Atlas 不仅仅是异步
  9. 世界 Web 2.0 网站评奖揭晓
  10. 限制 计算机中 某用户上网 win7,Win7旗舰版怎么限制孩子的上网时间?电脑限制孩子上网时间的方法...