SpringBoot集成antlr實(shí)現(xiàn)詞法和語(yǔ)法分析
1.什么是antlr?
Antlr4 是一款強(qiáng)大的語(yǔ)法生成器工具,可用于讀取、處理、執(zhí)行和翻譯結(jié)構(gòu)化的文本或二進(jìn)制文件。基本上是當(dāng)前 Java 語(yǔ)言中使用最為廣泛的語(yǔ)法生成器工具。Twitter搜索使用ANTLR進(jìn)行語(yǔ)法分析,每天處理超過20億次查詢;Hadoop生態(tài)系統(tǒng)中的Hive、Pig、數(shù)據(jù)倉(cāng)庫(kù)和分析系統(tǒng)所使用的語(yǔ)言都用到了ANTLR;Lex Machina將ANTLR用于分析法律文本;Oracle公司在SQL開發(fā)者IDE和遷移工具中使用了ANTLR;NetBeans公司的IDE使用ANTLR來解析C++;Hibernate對(duì)象-關(guān)系映射框架(ORM)使用ANTLR來處理HQL語(yǔ)言
基本概念
語(yǔ)法分析器(parser)是用來識(shí)別語(yǔ)言的程序,本身包含兩個(gè)部分:詞法分析器(lexer)和語(yǔ)法分析器(parser)。詞法分析階段主要解決的關(guān)鍵詞以及各種標(biāo)識(shí)符,例如 INT、ID 等,語(yǔ)法分析主要是基于詞法分析的結(jié)果,構(gòu)造一顆語(yǔ)法分析樹。大致的流程如下圖參考2所示。
因此,為了讓詞法分析和語(yǔ)法分析能夠正常工作,在使用 Antlr4 的時(shí)候,需要定義語(yǔ)法(grammar),這部分就是 Antlr 元語(yǔ)言。
使用 ANTLR4 編程的基本流程是固定的,通常分為如下三步:
基于需求按照 ANTLR4 的規(guī)則編寫自定義語(yǔ)法的語(yǔ)義規(guī)則, 保存成以 g4 為后綴的文件。
使用 ANTLR4 工具處理 g4 文件,生成詞法分析器、句法分析器代碼、詞典文件。
編寫代碼繼承 Visitor 類或?qū)崿F(xiàn) Listener 接口,開發(fā)自己的業(yè)務(wù)邏輯代碼。
Listener 模式和 Visitor 模式的區(qū)別
Listener 模式:
Visitor 模式:
- Listener 模式通過 walker 對(duì)象自行遍歷,不用考慮其語(yǔ)法樹上下級(jí)關(guān)系。Vistor 需要自行控制訪問的子節(jié)點(diǎn),如果遺漏了某個(gè)子節(jié)點(diǎn),那么整個(gè)子節(jié)點(diǎn)都訪問不到了。
- Listener 模式的方法沒有返回值,Vistor 模式可以設(shè)定任意返回值。
- Listener 模式的訪問棧清晰明確,Vistor 模式是方法調(diào)用棧,如果實(shí)現(xiàn)出錯(cuò)有可能導(dǎo)致 StackOverFlow。
2.代碼工程
實(shí)驗(yàn)?zāi)康模簩?shí)現(xiàn)基于antlr的計(jì)算器
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springboot-demo</artifactId> <groupId>com.et</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>ANTLR</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <antlr4.version>4.9.1</antlr4.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.antlr</groupId> <artifactId>antlr4-runtime</artifactId> <version>${antlr4.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>${antlr4.version}</version> <configuration> <sourceDirectory>src/main/java</sourceDirectory> <outputDirectory>src/main/java</outputDirectory> <arguments> <argument>-visitor</argument> <argument>-listener</argument> </arguments> </configuration> <executions> <execution> <goals> <goal>antlr4</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
元語(yǔ)言LabeledExpr.g4
grammar LabeledExpr; // rename to distinguish from Expr.g4 prog: stat+ ; stat: expr NEWLINE # printExpr | ID '=' expr NEWLINE # assign | NEWLINE # blank ; expr: expr op=('*'|'/') expr # MulDiv | expr op=('+'|'-') expr # AddSub | INT # int | ID # id | '(' expr ')' # parens ; MUL : '*' ; // assigns token name to '*' used above in grammar DIV : '/' ; ADD : '+' ; SUB : '-' ; ID : [a-zA-Z]+ ; // match identifiers INT : [0-9]+ ; // match integers NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal) WS : [ \t]+ -> skip ; // toss out whitespace
簡(jiǎn)單解讀一下 LabeledExpr.g4 文件。ANTLR4 規(guī)則是基于正則表達(dá)式定義定義。規(guī)則的理解是自頂向下的,每個(gè)分號(hào)結(jié)束的語(yǔ)句表示一個(gè)規(guī)則 。例如第一行:grammar LabeledExpr; 表示我們的語(yǔ)法名稱是 LabeledExpr, 這個(gè)名字需要跟文件名需要保持一致。Java 編碼也有相似的規(guī)則:類名跟類文件一致。
- 規(guī)則 prog 表示 prog 是一個(gè)或多個(gè) stat。
- 規(guī)則 stat 適配三種子規(guī)則:空行、表達(dá)式 expr、賦值表達(dá)式 ID’=’expr。
- 表達(dá)式 expr 適配五種子規(guī)則:乘除法、加減法、整型、ID、括號(hào)表達(dá)式。很顯然,這是一個(gè)遞歸的定義。
最后定義的是組成復(fù)合規(guī)則的基礎(chǔ)元素,比如:規(guī)則 **ID: [a-zA-Z]+**表示 ID 限于大小寫英文字符串;INT: [0-9]+; 表示 INT 這個(gè)規(guī)則是 0-9 之間的一個(gè)或多個(gè)數(shù)字,當(dāng)然這個(gè)定義其實(shí)并不嚴(yán)格。再嚴(yán)格一點(diǎn),應(yīng)該限制其長(zhǎng)度。
在理解正則表達(dá)式的基礎(chǔ)上,ANTLR4 的 g4 語(yǔ)法規(guī)則還是比較好理解的。
定義 ANTLR4 規(guī)則需要注意一種情況,即可能出現(xiàn)一個(gè)字符串同時(shí)支持多種規(guī)則,如以下的兩個(gè)規(guī)則:
ID: [a-zA-Z]+;
FROM: ‘from’;
很明顯,字符串” from”同時(shí)滿足上述兩個(gè)規(guī)則,ANTLR4 處理的方式是按照定義的順序決定。這里 ID 定義在 FROM 前面,所以字符串 from 會(huì)優(yōu)先匹配到 ID 這個(gè)規(guī)則上。
其實(shí)在定義好與法規(guī)中,編寫完成 g4 文件后,ANTLR4 已經(jīng)為我們完成了 50%的工作:幫我們實(shí)現(xiàn)了整個(gè)架構(gòu)及接口了,剩下的開發(fā)工作就是基于接口或抽象類進(jìn)行具體的實(shí)現(xiàn)。實(shí)現(xiàn)上有兩種方式來處理生成的語(yǔ)法樹,其一 Visitor 模式,另一種方式是 Listener(監(jiān)聽器模式)。
生成詞法和語(yǔ)法解析器
基于maven插件生成
<plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>${antlr4.version}</version> <configuration> <sourceDirectory>src/main/java</sourceDirectory> <outputDirectory>src/main/java</outputDirectory> <arguments> <argument>-visitor</argument> <argument>-listener</argument> </arguments> </configuration> <executions> <execution> <goals> <goal>antlr4</goal> </goals> </execution> </executions> </plugin>
執(zhí)行命令
mvn antlr4:antlr4
使用ideal插件生成
實(shí)現(xiàn)運(yùn)算邏輯
第一種:基于visitor實(shí)現(xiàn)
package com.et.antlr; import java.util.HashMap; import java.util.Map; public class EvalVisitor extends LabeledExprBaseVisitor<Integer> { // Store variables (for assignment) Map<String, Integer> memory = new HashMap<>(); /** stat : expr NEWLINE */ @Override public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) { Integer value = visit(ctx.expr()); // evaluate the expr child // System.out.println(value); // print the result return value; // return dummy value } /** stat : ID '=' expr NEWLINE */ @Override public Integer visitAssign(LabeledExprParser.AssignContext ctx) { String id = ctx.ID().getText(); // id is left-hand side of '=' int value = visit(ctx.expr()); // compute value of expression on right memory.put(id, value); // store it in our memory return value; } /** expr : expr op=('*'|'/') expr */ @Override public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) { int left = visit(ctx.expr(0)); // get value of left subexpression int right = visit(ctx.expr(1)); // get value of right subexpression if (ctx.op.getType() == LabeledExprParser.MUL) return left * right; return left / right; // must be DIV } /** expr : expr op=('+'|'-') expr */ @Override public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) { int left = visit(ctx.expr(0)); // get value of left subexpression int right = visit(ctx.expr(1)); // get value of right subexpression if (ctx.op.getType() == LabeledExprParser.ADD) return left + right; return left - right; // must be SUB } /** expr : INT */ @Override public Integer visitInt(LabeledExprParser.IntContext ctx) { return Integer.valueOf(ctx.INT().getText()); } /** expr : ID */ @Override public Integer visitId(LabeledExprParser.IdContext ctx) { String id = ctx.ID().getText(); if (memory.containsKey(id)) return memory.get(id); return 0; // default value if the variable is not found } /** expr : '(' expr ')' */ @Override public Integer visitParens(LabeledExprParser.ParensContext ctx) { return visit(ctx.expr()); // return child expr's value } /** stat : NEWLINE */ @Override public Integer visitBlank(LabeledExprParser.BlankContext ctx) { return 0; // return dummy value } }
第二種:基于listener實(shí)現(xiàn)
package com.et.antlr; import org.antlr.v4.runtime.tree.ParseTreeProperty; import org.antlr.v4.runtime.tree.TerminalNode; import java.util.HashMap; import java.util.Map; public class EvalListener extends LabeledExprBaseListener { // Store variables (for assignment) private final Map<String, Integer> memory = new HashMap<>(); // Store expression results private final ParseTreeProperty<Integer> values = new ParseTreeProperty<>(); private int result=0; @Override public void exitPrintExpr(LabeledExprParser.PrintExprContext ctx) { int value = values.get(ctx.expr()); //System.out.println(value); result=value; } public int getResult() { return result; } @Override public void exitAssign(LabeledExprParser.AssignContext ctx) { String id = ctx.ID().getText(); int value = values.get(ctx.expr()); memory.put(id, value); } @Override public void exitMulDiv(LabeledExprParser.MulDivContext ctx) { int left = values.get(ctx.expr(0)); int right = values.get(ctx.expr(1)); if (ctx.op.getType() == LabeledExprParser.MUL) { values.put(ctx, left * right); } else { values.put(ctx, left / right); } } @Override public void exitAddSub(LabeledExprParser.AddSubContext ctx) { int left = values.get(ctx.expr(0)); int right = values.get(ctx.expr(1)); if (ctx.op.getType() == LabeledExprParser.ADD) { values.put(ctx, left + right); } else { values.put(ctx, left - right); } } @Override public void exitInt(LabeledExprParser.IntContext ctx) { int value = Integer.parseInt(ctx.INT().getText()); values.put(ctx, value); } @Override public void exitId(LabeledExprParser.IdContext ctx) { String id = ctx.ID().getText(); if (memory.containsKey(id)) { values.put(ctx, memory.get(id)); } else { values.put(ctx, 0); // default value if the variable is not found } } @Override public void exitParens(LabeledExprParser.ParensContext ctx) { values.put(ctx, values.get(ctx.expr())); } }
以上只是一些關(guān)鍵代碼,所有代碼請(qǐng)參見下面代碼倉(cāng)庫(kù)
代碼倉(cāng)庫(kù)
3.測(cè)試
測(cè)試vistor方式
package com.et.antlr; /*** * Excerpted from "The Definitive ANTLR 4 Reference", * published by The Pragmatic Bookshelf. * Copyrights apply to this code. It may not be used to create training material, * courses, books, articles, and the like. Contact us if you are in doubt. * We make no guarantees that this code is fit for any purpose. * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information. ***/ import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.tree.ParseTree; import java.io.FileInputStream; import java.io.InputStream; public class CalcByVisit { public static void main(String[] args) throws Exception { /* String inputFile = null; if ( args.length>0 ) inputFile = args[0]; InputStream is = System.in; if ( inputFile!=null ) is = new FileInputStream(inputFile);*/ ANTLRInputStream input = new ANTLRInputStream("1+2*3\n"); LabeledExprLexer lexer = new LabeledExprLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); LabeledExprParser parser = new LabeledExprParser(tokens); ParseTree tree = parser.prog(); // parse EvalVisitor eval = new EvalVisitor(); int result =eval.visit(tree); System.out.println(result); } }
測(cè)試listener方式
package com.et.antlr; import org.antlr.v4.runtime.ANTLRInputStream; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * @author liuhaihua * @version 1.0 * @ClassName CalbyLisenter * @Description todo * @date 2024年06月06日 16:40 */ public class CalbyLisener { public static void main(String[] args) throws IOException { /* String inputFile = null; if ( args.length>0 ) inputFile = args[0]; InputStream is = System.in; if ( inputFile!=null ) is = new FileInputStream(inputFile);*/ ANTLRInputStream input = new ANTLRInputStream("1+2*3\n"); LabeledExprLexer lexer = new LabeledExprLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); LabeledExprParser parser = new LabeledExprParser(tokens); ParseTree tree = parser.prog(); // parse ParseTreeWalker walker = new ParseTreeWalker(); EvalListener evalListener =new EvalListener(); walker.walk(evalListener, tree); int result=evalListener.getResult(); System.out.println(result); } }
運(yùn)行上述測(cè)試用例,計(jì)算結(jié)果符合預(yù)期
4.引用
以上就是SpringBoot集成antlr實(shí)現(xiàn)詞法和語(yǔ)法分析的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot antlr詞法和語(yǔ)法分析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java使用POI讀取properties文件并寫到Excel的方法
這篇文章主要介紹了java使用POI讀取properties文件并寫到Excel的方法,涉及java操作properties文件及Excel文件的相關(guān)技巧,需要的朋友可以參考下2015-06-06Java輕松使用工具類實(shí)現(xiàn)獲取wav時(shí)間長(zhǎng)度
在Java中,工具類定義了一組公共方法,這篇文章將介紹Java中使用工具類來獲取一個(gè)wav文件的時(shí)間長(zhǎng)度,感興趣的同學(xué)繼續(xù)往下閱讀吧2021-10-10java實(shí)現(xiàn)MapReduce對(duì)文件進(jìn)行切分的示例代碼
本文主要介紹了java實(shí)現(xiàn)MapReduce對(duì)文件進(jìn)行切分的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01java高并發(fā)鎖的3種實(shí)現(xiàn)示例代碼
本篇文章主要介紹了java高并發(fā)鎖的3種實(shí)現(xiàn)示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08Spring中的DefaultResourceLoader使用方法解讀
這篇文章主要介紹了Spring中的DefaultResourceLoader使用方法解讀,DefaultResourceLoader是spring提供的一個(gè)默認(rèn)的資源加載器,DefaultResourceLoader實(shí)現(xiàn)了ResourceLoader接口,提供了基本的資源加載能力,需要的朋友可以參考下2024-02-02