Antlr實戰之JSON解析器slowjson

最近一直在學習編譯原理,然後就瞭解到了antlr4這個強大的工具,antlr的全稱是(Another Tool for Language Recognition),是一款很強大的詞法和語法分析工具,雖然是用java寫成的,但它也能生成c++、go……等語言的代碼。它的主要作用就是你可以用巴科斯範式來描述語法規則,然後它幫你生成對應的解析器。
大家都知道實踐是最好的學習方式,要快速深刻地理解antlr的操作和相關接口就不得不找一個練手的東西。回想到去年連續報安全漏洞的fastjson,所以我準備霍霍一下json解析器。咱寫不出來比fastjson更快、bug更少、更安全的json解析器,難道還寫不出來一個bug更多、更慢、更不安全的解析器嗎,正面拼不贏咱反其道而行。
爲了對標阿里的fastjson,我給它起名 slowjson,源碼已在github slowjson 歡迎star。爲了推廣slowjson,我都想好廣告詞了。

你想升職加薪嗎?
你想拿年終獎嗎?
你想成爲同事眼中的性能優化小能手嗎?
今天用slowjson,年底做性能優化換回fastjson,十倍性能不是夢,升職加薪準能成。

解析JSON字符串

說這麼多進入正題,json解析器該怎麼寫?實際上你並不需要自己動手寫詞法分析器、語法分析器……,今天的主角antlr都會幫你生成,你只需要用巴科斯範式把json的語法規則描述清楚就行了,這份描述你可以直接在json.org找到,在antlr的github代碼庫裏也有,二者看起來稍有差別,json官網的規則更詳細些。這裏我直接用antlr提供的規則描述。

grammar JSON;

json
   : value
   ;

obj
   : '{' pair (',' pair)* '}'
   | '{' '}'
   ;

pair
   : STRING ':' value
   ;

array
   : '[' value (',' value)* ']'
   | '[' ']'
   ;

value
   : STRING
   | NUMBER
   | obj
   | array
   | 'true'
   | 'false'
   | 'null'
   ;


STRING
   : '"' (ESC | SAFECODEPOINT)* '"'
   ;


fragment ESC
   : '\\' (["\\/bfnrt] | UNICODE)
   ;
fragment UNICODE
   : 'u' HEX HEX HEX HEX
   ;
fragment HEX
   : [0-9a-fA-F]
   ;
fragment SAFECODEPOINT
   : ~ ["\\\u0000-\u001F]
   ;


NUMBER
   : '-'? INT ('.' [0-9] +)? EXP?
   ;


fragment INT
   : '0' | [1-9] [0-9]*
   ;

// no leading zeros

fragment EXP
   : [Ee] [+\-]? INT
   ;

// \- since - means "range" inside [...]

WS
   : [ \t\n\r] + -> skip
   ;

把這個文件保存成 JSON.g4,然後執行下面命令,當然前提是你得正確安裝antlr4。

antlr4 JSON.g4  -no-listener -package xyz.xindoo.slowjson

這個時候antlr就會幫你生成json的詞法分析器JSONLexer.java和語法分析器JSONParser.java。

    private static String jsonStr = "{\"key1\":\"value1\",\"sub\":{\"subkey\":\"subvalue1\"}}"; 
    public static JSONParser.ObjContext parse() {
        JSONLexer lexer = new JSONLexer(CharStreams.fromString(jsonStr));
        CommonTokenStream tokens = new CommonTokenStream(lexer);  //生成token 
        JSONParser parser = new JSONParser(tokens);
        JSONParser.ObjContext objCtx = parser.obj(); // 將token轉化爲抽象語法樹(AST) 
        return new objCtx;
    }

實際上你只需要寫上面這麼多代碼,就可以完成對一個jsonStr的解析,不過這裏解析後的結果是antlr內部封裝的抽象語法樹,利用antlr的idea插件,我們可以將解析後的AST可視化出來, "{“key1”:“value1”,“sub”:{“subkey”:“subvalue1”}}"的語法樹長下面這樣。
在這裏插入圖片描述

JSON字符到JSONObject

雖然已經完成了json字符串的解析,但如果你想像fastjson那樣使用,你還得完成對語法樹節點到JSONObject的轉化。antlr根據語法規則,已經自動幫你生成了每個節點類型,實際上你只需要遍歷整個樹,然後把每個節點轉化爲JSONObject或者k-v對就可以了。

package xyz.xindoo.slowjson;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

public class JSONObject {

    private Map<String, Object> map;

    public JSONObject() {
        this.map = new HashMap<>();
    }

    protected JSONObject(JSONParser.ObjContext objCtx) {
        this.map = new HashMap<>();
        for (JSONParser.PairContext pairCtx: objCtx.pair()) {
            String key = pairCtx.STRING().getText();
            map.put(key.substring(1, key.length()-1), pairCtx.value());
        }
    }

    public JSONObject getJSONObject(String key) {
        JSONParser.ValueContext value = (JSONParser.ValueContext)map.get(key);
        if (value == null) {
            return null;
        }
        return new JSONObject(value.obj());
    }

    public String getString(String key) {
        Object value = map.get(key);
        if (value == null) {
            return null;
        }
        if (JSONParser.ValueContext.class.isInstance(value)) {
            JSONParser.ValueContext ctx = (JSONParser.ValueContext)value;
            String newValue = ctx.STRING().getText();
            map.put(key, newValue.substring(1, newValue.length()-1));
        }
        return (String)map.get(key);
    }

    public int getInt(String key) {
        String value = getString(key);
        if (value == null || "".equals(value)) {
            return 0;
        }
        return Integer.parseInt(value);
    }

    public long getLong(String key) {
        String value = getString(key);
        if (value == null || "".equals(value)) {
            return 0L;
        }
        return Long.parseLong(value);
    }

    public double getDouble(String key) {
        String value = getString(key);
        if (value == null || "".equals(value)) {
            return 0.0;
        }
        return Double.parseDouble(value);
    }
    
    public JSONArray getJSONArray(String key) {
        JSONParser.ValueContext value = (JSONParser.ValueContext)map.get(key);
        if (value == null) {
            return null;
        }
        return new JSONArray(value.array());
    }

    public void put(String key, Object object) {
        map.put(key, object);
    }

    public static JSONObject parseObject(String text) {
        JSONLexer lexer = new JSONLexer(CharStreams.fromString(text));
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        JSONParser parser = new JSONParser(tokens);
        JSONParser.ObjContext objCtx = parser.obj();
        return new JSONObject(objCtx);
    }

    public static JSONArray parseArray(String text) {
        if (text == null) {
            return null;
        }
        JSONArray array = JSONArray.parseArray(text);
        return array;
    }
}

代碼中我並沒有遍歷整個AST並將其轉化爲JSONObject,而是等到需要的時候再轉,實現起來比較方便。看到這裏有沒有發現slowjson的API和fastjson的很像! 沒錯,我就是抄的fastjson,而且我還沒抄全。。。

性能測試

接下來做個很隨便的性能測試,我隨便找了個json字符串,並拉來了slowjson的幾個主要競爭對手 fastjson、jackson、gson,測試結果如下:

Benchmark       Mode  Cnt       Score   Error  Units
Test.fastjson  thrpt    2  235628.511          ops/s
Test.gson      thrpt    2  237975.534          ops/s
Test.jackson   thrpt    2  212453.073          ops/s
Test.slowjson  thrpt    2   29905.109          ops/s

性能只差一個數量級,沒我預期的慢……這這麼行呢,加上隨機自旋……

    private static void randomSpin() {
        Random random = new Random();
        int nCPU = Runtime.getRuntime().availableProcessors();
        int spins = (random.nextInt()%8 + nCPU) * SPIN_UNIT;
        while (spins > 0) {
            spins--;
            float a = random.nextFloat();
        }
    }

然後在所有get的方法裏先調用一次隨機自旋,消耗掉cpu。再來測試下性能。

Benchmark       Mode  Cnt       Score   Error  Units
Test.fastjson  thrpt    2  349994.543          ops/s
Test.gson      thrpt    2  318087.884          ops/s
Test.jackson   thrpt    2  244393.573          ops/s
Test.slowjson  thrpt    2    2681.164          ops/s

嗯~ 這次差兩個量級了,達到了我生產環境的性能標準,可以上線了……

JSONObject到JSON字符串

wait wait 橋都麻袋,目前只實現了json字符串到JSONObject的轉換,沒有實現從JSONObject到json字符串的轉化,功能不完整啊。不過這個也簡單,我們按照JSONObject裏對象的層次,遞歸地來做toSting,代碼如下。

    @Override
    public String toString() {
        return toJSONString();
    }

    public String toJSONString() {
        StringBuilder sb = new StringBuilder();
        List<String> list = new ArrayList<>(map.size());
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object object = entry.getValue();
            String value = null;
            if (String.class.isInstance(object)) {
                value = "\"" + object.toString() + "\"";
            } else if (JSONObject.class.isInstance(object)) {
                value = object.toString();
            } else if (JSONArray.class.isInstance(object)) {
                value = object.toString();
            } else {
                value = ((JSONParser.ValueContext)object).getText();
            }
            list.add("\"" + key + "\":" + value);
        }
        sb.append("{");
        sb.append(String.join(",", list));
        sb.append("}");
        return sb.toString();
    }

JSONArray

上面始終沒有提到JSONArray,其實JSONArray也是JSON中重要組成部分,之所以沒提是因爲JSONArray和JSONObject的實現思路是非常相似的,而且簡單多了,我的封裝如下。

package xyz.xindoo.slowjson;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class JSONArray {
    private final List<JSONObject> list;

    public JSONArray() {
        this.list = new ArrayList<>();
    }
    public JSONArray(List<JSONObject> list) {
        this.list = new ArrayList<>(list.size());
        this.list.addAll(list);
    }

    protected JSONArray(JSONParser.ArrayContext arrayCtx) {
        this.list = arrayCtx.value()
                            .stream()
                            .map(valueContext -> new JSONObject(valueContext.obj()))
                            .collect(Collectors.toList());
    }

    public static JSONArray parseArray(String text) {
        JSONLexer lexer = new JSONLexer(CharStreams.fromString(text));
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        JSONParser parser = new JSONParser(tokens);
        JSONParser.ArrayContext arrayCtx = parser.array();
        return new JSONArray(arrayCtx);
    }

    public JSONObject getJSONObject(int index) {
        return list.get(index);
    }

    public void add(JSONObject jsonObject) {
        list.add(jsonObject);
    }

    @Override
    public String toString() {
        return toJSONString();
    }

    public String toJSONString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        List<String> strList = list.stream().map(JSONObject::toString).collect(Collectors.toList());
        sb.append(String.join(",", strList));
        sb.append("]");
        return sb.toString();
    }
}

Todo

  1. 上傳至maven中心倉庫,方便大家衝KPI,嘿嘿嘿。
  2. 完善API,雖然抄了fastjson的api,但確實沒抄全。
  3. 完善類型,json規範裏其實是支持null, boolean, 數字類型的,我這圖簡單都用了String類型。
  4. 完善Excption,目前如果拋Exception都是拋的antlr的,會對用戶有誤導作用。
  5. 增加控制隨機自旋的API,性能控制交於用戶。

實際上列Todo是爲了讓slowjson看起來像個項目,至於做不做就隨緣了,畢竟不完美才是slowjson最大的特點。。。。

最後所有源碼已上傳至github slowjson ,歡迎star。

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