
Antlr全称(ANother Tool for Language Recognition),Antlr4是一款强大的语法分析器生成工具,推特,Haddop,Oracle等各大知名公司在用到了Antlr来构建自己的语言处理类项目。
一门语言的正式描述称为语法(grammar),Antlr可以为语言生成一个词法分析器,并且自动建立语法分析树,也能自动生成树的遍历器,然后我们就可以访问树的节点,执行自定义业务逻辑代码。
本文主要是介绍Antlr4的使用,因此不过多的介绍其特性和各种使用方式,以简单的Demo来了解下它的功能。
词法分析也称为分词,此阶段从左向右扫描源文件,将其字符流分割成一个个的词(token) 。所谓 token ,就是源文件中不可再进一步分割的一串字符,类似于英语中单词,或汉语中的词。


以上词法分析和语法分析的过程,在实际使用antlr的过程都不需要关心,只需要进行定义语法规则,以及处理最后的语法分析树即可。
有两种方式可以快速的跑起来Demo,命令行或者IDE,不过由于命令行当前默认需要JDK 11,可能大部分人不支持这个版本,还有一些额外的配置。
antlr 在 4.10仍然支持JDK 1.8,在4.10.1之后需要JDK11。
我们直接使用Idea自带的插件来完成Demo;
安装antlr4的插件;

新建一个Expr.g4文件,记得文件名要和里面定义的语法规则名称保持一致。

- grammar Expr;
- prog: expr EOF ;
- expr: expr ('*'|'/') expr
- | expr ('+'|'-') expr
- | INT
- | '(' expr ')'
- ;
- NEWLINE : [\r\n]+ -> skip;
- INT : [0-9]+ ;
- 复制代码
在文件目标规则上右键测试,Antlr4帮我们生成了最终的分析树:

- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>antlr4</artifactId>
- <version>4.10.1</version>
- </dependency>
- 复制代码

把生成的文件拷贝到自己的包下:

其中文件的含义:

- package me.aihe.bizim.expr;
-
- import me.aihe.bizim.expr.ExprParser.ProgContext;
- import org.antlr.v4.runtime.CharStreams;
- import org.antlr.v4.runtime.CodePointCharStream;
- import org.antlr.v4.runtime.CommonTokenStream;
- import org.antlr.v4.runtime.tree.ParseTreeWalker;
-
- public class ExprDemo {
-
- public static void main(String[] args) {
- // 构建字符流
- CodePointCharStream charStream = CharStreams.fromString("1+2+3*4");
-
- // 从字符流分析词法, 解析为token
- ExprLexer lexer = new ExprLexer(charStream);
-
- // 从token进行分析
- ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
-
- // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
- ProgContext prog = parser.prog();
- ParseTreeWalker walker = new ParseTreeWalker();
- walker.walk( new ExprBaseListener(), prog );
-
- // 使用visitor,生成自定义的对象
- Object accept = prog.accept(new ExprBaseVisitor<>());
-
-
- // 打印生成的语法树
- System.out.println( prog.toStringTree(parser));
-
- }
-
- }
-
-
- 复制代码
通过antlr4的API,我们可以识别定义语言的语法树,但是如何做自定义的处理呢,基于上述的demo,我们实现自己的visior用于构建一个计数器的实现。
在真正实现之前,我们需要将语法重新区分一下不同的分支,方便逻辑处理:
- grammar Expr;
- prog: expr EOF ;
- expr: expr ('*'|'/') expr #MultiOrDiv
- | expr ('+'|'-') expr #AddOrSub
- | INT #Lieteral
- | '(' expr ')' #Single
- ;
- NEWLINE : [\r\n]+ -> skip;
- INT : [0-9]+ ;
- 复制代码
- package me.aihe.bizim.expr;
-
- import java.util.Objects;
-
- import me.aihe.bizim.expr.ExprParser.AddOrSubContext;
- import me.aihe.bizim.expr.ExprParser.ExprContext;
- import me.aihe.bizim.expr.ExprParser.LieteralContext;
- import me.aihe.bizim.expr.ExprParser.MultiOrDivContext;
- import me.aihe.bizim.expr.ExprParser.ProgContext;
- import me.aihe.bizim.expr.ExprParser.SingleContext;
-
- public class EvalExprVisitor extends ExprBaseVisitor<Integer>{
-
- /**
- * 入口处调用
- * @param ctx the parse tree
- * @return
- */
- @Override
- public Integer visitProg(ProgContext ctx) {
- ExprContext expr = ctx.expr();
- return visit(expr);
- }
-
- /**
- * expr: expr ('*'|'/') expr
- * | expr ('+'|'-') expr
- * | INT
- * | '(' expr ')'
- * @param ctx the parse tree
- * @return
- */
- /**
- * expr ('*'|'/') expr
- */
- @Override
- public Integer visitMultiOrDiv(MultiOrDivContext ctx) {
- Integer op1 = visit(ctx.expr(0));
- Integer op2 = visit(ctx.expr(1));
-
- String operator = ctx.getChild(1).getText();
- if (Objects.equals(operator, "*")){
- return op1 * op2;
- }
-
- if (Objects.equals(operator, "/")){
- return op1 / op2;
- }
-
- return 0;
- }
-
- /**
- * expr ('+'|'-') expr
- */
- @Override
- public Integer visitAddOrSub(AddOrSubContext ctx) {
- Integer op1 = visit(ctx.expr(0));
- Integer op2 = visit(ctx.expr(1));
-
- String operator = ctx.getChild(1).getText();
- if (Objects.equals(operator, "+")){
- return op1 + op2;
- }
-
- if (Objects.equals(operator, "-")){
- return op1 - op2;
- }
-
- return 0;
- }
-
- /**
- *
- * @param ctx the parse tree
- * @return
- */
- @Override
- public Integer visitSingle(SingleContext ctx) {
- return visit(ctx);
- }
-
- /**
- * INT
- * @return
- */
- @Override
- public Integer visitLieteral(LieteralContext ctx) {
- return Integer.valueOf(ctx.INT().getText());
- }
- }
-
- 复制代码
符合预期,这样我们自定义的计数器解析程序就处理好了;
- import java.util.Arrays;
- import java.util.List;
- import java.util.Objects;
-
- import me.aihe.bizim.expr.ExprParser.ProgContext;
- import org.antlr.v4.runtime.CharStreams;
- import org.antlr.v4.runtime.CodePointCharStream;
- import org.antlr.v4.runtime.CommonTokenStream;
- import org.springframework.util.Assert;
-
- public class ExprDemo {
-
- public static void main(String[] args) {
-
- List<String> testSet = Arrays.asList(
- "1+2",
- "1+2+3*4",
- "3/3",
- "10/2",
- "5*5+10+5*5"
- );
-
- List<Integer> res = Arrays.asList(
- 3,15,1,5,60
- );
-
- for (int i = 0; i < testSet.size(); i++) {
- // 构建字符流
- CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));
-
- // 从字符流分析词法, 解析为token
- ExprLexer lexer = new ExprLexer(charStream);
-
- // 从token进行分析
- ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
-
- // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
- ProgContext prog = parser.prog();
-
-
- // 使用visitor,生成自定义的对象
- Integer integer = prog.accept(new EvalExprVisitor());
- System.out.println(integer);
- Assert.isTrue(Objects.equals(integer, res.get(i)),"");
- }
- }
-
- }
-
- 复制代码

借用一个辅助工具dot语言,用来生成各种自定义图形的,基于上述的语法树监听器,我们构建自己的执行程序,然后查看真实的调用过程。
- package me.aihe.bizim.expr;
-
- import java.util.List;
- import java.util.Set;
- import java.util.Stack;
-
- import me.aihe.bizim.expr.ExprParser.ProgContext;
- import org.antlr.v4.runtime.ParserRuleContext;
- import org.antlr.v4.runtime.misc.MultiMap;
- import org.antlr.v4.runtime.misc.OrderedHashSet;
-
- public class EvalExprListener extends ExprBaseListener {
-
- private Graph graph = new Graph();
- Stack<String> current = new Stack<>();
-
- String prefix = "exp_";
-
-
-
- @Override
- public void enterEveryRule(ParserRuleContext ctx) {
- if (ctx instanceof ProgContext){
- return;
- }
-
- // 生成唯一的节点标识
- String n = prefix + System.identityHashCode(ctx);
- String node = n + String.format("[label=<%s>]",ctx.getText());
- graph.nodes.add(node);
-
- if (!current.empty()){
- graph.edge( current.peek(), n );
- }
-
- current.push(n);
- }
-
- @Override
- public void exitEveryRule(ParserRuleContext ctx) {
- if (ctx instanceof ProgContext){
- return;
- }
- current.pop();
- }
-
- public String toDot(){
- return graph.toDot();
- }
-
-
- static class Graph {
- Set<String> nodes = new OrderedHashSet<>();
- MultiMap<String, String> edges = new MultiMap<>();
-
-
- public void edge(String source, String target) {
- if (source == null || target == null){
- return;
- }
- edges.map(source, target);
- }
-
- public String toDot() {
- StringBuffer sb = new StringBuffer();
- sb.append("digraph G { \n");
- sb.append("node[shape=plaintext,style=filled];graph[splines=ortho];\n");
-
- // 声明节点
- for (String node : nodes) {
- sb.append(node).append(";");
- sb.append("\n");
- }
-
- sb.append("\n");
-
- for (String source : edges.keySet()) {
- List<String> targets = edges.get(source);
- for (String target : targets) {
- sb.append(" ")
- .append(source)
- .append(" -> ")
- .append(target).append("\n");
- }
- }
-
- sb.append("}\n");
- return sb.toString();
- }
-
- }
- }
-
- 复制代码
使用监听器:
- CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));
-
- // 从字符流分析词法, 解析为token
- ExprLexer lexer = new ExprLexer(charStream);
-
- // 从token进行分析
- ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
-
- // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
- ProgContext prog = parser.prog();
-
-
- // 使用visitor,生成自定义的对象
- Integer integer = prog.accept(new EvalExprVisitor());
- System.out.println(integer);
- Assert.isTrue(Objects.equals(integer, res.get(i)),"");
-
- ParseTreeWalker walker = new ParseTreeWalker();
-
- EvalExprListener listener = new EvalExprListener();
- walker.walk(listener, prog );
- System.out.println(listener.toDot());
- 复制代码
5*5+10+5*5 生成结果:
- digraph G {
- node[shape=plaintext,style=filled];graph[splines=ortho];
- exp_2085857771[label=<5*5+10+5*5>];
- exp_248609774[label=<5*5+10>];
- exp_708049632[label=<5*5>];
- exp_1887400018[label=<5>];
- exp_285377351[label=<5>];
- exp_344560770[label=<10>];
- exp_559450121[label=<5*5>];
- exp_716083600[label=<5>];
- exp_791885625[label=<5>];
-
- exp_2085857771 -> exp_248609774
- exp_2085857771 -> exp_559450121
- exp_248609774 -> exp_708049632
- exp_248609774 -> exp_344560770
- exp_708049632 -> exp_1887400018
- exp_708049632 -> exp_285377351
- exp_559450121 -> exp_716083600
- exp_559450121 -> exp_791885625
- }
- 复制代码
在dot语言解析网站上贴下生成的代码: dreampuf.github.io/GraphvizOnl…

或者直接用Java代码生成对应的SVG图也是可以的:仍然使用上述监听器。
下载plantuml:plantuml.com/zh/download graphviz配置:plantuml.com/zh/graphviz…
Java引入依赖:
- <dependency>
- <groupId>net.sourceforge.plantuml</groupId>
- <artifactId>plantuml</artifactId>
- <version>1.2022.13</version>
- </dependency>
- 复制代码
使用Java生成对应的点图:
-
- public class Demo {
- public static void main(String[] args) throws IOException {
- FileFormat gif = FileFormat.valueOf("SVG");
- StringBuffer sb = new StringBuffer();
- // 注意 digraph g {的后面不要有空格
- sb.append("@startuml\n"
- + "digraph g {\n"
- + "node[shape=plaintext,style=filled];graph[splines=ortho];\n"
- + "exp_2085857771[label=<5*5+10+5*5>];\n"
- + "exp_248609774[label=<5*5+10>];\n"
- + "exp_708049632[label=<5*5>];\n"
- + "exp_1887400018[label=<5>];\n"
- + "exp_285377351[label=<5>];\n"
- + "exp_344560770[label=<10>];\n"
- + "exp_559450121[label=<5*5>];\n"
- + "exp_716083600[label=<5>];\n"
- + "exp_791885625[label=<5>];\n"
- + "\n"
- + " exp_2085857771 -> exp_248609774\n"
- + " exp_2085857771 -> exp_559450121\n"
- + " exp_248609774 -> exp_708049632\n"
- + " exp_248609774 -> exp_344560770\n"
- + " exp_708049632 -> exp_1887400018\n"
- + " exp_708049632 -> exp_285377351\n"
- + " exp_559450121 -> exp_716083600\n"
- + " exp_559450121 -> exp_791885625\n"
- + "}\n"
- + "@enduml");
-
- String str = sb.toString();
- System.out.println(str);
- SourceStringReader dot = new SourceStringReader(str);
- FileOutputStream os = new FileOutputStream(new File("result.svg"));
- dot.outputImage(os, new FileFormatOption(gif));
- os.flush();
-
- }
- }
- 复制代码

- // 可以在识别到错误的时候,抛出异常终止进行
- parser.addErrorListener( new BaseErrorListener());
- 复制代码
如果说想要自定义一些语言类应用,可以参考antlr4的lab网站,内置了当前主流的语言语法结构。
比如URL语法,以及JSON,CSV,DOT、SQL等语言的解析器;


1、Antlr4是一种语法分析器的 生成器,帮我们屏蔽了词法分析和语法分析的过程,直接生成出对应的语法分析树。 基于语法分析树,我们可以构建自己的语言类应用;
2、Idea工具可以直接安装Antlr4的插件,可以帮我们检验自定义的语法特性,并且可以生成对应的Java基础类。
3、通过生成的Java基础类,配合Antlr4的API,可以快速完成一个语言类的应用。
参考: