前言
現(xiàn)有一個(gè)需求需要實(shí)現(xiàn)SLS那樣的加工DSL語(yǔ)句,前面有一篇文章介紹了JavaCC,同樣還存在一個(gè)類似的工具叫ANTLR(ANother Tool for Language Recognition)也是一個(gè)非常強(qiáng)大的詞法和語(yǔ)法解析器代碼生成器,當(dāng)前大版本為ANTLR4。整體思路基本上和JavaCC一致,相比于JavaCC而言ANTLR4提供了較簡(jiǎn)單強(qiáng)大的語(yǔ)法文件調(diào)試功能,AST樹的遍歷方法,還有多語(yǔ)言支持(java,golang,c++,python等)。ANTLR4也被各種強(qiáng)大的中間件使用,比如Groovy,SparkSQL,Presto,HIVE,debezium等。
整體邏輯

一些細(xì)節(jié)
1,用戶編寫xx.g4語(yǔ)法文件。
2,使用Antlr4生成詞法解析器和語(yǔ)法解析器。
3,用戶輸入待解析的文本,詞法解析器將待解析的文本內(nèi)容轉(zhuǎn)換成Tokens流,并過(guò)濾一些沒(méi)用的字符串。
4,詞法解析器將Tokens轉(zhuǎn)換成AST樹。
5,Antlr4提供了兩種遍歷AST樹的方法,Listener和Visitor。
基礎(chǔ)知識(shí)
ANTLR4 xx.g4語(yǔ)法文件
antlr4 grammars-v4中提供了大量g4的例子,基本上有需求照著修改即可。一般如果不是很復(fù)雜的規(guī)則文法一個(gè)g4文件即可,比如lucene的LuceneLexer.g4,如果是比較復(fù)雜的規(guī)則文法則一般放兩個(gè)g4文件,比如MYSQL的規(guī)則文法可描述為MySqlLexer.g4和MySqlParser.g4。模板如下。
/** Optional javadoc style comment */
/** 詳情可以查看官方文檔https://github.com/antlr/antlr4/blob/master/doc/grammars.md */
grammar Name;
options {...}
import ... ;
tokens {...}
channels {...} // lexer only
@actionName {...}
rule1 // parser and lexer rules, possibly intermingled
...
ruleN
g4文件一些細(xì)節(jié)
0,g4文件中的關(guān)鍵字import, fragment, lexer, parser, grammar, returns,locals, throws, catch, finally, mode, options, tokens。
1,grammar 名稱必須和文件名要一致。
2,語(yǔ)法規(guī)則(Parser)以小寫字母開始,詞法文件(Lexer)以大寫字母開始,一般全部大寫,所有的Lexer規(guī)則無(wú)論寫在哪里都會(huì)被重排到Parser規(guī)則之后。
3,同理JavaCC的匹配沖突,先出現(xiàn)的規(guī)則優(yōu)先匹配。
4,g4代碼中注釋使用和java一致,比如/** block comment /和// line comment。
5,anltr4默認(rèn)使用<assoc=left>左結(jié)合,可以手動(dòng)指定<assoc=right>右結(jié)合,anltr4默認(rèn)對(duì)一些常用操作符做了特殊處理比如加減乘除等,這些就不需要再特殊處理。
6,fragment關(guān)鍵字可以給 Lexer 規(guī)則中的公共部分命名。
7,詞法和語(yǔ)法規(guī)則均以分號(hào) ';' 終結(jié)。
8,產(chǎn)生式后面 # label 可以給某條產(chǎn)生式命名,在生成的代碼中即可根據(jù)標(biāo)簽分辨不同產(chǎn)生式。
9,用 'string' 單引號(hào)引出字符串,| 用于分隔兩個(gè)產(chǎn)生式,(a|b) 括號(hào)用于指定子產(chǎn)生式,?+用法同正則表達(dá)式。
10,符號(hào)#表示替代標(biāo)簽,可以作為變量使用,注意和產(chǎn)生式后的#符號(hào)區(qū)分。
11,動(dòng)作Action,@header設(shè)置生成的代碼的package信息,@members可以定義額外的一些變量到Antlr4語(yǔ)法文件中。
12,options主要是是設(shè)置語(yǔ)法生成的一些規(guī)則,比如設(shè)置生成的目標(biāo)語(yǔ)言,編碼等。
ANTLR4語(yǔ)法模式
ANTLR4主要有4中語(yǔ)法模式來(lái)定義語(yǔ)法規(guī)則。序列模式(普通序列模式,帶終止符序列模式),選擇模式,詞法符號(hào)依賴模式,嵌套結(jié)構(gòu)模式。
# 序列模式
一系列元素,它是一個(gè)任意長(zhǎng)的,可能為空的序列,其中的元素可以是詞法符號(hào)或者子規(guī)則。序列模式的例子包括變量聲明和整數(shù)序列等等。
比如:
'[' NUMBER+ ']'
## 帶終止符序列模式
比如:
(script ';')* //語(yǔ)句集合
(text '\n')* //多行數(shù)據(jù)
## 帶分隔符的序列模式
比如:
params: expression ( ',' expression )*;
# 選擇模式
使用|來(lái)分隔同一個(gè)語(yǔ)言規(guī)則的若干備選分支。
比如:
expr:NUMBER | TEXT;
# 詞法符號(hào)依賴模式
一個(gè)詞法符號(hào)需要和某處的另外一個(gè)詞法符號(hào)配對(duì)。這樣的例子包括配對(duì)的圓括號(hào)(),花括號(hào){},方括號(hào)[]和尖括號(hào)<>。
比如:
tarray: '[' NUMBER+ ']' ; //[11,22,33]
# 嵌套結(jié)構(gòu)
自相似的語(yǔ)言結(jié)構(gòu),表達(dá)式。一般用作最頂層的詞法分析器的入口。類似java的內(nèi)部類,嵌套的代碼塊。
比如:
expression: ID '(' expression ')' #funcDef
| '(' expression ')' #funcExp
| function #funcBase
| bool #funcBool
| TEXT #text
| NUMBER #number
;
ANTLR4生成文件
<Grammar>Lexer.java: Lexer
<Grammar>Parser.java: Parser
<Grammar>Listener.java: Listener 接口
<Grammar>BaseListener.java: Listener 默認(rèn)實(shí)現(xiàn)
<Grammar>Visitor.java: Visitor 接口
<Grammar>BaseVisitor.java: Visitor 默認(rèn)實(shí)現(xiàn)
<Grammar>[Lexer].tokens: 當(dāng)語(yǔ)法被拆分成多個(gè)多個(gè)文件時(shí)用于同步編號(hào)
ANTLR4 Listener和Visitor兩種樹遍歷方式對(duì)比
1,Listener由ParseTreeWalker對(duì)象自動(dòng)調(diào)用遍歷所有節(jié)點(diǎn)。Visitor為訪問(wèn)者模式,樹的遍歷由自己自動(dòng)手動(dòng)控制,子節(jié)點(diǎn)需要主動(dòng)調(diào)用visit()訪問(wèn),否則某寫子節(jié)點(diǎn)則不會(huì)被調(diào)用。
2,Listener模式不能返回值,因此一般使用隊(duì)列或者棧保存中間結(jié)果,而Visitor模式可以返回任何自定義類型。
3,如果要實(shí)現(xiàn)樹上的解釋器,則使用Visitor是最好的,比如函數(shù)調(diào)用 print(concat("Hello ", "World"))" ,這邊只需要只執(zhí)行concat函數(shù),如果使用Visitor則非常方便,而Listener的ParseTreeWalker則會(huì)一直按順序遍歷,就不方便處理。
4,Listener在訪問(wèn)所有節(jié)點(diǎn)的時(shí)候,會(huì)依次觸發(fā)進(jìn)入時(shí)(enterXXX方法)和退出時(shí)(exitXXX方法),且都沒(méi)有返回值。
ANTLR4 Idea插件安裝
在JavaCC的時(shí)候想判斷xx.jj文件是否能正確解析我的語(yǔ)句非常麻煩,但是Antlr4則提供了一個(gè)ANTLR Preview工具非常好用。官方安裝文檔。
1,依次打開IDEA Settings -> Plugins。
2,輸入antlr搜索。

3,安裝,重啟IDEA。
4,檢查,簡(jiǎn)單寫一個(gè)g4文件,出現(xiàn)如下表示安裝成功。

寫個(gè)DEMO
現(xiàn)有個(gè)數(shù)據(jù)加工語(yǔ)句的DSL需求,類似與SLS加工函數(shù)語(yǔ)法,這邊實(shí)現(xiàn)一個(gè)Demo,使用Visitor遍歷方式簡(jiǎn)單實(shí)現(xiàn)解析函數(shù)表達(dá)式" str_join('##',str_lowercase('aBc5'),'d',1,true) "。如果是入門例子可以查看官網(wǎng)的calculator.g4例子。
編寫FuncBase.g4文件
//--文法名字必須和文件名相同
grammar FuncBase;
//--設(shè)置包名信息
@header { package com.xxx.demo2.antlr4.a5; }
//--語(yǔ)法分析器起點(diǎn),表示可以輸入methodExec,TEXT,NUMBER,BOOL匹配規(guī)則
expression: methodExec
| TEXT
| NUMBER
| BOOL
;
//--函數(shù)定義語(yǔ)法
methodExec
: methodName '(' methodExecArguments? ')'
;
//--函數(shù)名語(yǔ)法
methodName
: ID
;
//--函數(shù)參數(shù),正則語(yǔ)法表示使用逗號(hào)分割,可以輸入多個(gè)參數(shù)
methodExecArguments
: expression ( ',' expression )*
;
//--bool數(shù)據(jù)類型詞法定義
BOOL
: TRUE
| FALSE
;
//--函數(shù)名詞法定義
ID : [a-zA-Z_] [a-zA-Z0-9_]*;
//--true詞法定義
TRUE : 'true';
//--false詞法定義
FALSE : 'false';
//--數(shù)值詞法定義
NUMBER : '-'?( [0-9]* '.' )? [0-9]+;
//--字符串文本定義
TEXT : ('"'|'\'') ~[\r\n']* ('"'|'\'');
//--忽略字符
WS : [\t\r\n]+ -> skip;
調(diào)試xx.g4語(yǔ)法文件
輸入函數(shù)語(yǔ)法" str_join('##',str_lowercase('aBc5'),'d',1,true) ",可以看到左側(cè)的函數(shù)表達(dá)式翻譯成了右側(cè)的AST語(yǔ)法樹。

如果輸入的語(yǔ)法有錯(cuò),則左邊的AST樹也會(huì)提示

生成分析器代碼
生成代碼有多種方式,因?yàn)槲疫@邊IDEA已經(jīng)裝了Antlr插件,直接生成即可,其他方式可以查看官網(wǎng)文檔(Runtime Libraries and Code Generation Targets),還包括其他語(yǔ)言的生成方式。

Visitor遍歷樹
使用Visitor遍歷樹,并執(zhí)行函數(shù)返回相應(yīng)的結(jié)果,這里只是一個(gè)demo,如果是正式業(yè)務(wù)開發(fā),一般為了避免頻繁修復(fù)g4文件,不會(huì)直接將函數(shù)定義到g4中,而是定義一個(gè)抽象的函數(shù)模型,然后再代碼中做相關(guān)的業(yè)務(wù)處理,然后遍歷函數(shù)列表和參數(shù)列表,做相關(guān)的函數(shù)執(zhí)行器邏輯,就像我下面代碼一樣。
public class MyFuncBaseVisitor extends FuncBaseBaseVisitor<Object> {
@Override
public Object visitExpression(FuncBaseParser.ExpressionContext ctx) {
if (null != ctx.methodExec()) {
return visitMethodExec(ctx.methodExec());
} else if (null != ctx.BOOL()) {
return Boolean.parseBoolean(ctx.BOOL().getText());
} else if (null != ctx.NUMBER()) {
return Double.parseDouble(ctx.NUMBER().getText());
} else {
return ctx.TEXT().getText();
}
}
@Override
public Object visitMethodExec(FuncBaseParser.MethodExecContext ctx) {
// --獲得函數(shù)名
String methodName = ctx.methodName().getText();
// --獲得參數(shù)信息
List<String> argList = ctx.methodExecArguments().expression().stream().map(expressionContext -> {
Object o = visitExpression(expressionContext);
if (null == o) {
return null;
}
return o.toString().replaceAll("'", "");
}).collect(Collectors.toList());
// --函數(shù)處理,這里只是demo,一般情況下需要使用不同放入函數(shù)執(zhí)行邏輯器來(lái)處理相應(yīng)的功能
if ("str_join".equals(methodName)) {
// --System.out.println("str_join,params:" + argList);
String splitChar = argList.get(0);
return String.join(splitChar, argList.subList(1, argList.size()));
} else if ("str_lowercase".equals(methodName)) {
// --System.out.println("str_lowercase,params:" + argList);
StringBuilder strBuffer = new StringBuilder();
for (int i = 0; i < argList.size(); i++) {
strBuffer.append(argList.get(i).toLowerCase());
if (i < argList.size() - 1) {
strBuffer.append(",");
}
}
return strBuffer.toString();
} else {
System.err.println("Unknown Method " + methodName);
return null;
}
}
}
測(cè)試解析器
public class MyFuncBaseTest {
public static void main(String[] args) {
// --編寫DSL語(yǔ)句
String exprStr = "str_join('##',str_lowercase('aBc5'),'d',1,true)";
CodePointCharStream codePointCharStream = CharStreams.fromString(exprStr);
// --創(chuàng)建詞法解析器(Lexer)
FuncBaseLexer lexer = new FuncBaseLexer(codePointCharStream);
// --獲得Tokens流
CommonTokenStream tokens = new CommonTokenStream(lexer);
// --創(chuàng)建語(yǔ)法解析器(parser)
FuncBaseParser parser = new FuncBaseParser(tokens);
// --獲得AST解析樹
FuncBaseParser.ExpressionContext parserTree = parser.expression();
// --打印AST解析樹
System.out.println("ParserTree: " + parserTree.toStringTree(parser));
// --使用visitor遍歷樹
MyFuncBaseVisitor myFuncBaseVisitor = new MyFuncBaseVisitor();
Object val = myFuncBaseVisitor.visit(parserTree);
System.out.println("Visitor Result: { " + val + " }");
}
}
返回結(jié)果

其他的函數(shù)可以同理實(shí)現(xiàn)。
總結(jié)
Antlr4和JavaCC思路大體一致。不過(guò)個(gè)人在開發(fā)使用上,感覺(jué)Antlr4更加人性化,尤其是Antlr4語(yǔ)法文件的IDEA調(diào)試和AST樹的遍歷上。
1,大體流程都是將語(yǔ)法文件翻譯詞法解析器(Lexer)和語(yǔ)法分析器(Parser)。
2,Antlr4和JavaCC語(yǔ)法文件除了格式不一樣,思路差不多,詞法器描述主要是使用正則匹配,語(yǔ)法器描述使用EBNF語(yǔ)法。
2,Antlr4提供了多語(yǔ)言(java,C++,Python,golang等)的支持,JavaCC只能翻譯成java的Lexer和Parser。
3,Antlr4 Idea Plugin在調(diào)試語(yǔ)法文件上非常人性化,直觀簡(jiǎn)單。
4,Antlr4提供的Listener和Visitor兩種樹遍歷器非常方便,jjTree使用上則需要一定的功底。
5,一般業(yè)務(wù)場(chǎng)景都是使用Antlr4處理SQL解析,除了大名鼎鼎的Calcite,我們也可以使用Antlr4處理相應(yīng)的SQL解析在執(zhí)行的業(yè)務(wù),就像SparkSQL一樣。