最近一直在學習編譯原理,然後就了解到了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
- 上傳至maven中心倉庫,友善大家沖KPI,嘿嘿嘿。
- 完善API,雖然抄了fastjson的api,但确實沒抄全。
- 完善類型,json規範裡其實是支援null, boolean, 數字類型的,我這圖簡單都用了String類型。
- 完善Excption,目前如果抛Exception都是抛的antlr的,會對使用者有誤導作用。
- 增加控制随機自旋的API,性能控制交于使用者。
實際上列Todo是為了讓slowjson看起來像個項目,至于做不做就随緣了,畢竟不完美才是slowjson最大的特點。。。。
最後所有源碼已上傳至github
slowjson,歡迎star。