Antlr是一个强大的工具,可以用来创建正式的语言。符号和规则对语言的形式化至关重要,也称为语法。定义自定义语法并生成相关的解析器和lexers是一个简单的过程Antlr。Antlr的运行时支持给定字符流的标记化和这些标记的解析。它提供了遍历生成的解析树和应用定制逻辑的机制。让我们利用这个工具,创建一个自定义语法来查询JSON。我们的最终目标是能够编写如下所示的查询:
bpi.current.code eq "USD" and bpi.current.rate gt 650.60
要创建一个新的语法,你必须定义语法规则。让我们通过创建一个名为“JsonQuery.g4”的文件来实现这一点。然后,我们可以开始编写语法规则来允许我们查询JSON。以下是片段:
grammar JsonQuery;
query
: SP? '(' query ')' #parenExp
| query SP LOGICAL_OPERATOR SP query #logicalExp
| attrPath SP 'pr' #presentExp
| attrPath SP op=( 'eq' | 'ne' ) SP value #compareExp
;
LOGICAL_OPERATOR
: 'and' | 'or'
;
EQ : 'eq' ;
NE : 'ne' ;
attrPath
: ATTRNAME subAttr?
;
subAttr
: '.' attrPath
;
ATTRNAME
: ALPHA ATTR_NAME_CHAR* ;
fragment ATTR_NAME_CHAR
: '-' | '_' | ':' | DIGIT | ALPHA
;
您可以浏览整套规则here。
Antlr要求我们在创建语法时遵循某些惯例。首先,文件应该包含一个标题,标题名称应该与包含语法的文件名相匹配。Antlr识别两种类型的规则——解析器规则和lexer规则。解析器规则必须以小写字母开头,而lexer规则必须以大写字母开头。在上面的代码片段中,“查询”是一个解析器规则,“均衡器”是一个lexer规则。规则替代项,如为“查询”解析器规则定义的替代项,可以通过使用“#”运算符来标记(例如:“#parenExp”)。当我们遍历解析树时,标记替代项将触发更精确的事件。正如我之前提到的,Antlr是非常通用的,它提供了大量的功能,从定义规则、生成解析器、lexers、监听器和访问者到非贪婪子规则,以及处理优先和左递归的方法。
Antlr还提供了可以用来创建和可视化语法的集成开发环境插件。我们可以根据我们的语法快速测试示例表达式,并预览生成的解析树。下面是基于我们之前编写的JSON查询表达式生成的解析树的视图:
现在我们有了一个查询JSON的工作语法,让我们将注意力转向创建一个Java程序和实现一个查询引擎。引擎将基于给定的查询表达式遍历生成的解析树,根据指定的JSON对象对其进行评估,并返回一个布尔值来指示查询是否匹配。让我们使用Gradle来创建我们的项目。以下是启用Antlr插件及其依赖项的相关Gradle构建文件:
plugins {
id "antlr"
}
dependencies {
antlr "org.antlr:antlr4:4.7"
}
generateGrammarSource {
arguments += ["-visitor"]
}
请注意,Antlr可以配置为生成一个侦听器类或一个访问者类——两个解析树遍历机制。我们将使用访问者机制遍历解析树并评估查询表达式。Antlr的Gradle插件将根据我们的语法生成定义lexer、parser和visitor类的源代码。我们可以简单地扩展生成的抽象类,并实现相关的定制逻辑来评估JSON查询表达式。下面是JsonQueryEvaluator
类别:
public class JsonQueryEvaluator
extends JsonQueryBaseVisitor<Boolean> {
@Override
public Boolean visitParenExp(ParenExpContext ctx) {
Boolean result = visit(ctx.filter());
return ctx.NOT() != null ? !result : result;
}
@Override
public Boolean visitLogicalExp(LogicalExpContext ctx) {
Boolean leftExp = visit(ctx.filter(0));
if (OR.equals(ctx.LOGICAL_OPERATOR().getText())) {
// Short circuit "or"
return leftExp;
} else {
return leftExp && visit(ctx.filter(1));
}
}
...
}
请注意,访问者方法名称是如何基于我们在语法中指定的标签生成的。这使我们能够针对给定的JSON对象评估解析器规则的各种替代方案。如果我们没有使用标签,我们将被迫使用许多if-else或switch语句来实现相同的功能。
既然我们已经有了一个定制的赋值器,让我们创建查询引擎类。它的工作是将表达式流式传输到lexer,标记该流,生成相应的解析树,然后遍历解析树,根据JSON对象评估表达式。下面是JsonQueryEngine
类别:
public class JsonQueryEngine {
public boolean execute(String expression, JsonObject item) {
if (StringUtils.isNotBlank(expression)) {
CharStream stream = CharStreams
.fromString(expression.trim());
QueryLexer lexer = new QueryLexer(stream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
QueryParser parser = new QueryParser(tokens);
ParseTree parseTree = parser.query();
JsonQueryEvaluator evaluator =
new JsonQueryEvaluator(item);
return evaluator.visit(parseTree)
} else {
...
}
}
...
}
就这样,伙计们。我们现在有了一个自定义语法,可以用来在编写测试时断言JSON对象中的条件。当然,在优化语法和解析逻辑方面还有改进的空间。前往GitHub获取源代码并进行实验。
快乐编码!