语法解析器ANTLR4的java表达式解析实战

背景

自动化测试框架中需要支持计算解析断言表达式

需求

发送完一个http请求后,需要对响应的内容做精确校验,实现方式:填写断言表达式,框架实现对类似如下表达式的计算解析。

( [$.statusCode] == 200 OR [$.comments] == '成功' ) AND ${v_cardNo} != '12345' AND ${v_money} > 100.98 AND ${v_money} > 0.98
  1. [json路径],中括号中间的表示http请求响应的参数路径
  2. ${变量名},美元符和大括号中间的表示用例执行上下文中储存的变量
  3. 字符串用单引号引用
  4. 支持等于、不等于、大于、大于等于、小于、小于等于、与、或等操作

实现

1. 新建maven工程,并创建一个g4文件

src/main/resources 目录下,命名:Expression.g4

内容如下

// 定义了 grammar 的名字,名字需要与文件名对应
grammar Expression;
@header {
// 定义package
package com.bobo.learn.antlr.core;
}
// Lexer rules
WS:             [ \t\r\n]+ -> skip;
STRING:         '\'' (~['\r\n])* '\'';
NUMBER:         ([0-9][0-9]*)+(.[0-9]+)?;
EXTRACT_PATH:   '[' ~[[\]]+ ']';
VARIABLE_NAME:  '${' ~[${}]+ '}';
LPAREN:         '(';
RPAREN:         ')';
EQUAL:          '==';
NOT_EQUAL:      '!=';
LESS_THAN:      '<';
LESS_EUQAL:     '<=';
LARGER_THAN:    '>';
LARGER_EQUAL:   '>=';
AND:            'AND';
OR:             'OR';

// Parser rules
// #后面的名字,是在后续处理对应规则的时候会用到的
expression
           : LPAREN expression RPAREN                               #expressionWithBr
           | EXTRACT_PATH op=(EQUAL | NOT_EQUAL) STRING             #expressionWithString
           | EXTRACT_PATH op=(EQUAL | NOT_EQUAL) VARIABLE_NAME      #expressionWithString
           | EXTRACT_PATH op=(EQUAL | NOT_EQUAL) EXTRACT_PATH       #expressionWithString
           | EXTRACT_PATH op=(EQUAL | NOT_EQUAL | LESS_THAN | LESS_EUQAL | LARGER_THAN | LARGER_EQUAL) NUMBER   #expressionWithNumber
           | VARIABLE_NAME op=(EQUAL | NOT_EQUAL) STRING            #expressionWithString
           | VARIABLE_NAME op=(EQUAL | NOT_EQUAL) EXTRACT_PATH      #expressionWithString
           | VARIABLE_NAME op=(EQUAL | NOT_EQUAL) VARIABLE_NAME     #expressionWithString
           | VARIABLE_NAME op=(EQUAL | NOT_EQUAL | LESS_THAN | LESS_EUQAL | LARGER_THAN | LARGER_EQUAL) NUMBER  #expressionWithNumber
           | expression AND expression   #andOperation
           | expression OR expression   #orOperation
           ;


2. 安装antlr4插件

此插件可以调试,antlr4语法规则

3. 调试语法规则

  • 在语法上右键-》点击test rule expression

  • 输入表达式,可以到右下方会出现生成的解析树,如果有错误,会有提示

4. 生成java源码文件

  • 右键g4文件,点击Configure ANTLR

  • 选择生成的源码文件的目标目录

在这里直接与g4文件配置的package目录对应

  • 右键g4文件,点击Generate ANTLR

  • 生成结果

5. 测试

  • 新建测试代码ExpressionTest.java
package com.bobo.learn.antlr;
import com.bobo.learn.antlr.core.ExpressionBaseVisitor;
import com.bobo.learn.antlr.core.ExpressionLexer;
import com.bobo.learn.antlr.core.ExpressionParser;
import cn.hutool.core.util.ReUtil;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.apache.commons.lang3.StringUtils;

@Slf4j
public class ExpressionTest {
    public static void main(String[] args) {
        final String expr = "( [$.statusCode] == 200 OR [$.comments] == '成功' OR [$.comments] == ${v_hello} ) AND ${v_cardNo} != '12345' AND ${v_money} > 100.99";
        log.info(expr);
        CharStream input = CharStreams.fromString(expr);
        ExpressionLexer lexer = new ExpressionLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        ExpressionParser parser = new ExpressionParser(tokens);
        ExpressionParser.ExpressionContext context = parser.expression();
        AssertExpressionVisitor visitor = new AssertExpressionVisitor();
        Object result = visitor.visit(context);
        log.info("Expression result: {}", result);
    }

    static class AssertExpressionVisitor extends ExpressionBaseVisitor<Object> {

        @Override
        public Object visitExpressionWithBr(ExpressionParser.ExpressionWithBrContext ctx) {
            return visit(ctx.getChild(1));
        }

        @Override
        public Object visitExpressionWithString(ExpressionParser.ExpressionWithStringContext ctx) {
            String left = ctx.getChild(0).getText();
            String right = ctx.getChild(2).getText();

            if(ctx.op != null) {
                log.info("原始:{} {} {}", left, ctx.op.getText(), right);
                String leftOperand = null;
                String rightOperand = null;

                if(left.startsWith("[")) {
                    leftOperand = extract(left) + "";
                }else if(left.startsWith("${")) {
                    leftOperand = getVariableValue(left) + "";
                }

                if(right.startsWith("[")) {
                    rightOperand = extract(right) + "";
                }else if(right.startsWith("${")) {
                    rightOperand = getVariableValue(right) + "";
                }else if(ctx.STRING() != null) {
                    rightOperand = ctx.STRING().getText();
                    rightOperand = rightOperand.substring(1, rightOperand.length() - 1);
                }
                log.info("实际:{} {} {}", leftOperand, ctx.op.getText(), rightOperand);

                switch (ctx.op.getType()) {
                    case ExpressionLexer.EQUAL:
                        return StringUtils.equals(leftOperand, rightOperand);
                    case ExpressionLexer.NOT_EQUAL:
                        return !StringUtils.equals(leftOperand, rightOperand);
                    default: throw new RuntimeException("unsupported operator type");
                }
            }
            return super.visitExpressionWithString(ctx);
        }

        @Override
        public Object visitExpressionWithNumber(ExpressionParser.ExpressionWithNumberContext ctx) {
            String left = ctx.getChild(0).getText();
            String right = ctx.getChild(2).getText();

            if(ctx.op != null) {
                log.info("原始:{} {} {}", left, ctx.op.getText(), right);
                Double leftOperand = null;
                Double rightOperand = Double.valueOf(right);
                Object leftString = null;
                if(left.startsWith("[")) {
                    leftString = extract(left);
                } else if(left.startsWith("${")) {
                    leftString = getVariableValue(left);
                }
                if(leftString != null && ReUtil.isMatch("([1-9][0-9]*)+(.[0-9]+)?", leftString.toString())) {
                    leftOperand = Double.valueOf(leftString.toString());
                }
                log.info("{} {} {}", leftOperand, ctx.op.getText(), rightOperand);
                if(leftOperand == null) {
                    return false;
                }

                switch (ctx.op.getType()) {
                    case ExpressionLexer.EQUAL:
                        return rightOperand.equals(leftOperand);
                    case ExpressionLexer.NOT_EQUAL:
                        return !rightOperand.equals(leftOperand);
                    case ExpressionLexer.LESS_THAN:
                        return leftOperand < rightOperand;
                    case ExpressionLexer.LESS_EUQAL:
                        return leftOperand <= rightOperand;
                    case ExpressionLexer.LARGER_THAN:
                        return leftOperand > rightOperand;
                    case ExpressionLexer.LARGER_EQUAL:
                        return leftOperand >= rightOperand;
                    default: throw new RuntimeException("unsupported operator type");
                }
            }
            return super.visitExpressionWithNumber(ctx);
        }

        @Override
        public Object visitOrOperation(ExpressionParser.OrOperationContext ctx) {
            Object left = visit(ctx.getChild(0));
            Object right = visit(ctx.getChild(2));
            log.info("实际:{} {} {}", left, ctx.OR().getText(), right);
            if(left instanceof Boolean && right instanceof Boolean) {
                return (Boolean)left || (Boolean)right;
            }
            return false;
        }

        @Override
        public Object visitAndOperation(ExpressionParser.AndOperationContext ctx) {
            Object left = visit(ctx.getChild(0));
            Object right = visit(ctx.getChild(2));
            log.info("{} {} {}", left, ctx.AND().getText(), right);
            if(left instanceof Boolean && right instanceof Boolean) {
                return (Boolean)left && (Boolean)right;
            }
            return false;
        }

        /**
         * 根据路径提取http响应参数的值
         * @param path
         * @return
         */
        public Object extract(String path) {
            if("[$.statusCode]".equalsIgnoreCase(path)) {
                return 200;
            }
            if("[$.comments]".equalsIgnoreCase(path)) {
                return "成功";
            }
            return "";
        }

        /**
         * 提取用例执行上下文中保存的变量的值
         * @param variableName
         * @return
         */
        public Object getVariableValue(String variableName) {
            return 100;
        }
    }
}
  • 执行测试代码,可以看到一个解析过程,测试结果如下

maven工程pom文件

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bobo</groupId>
    <artifactId>learn</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>


        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>
        <dependency>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-runtime</artifactId>
            <version>4.12.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>


        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.5</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>2.0.5</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值