自定义一个JSON解析库

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

一个JSON解析器实际上就是一个方法,它的输入是一个表示JSON的字符串,输出是结构化的对应到语言本身的数据结构。

一般来说,解析过程包括词法分析和语法分析两个阶段。词法分析阶段的目标是按照构词规则将 JSON字符串解析成 Token 流,比如有如下的 JSON 字符串:

{
    "key":"value",
     ...
}
复制代码

经过词法分析后,得到一组 Token,如下:

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

{
    "key","value"
}
复制代码

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

1 JSON解析分析小结

  1. 通过词法分析是将字符串解析成一组 Token 序列
  2. 然后通过语法分析检查输入的 Token 序列所构成的 JSON 格式是否合法

2 词法分析

按照“构词规则”将 JSON 字符串解析成 Token 流。请注意双引号引起来的词–构词规则,所谓构词规则是指词法分析模块在将字符串解析成 Token 时所参考的规则。在 JSON 中,构词规则对应于几种数据类型,当词法解析器读入某个词,且这个词类型符合 JSON 所规定的数据类型时,词法分析器认为这个词符合构词规则,就会生成相应的 Token。这里我们可以参考www.json.org/对 JSON 的定义,罗列一下 JSON 所规定的数据类型:

BEGIN_OBJECT {
END_OBJECT }
BEGIN_ARRAY [
END_ARRAY ]
NULL null
NUMBER 数字
STRING 字符串
BOOLEAN true/false
SEP_COLON :
SEP_COMMA ,
END_DOCUMENT 文档结束

当词法分析器读取的词是上面类型中的一种时,即可将其解析成一个 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);

    private int code;

    TokenType(int code) {
        this.code = code;
    }

    public int getTokenCode() {
        return code;
    }
}
复制代码

在解析过程中,仅有 TokenType 类型还不行。我们除了要将某个词的类型保存起来,还需要保存这个词的字面量。所以,所以这里还需要定义一个 Token 类。用于封装词类型和字面量,如下:

public class Token {
    private TokenType tokenType;
    private String value;

    public Token(TokenType tokenType, String value) {
        this.tokenType = tokenType;
        this.value = value;
    }

    public TokenType getTokenType() {
        return tokenType;
    }

    public void setTokenType(TokenType tokenType) {
        this.tokenType = tokenType;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Token{" +
                "tokenType=" + tokenType +
                ", value='" + value + ''' +
                '}';
    }
}
复制代码

定义好了 Token 类,接下来再来定义一个读取字符串的类:

public class CharReader {
    private static final int BUFFER_SIZE = 1024;
    private final Reader reader;
    private final char[] buffer;
    private int pos;
    private int size;

    public CharReader(Reader reader) {
        this.reader = reader;
        buffer = new char[BUFFER_SIZE];
    }

    /**
     * 返回pos下标处的字符
     *
     * @return 字符
     */
    public char peek() {
        if (pos - 1 >= size) {
            //越界 返回-1
            return (char) -1;
        }

        //用Math.max()确保索引不会为负值
        return buffer[Math.max(0, pos - 1)];
    }

    /**
     * 返回pos下标处的字符 并将pos+1
     *
     * @return 字符
     */
    public char next() {
        if (!hasMore()) {
            return (char) -1;
        }

        return buffer[pos++];
    }

    public void back() {
        pos = Math.max(0, --pos);
    }

    private boolean hasMore() {
        if (pos < size) {
            return true;
        }

        fillBuffer();
        return pos < size;
    }

    private void fillBuffer() {
        try {
            int n = reader.read(buffer);
            if (n == -1) {
                return;
            }
            pos = 0;
            size = n;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
复制代码

有了 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;
            }
        }
        /**
         * n ➔ null
         * t ➔ true
         * f ➔ false
         * " ➔ string
         * 0-9/- ➔ number
         * [ ➔ array
         * { ➔ object
         */
        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 boolean isWhiteSpace(char ch) {
        return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
    }

    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);
                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 == '"') {
                return new Token(TokenType.STRING, sb.toString());
            } else if (ch == '\r' || ch == '\n') {
                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'));
    }

    private Token readNumber() throws IOException {
        char ch = charReader.peek();
        StringBuilder sb = new StringBuilder();
        if (ch == '-') {    // 处理负数
            sb.append(ch);
            ch = charReader.next();
            if (ch == '0') {    // 处理 -0.xxxx
                sb.append(ch);
                sb.append(readFracAndExp());
            } else if (isDigitOne2Nine(ch)) {
                do {
                    sb.append(ch);
                    ch = charReader.next();
                } while (isDigit(ch));
                if (ch != (char) -1) {
                    charReader.back();
                    sb.append(readFracAndExp());
                }
            } else {
                throw new JsonParseException("Invalid minus number");
            }
        } else if (ch == '0') {    // 处理小数
            sb.append(ch);
            sb.append(readFracAndExp());
        } else {
            do {
                sb.append(ch);
                ch = charReader.next();
            } while (isDigit(ch));
            if (ch != (char) -1) {
                charReader.back();
                sb.append(readFracAndExp());
            }
        }

        return new Token(TokenType.NUMBER, sb.toString());
    }

    private boolean isExp(char ch) throws IOException {
        return ch == 'e' || ch == 'E';
    }

    private boolean isDigit(char ch) {
        return ch >= '0' && ch <= '9';
    }

    private boolean isDigitOne2Nine(char ch) {
        return ch >= '0' && ch <= '9';
    }

    private String readFracAndExp() throws IOException {
        StringBuilder sb = new StringBuilder();
        char ch = charReader.next();
        if (ch == '.') {
            sb.append(ch);
            ch = charReader.next();
            if (!isDigit(ch)) {
                throw new JsonParseException("Invalid frac");
            }
            do {
                sb.append(ch);
                ch = charReader.next();
            } while (isDigit(ch));

            if (isExp(ch)) {    // 处理科学计数法
                sb.append(ch);
                sb.append(readExp());
            } else {
                if (ch != (char) -1) {
                    charReader.back();
                }
            }
        } else if (isExp(ch)) {
            sb.append(ch);
            sb.append(readExp());
        } else {
            charReader.back();
        }

        return sb.toString();
    }

    private String readExp() throws IOException {
        StringBuilder sb = new StringBuilder();
        char ch = charReader.next();
        if (ch == '+' || ch == '-') {
            sb.append(ch);
            ch = charReader.next();
            if (isDigit(ch)) {
                do {
                    sb.append(ch);
                    ch = charReader.next();
                } while (isDigit(ch));

                if (ch != (char) -1) {    // 读取结束,不用回退
                    charReader.back();
                }
            } else {
                throw new JsonParseException("e or E");
            }
        } else {
            throw new JsonParseException("e or E");
        }

        return sb.toString();
    }

    private Token readBoolean() throws IOException {
        if (charReader.peek() == 't') {
            if (!(charReader.next() == 'r' && charReader.next() == 'u' && charReader.next() == 'e')) {
                throw new JsonParseException("Invalid json string");
            }

            return new Token(TokenType.BOOLEAN, "true");
        } else {
            if (!(charReader.next() == 'a' && charReader.next() == 'l'
                    && charReader.next() == 's' && charReader.next() == 'e')) {
                throw new JsonParseException("Invalid json string");
            }

            return new Token(TokenType.BOOLEAN, "false");
        }
    }

    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");
    }
}
复制代码

核心方法 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,否则报异常。

接下来看看 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);
                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 == '"') {
                return new Token(TokenType.STRING, sb.toString());
            } else if (ch == '\r' || ch == '\n') {
                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。

3 语法分析

当词法分析结束后,且分析过程中没有抛出错误,那么接下来就可以进行语法分析了。语法分析过程以词法分析阶段解析出的 Token 序列作为输入,输出 JSON Object 或 JSON Array。

当词法分析结束后,且分析过程中没有抛出错误,那么接下来就可以进行语法分析了。语法分析过程以词法分析阶段解析出的 Token 序列作为输入,输出 JSON Object 或 JSON Array。语法分析器的实现的文法如下:

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-
复制代码

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

代码如下:

public class JsonObject {
    private Map<String, Object> 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<Map.Entry<String, Object>> 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 JsonTypeException("Invalid key");
        }
        
        Object obj = map.get(key);
        if (!(obj instanceof JsonArray)) {
            throw new JsonTypeException("Type of value is not JsonObject");
        }
        
        return (JsonArray) obj;
    }
    
    @Override
    public String toString() {
        return JsonUtils.beautify(this);
    }
}
复制代码
public class JsonArray {
    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;
    }

    @Override
    public String toString() {
        return JsonUtils.beautify(this);
    }

    public Iterator iterator() {
        return list.iterator();
    }
}
复制代码

语法解析器的核心逻辑封装在了 parseJsonObject 和 parseJsonArray 两个方法中:

public class Parser {
    private static final int BEGIN_OBJECT_TOKEN = 1;
    private static final int END_OBJECT_TOKEN = 2;
    private static final int BEGIN_ARRAY_TOKEN = 4;
    private static final int END_ARRAY_TOKEN = 8;
    private static final int NULL_TOKEN = 16;
    private static final int NUMBER_TOKEN = 32;
    private static final int STRING_TOKEN = 64;
    private static final int BOOLEAN_TOKEN = 128;
    private static final int SEP_COLON_TOKEN = 256;
    private static final int SEP_COMMA_TOKEN = 512;

    private TokenList tokens;

    public Object parse(TokenList tokens) {
        this.tokens = tokens;
        return parse();
    }

    private Object parse() {
        Token token = tokens.next();
        if (token == null) {
            return new JsonObject();
        } else if (token.getTokenType() == TokenType.BEGIN_OBJECT) {
            return parseJsonObject();
        } else if (token.getTokenType() == TokenType.BEGIN_ARRAY) {
            return parseJsonArray();
        } else {
            throw new JsonParseException("Parse error, invalid Token.");
        }
    }

    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 < Integer.MIN_VALUE) {
                            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 JsonArray parseJsonArray() {
        int expectToken = BEGIN_ARRAY_TOKEN | END_ARRAY_TOKEN | BEGIN_OBJECT_TOKEN | NULL_TOKEN
                | NUMBER_TOKEN | BOOLEAN_TOKEN | STRING_TOKEN;
        JsonArray jsonArray = new JsonArray();
        while (tokens.hasMore()) {
            Token token = tokens.next();
            TokenType tokenType = token.getTokenType();
            String tokenValue = token.getValue();
            switch (tokenType) {
                case BEGIN_OBJECT:
                    checkExpectToken(tokenType, expectToken);
                    jsonArray.add(parseJsonObject());
                    expectToken = SEP_COMMA_TOKEN | END_ARRAY_TOKEN;
                    break;
                case BEGIN_ARRAY:
                    checkExpectToken(tokenType, expectToken);
                    jsonArray.add(parseJsonArray());
                    expectToken = SEP_COMMA_TOKEN | END_ARRAY_TOKEN;
                    break;
                case END_ARRAY:
                    checkExpectToken(tokenType, expectToken);
                    return jsonArray;
                case NULL:
                    checkExpectToken(tokenType, expectToken);
                    jsonArray.add(null);
                    expectToken = SEP_COMMA_TOKEN | END_ARRAY_TOKEN;
                    break;
                case NUMBER:
                    checkExpectToken(tokenType, expectToken);
                    if (tokenValue.contains(".") || tokenValue.contains("e") || tokenValue.contains("E")) {
                        jsonArray.add(Double.valueOf(tokenValue));
                    } else {
                        Long num = Long.valueOf(tokenValue);
                        if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE) {
                            jsonArray.add(num);
                        } else {
                            jsonArray.add(num.intValue());
                        }
                    }
                    expectToken = SEP_COMMA_TOKEN | END_ARRAY_TOKEN;
                    break;
                case BOOLEAN:
                    checkExpectToken(tokenType, expectToken);
                    jsonArray.add(Boolean.valueOf(tokenValue));
                    expectToken = SEP_COMMA_TOKEN | END_ARRAY_TOKEN;
                    break;
                case STRING:
                    checkExpectToken(tokenType, expectToken);
                    jsonArray.add(tokenValue);
                    expectToken = SEP_COMMA_TOKEN | END_ARRAY_TOKEN;
                    break;
                case SEP_COMMA:
                    checkExpectToken(tokenType, expectToken);
                    expectToken = STRING_TOKEN | NULL_TOKEN | NUMBER_TOKEN | BOOLEAN_TOKEN
                            | BEGIN_ARRAY_TOKEN | BEGIN_OBJECT_TOKEN;
                    break;
                case END_DOCUMENT:
                    checkExpectToken(tokenType, expectToken);
                    return jsonArray;
                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 序列解析结束或者抛出异常退出。

上面的解析流程虽然不是很复杂,但在具体实现的过程中,还是需要注意一些细节问题。比如:

  1. 在 JSON 中,字符串既可以作为键,也可以作为值。作为键时,语法分析器期待下一个 Token 类型为 SEP_COLON。而作为值时,则期待下一个 Token 类型为 SEP_COMMA 或 END_OBJECT。所以这里要判断该字符串是作为键还是作为值,判断方法也比较简单,即判断上一个 Token 的类型即可。如果上一个 Token 是 SEP_COLON,即:,那么此处的字符串只能作为值了。否则,则只能
    做为键。
  2. 对于整数类型的 Token 进行解析时,简单点处理,可以直接将该整数解析成 Long 类型。但考虑到空间占用问题,对于 [Integer.MIN_VALUE, Integer.MAX_VALUE] 范围内的整数来说,解析成Integer 更为合适,所以解析的过程中也需要注意一下。

其他类:

public class JsonUtils {

    private static final char SPACE_CHAR = ' ';

    private static final int INDENT_SIZE = 2;

    private static int callDepth = 0;

    public static String beautify(JsonObject jsonObject) {

        StringBuilder sb = new StringBuilder();
        sb.append(getIndentString());
        sb.append("{");
        callDepth++;

        List<Map.Entry<String, Object>> keyValues = jsonObject.getAllKeyValue();
        int size = keyValues.size();
        for (int i = 0; i < size; i++) {
            Map.Entry<String, Object> keyValue = keyValues.get(i);

            String key = keyValue.getKey();
            Object value = keyValue.getValue();

            sb.append("\n");
            sb.append(getIndentString());
            sb.append(""");
            sb.append(key);
            sb.append(""");
            sb.append(": ");

            if (value instanceof JsonObject) {
                sb.append("\n");
                sb.append(beautify((JsonObject) value));
            } else if (value instanceof JsonArray){
                sb.append("\n");
                sb.append(beautify((JsonArray) value));
            } else if (value instanceof String) {
                sb.append(""");
                sb.append(value);
                sb.append(""");
            } else {
                sb.append(value);
            }

            if (i < size - 1) {
                sb.append(",");
            }
        }

        callDepth--;
        sb.append("\n");
        sb.append(getIndentString());
        sb.append("}");

        return sb.toString();
    }

    public static String beautify(JsonArray jsonArray) {
        StringBuilder sb = new StringBuilder();
        sb.append(getIndentString());
        sb.append("[");
        callDepth++;

        int size = jsonArray.size();
        for (int i = 0; i < size; i++) {

            sb.append("\n");

            Object ele = jsonArray.get(i);
            if (ele instanceof JsonObject) {
                sb.append(beautify((JsonObject) ele));
            } else if (ele instanceof JsonArray) {
                sb.append(beautify((JsonArray) ele));
            } else if (ele instanceof String) {
                sb.append(getIndentString());
                sb.append(""");
                sb.append(ele);
                sb.append(""");
            } else {
                sb.append(getIndentString());
                sb.append(ele);
            }

            if (i < size - 1) {
                sb.append(",");
            }
        }

        callDepth--;
        sb.append("\n");
        sb.append(getIndentString());
        sb.append("]");

        return sb.toString();
    }

    private static String getIndentString() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < callDepth * INDENT_SIZE; i++) {
            sb.append(SPACE_CHAR);
        }

        return sb.toString();
    }
}
复制代码
public class JSONParser {
    /**
     * 词法分析
     */
    private Tokenizer tokenizer = new Tokenizer();

    /**
     * 语法解析
     */
    private Parser parser = new Parser();

    public Object fromJSON(String json) throws IOException {
        CharReader charReader = new CharReader(new StringReader(json));
        TokenList tokens = tokenizer.tokenize(charReader);
        return parser.parse(tokens);
    }
}
复制代码

关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。

猜你喜欢

转载自juejin.im/post/7101699238878248967