用Antlr查询JSON的自定义语法


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获取源代码并进行实验。

快乐编码!