作者介绍

大家好,我是 CodeStats

一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程) 范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。

我的技术信条:所有高深的技术,最后都能用大白话讲清楚。如果讲不清楚,说明还没真正理解。


本文你能获得什么

  • ✅ 理解递归下降算法如何实现运算符优先级

  • ✅ 掌握表达式引擎的完整执行流程:词法分析→语法分析→AST构建→求值

  • ✅ 理解AST节点体系的设计与访问者模式的应用

  • ✅ 掌握EvaluationContext支持的数据类型与访问方式

  • ✅ 通过完整场景示例串联全流程

  • ✅ 学会如何扩展注册自定义函数


目录

  1. 递归下降算法与运算符优先级

  2. 表达式引擎执行流程:词法分析与AST构建

  3. 表达式节点体系与访问者模式

  4. 上下文参数支持的数据类型访问

  5. 完整场景示例流程及功能分析

  6. 自定义函数扩展机制


一、递归下降算法与运算符优先级

1.1 什么是递归下降解析

递归下降解析(Recursive Descent Parsing)是一种自顶向下的语法分析技术。它的核心思想是:为文法中的每一个非终结符编写一个对应的解析函数,函数之间通过递归调用来完成整个表达式的解析。

用大白话讲:我们把表达式当作一个“俄罗斯套娃”,每层运算符都有自己专属的解析函数,高优先级的运算符被包在更内层。

1.2 运算符优先级如何通过递归下降实现

表达式引擎需要正确处理运算符的优先级——例如 2 + 3 * 4 应当先算乘法再算加法。在递归下降解析器中,优先级的实现方式是将高优先级的运算符放在更深的解析层级中

以标准算术表达式为例,文法定义如下:

text

expr   → term (('+' | '-') term)*
term   → factor (('*' | '/') factor)*
factor → NUMBER | '(' expr ')'

对应的解析函数层级为:

层级 函数 处理的运算符 优先级
最高 parsePrimary() 字面量、括号、变量
parseUnary() ! ~ + -(一元) 最高
parseMultiplicative() * / %
parseAdditive() + -
parseShift() << >> >>>
parseRelational() < <= > >=
parseEquality() == !=
parseLogicalAnd() && 更低
最低 parseLogicalOr() || 最低

关键设计parseAdditive() 调用 parseMultiplicative() 解析右操作数,确保乘法在加法之前被解析。这种层级嵌套天然实现了优先级——高优先级的运算符在更深的递归层级中被处理

1.3 代码实现示例

java

// 加法/减法 —— 调用乘法/除法
private ExpressionNode parseAdditive() {
    ExpressionNode node = parseMultiplicative();
    while (match(TokenType.OPERATOR, "+", "-")) {
        String op = previous().getValue();
        ExpressionNode right = parseMultiplicative();
        node = new BinaryNode(node, right, op);
    }
    return node;
}

// 乘法/除法 —— 调用一元表达式
private ExpressionNode parseMultiplicative() {
    ExpressionNode node = parseUnary();
    while (match(TokenType.OPERATOR, "*", "/", "%")) {
        String op = previous().getValue();
        ExpressionNode right = parseUnary();
        node = new BinaryNode(node, right, op);
    }
    return node;
}

二、表达式引擎执行流程:词法分析与AST构建

表达式引擎的核心流程是经典的编译前端过程

text

原始表达式字符串 → 词法分析(Lexer) → Token流 → 语法分析(Parser) → AST → 求值器(Evaluator) → 计算结果

2.1 词法分析(Lexical Analysis)

词法分析器(Lexer)负责将字符串流分解为有意义的Token序列

Token类型定义

java

public enum TokenType {
    IDENTIFIER,  // 变量名、函数名
    NUMBER,      // 数字字面量
    STRING,      // 字符串字面量
    BOOLEAN,     // true/false
    NULL,        // null
    OPERATOR,    // + - * / && || == != 等
    LPAREN, RPAREN, LBRACKET, RBRACKET,  // 括号
    DOT, COMMA, EOF
}

词法分析核心逻辑

java

public List<Token> tokenize() {
    while (pos < length) {
        char ch = source.charAt(pos);
        if (Character.isWhitespace(ch)) { pos++; continue; }
        if (Character.isJavaIdentifierStart(ch)) { tokens.add(scanIdentifier()); continue; }
        if (Character.isDigit(ch)) { tokens.add(scanNumber()); continue; }
        if (ch == '"' || ch == '\'') { tokens.add(scanString()); continue; }
        // 多字符运算符: ==, !=, &&, ||, <=, >=, <<, >>, >>>
        // 单字符运算符: + - * / % ^ & | ~ ! < > ? : , ( ) [ ] .
    }
}

关键点

  • 空白字符直接被跳过

  • 标识符、数字、字符串分别进入不同的扫描分支

  • 运算符需要区分单字符和多字符(如 = vs ==

2.2 语法分析与AST构建

语法分析器(Parser)根据Token流,按照文法规则构建抽象语法树(AST)。

AST节点示例

java

// 二元运算节点:a + b
public class BinaryNode extends ExpressionNode {
    private final ExpressionNode left;
    private final ExpressionNode right;
    private final String operator;
}

// 字面量节点:123, "hello", true
public class LiteralNode extends ExpressionNode {
    private final Object value;
}

// 变量节点:userId
public class VariableNode extends ExpressionNode {
    private final String name;
}

解析示例:表达式 a + b * c

Token流:IDENTIFIER(a) → OPERATOR(+) → IDENTIFIER(b) → OPERATOR(*) → IDENTIFIER(c)

构建的AST:

text

      BinaryNode(+)
      /          \
VariableNode(a)  BinaryNode(*)
                 /          \
        VariableNode(b)  VariableNode(c)

由于 parseAdditive() 调用 parseMultiplicative() 解析右操作数,b * c 被优先组合成一个 BinaryNode(*),作为加法的右子节点,从而保证了乘法优先于加法的正确语义。


三、表达式节点体系与访问者模式

3.1 节点体系的作用

AST中的每个节点代表表达式的一个语法单元。节点体系的核心作用是:

  1. 结构化表示:将扁平的Token流转换为层次化的树结构

  2. 语义承载:每个节点类型对应一种语言构造(运算、变量、方法调用等)

  3. 操作分离:节点只负责“是什么”,不负责“怎么做”

3.2 采用的设计模式:访问者模式(Visitor Pattern)

表达式引擎的核心设计模式是访问者模式

为什么需要访问者模式?

AST中的节点类型有很多种(Binary、Literal、Variable、MethodCall等),我们需要针对每种节点执行不同的求值逻辑。如果使用传统的继承方式,每增加一种操作(如求值、类型检查、代码生成)就要修改所有节点类,违反了开闭原则

访问者模式的核心思想是:将作用于数据结构中各元素的操作与数据结构本身分离,在不修改现有数据结构的前提下定义新的操作

架构设计

text

┌─────────────────┐          ┌─────────────────┐
│  ExpressionNode  │          │    Evaluator    │
│  (抽象父类)      │          │    (接口)       │
│  + accept()      │◄─────────┤  + visitXxx()   │
└────────┬────────┘          └─────────────────┘
         │                              ▲
         │ 继承                          │ 实现
         ▼                              │
┌─────────────────────────────────────────┼─────────────────────┐
│                                         │                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ LiteralNode  │  │ BinaryNode   │  │ VariableNode     │  │
│  │ + accept()   │  │ + accept()   │  │ + accept()       │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
│         │                  │                   │            │
│         └──────────────────┼───────────────────┘            │
│                            ▼                                │
│              visitor.visitXxx(this, context)                │
└─────────────────────────────────────────────────────────────┘

代码实现

java

// 抽象节点:定义accept方法
public abstract class ExpressionNode {
    public abstract Object accept(Evaluator visitor, EvaluationContext context);
}

// 具体节点:调用对应的visit方法
public class BinaryNode extends ExpressionNode {
    @Override
    public Object accept(Evaluator visitor, EvaluationContext context) {
        return visitor.visitBinary(this, context);
    }
}

// 访问者接口:为每个节点类型定义访问方法
public interface Evaluator {
    Object visitLiteral(LiteralNode node, EvaluationContext context);
    Object visitVariable(VariableNode node, EvaluationContext context);
    Object visitBinary(BinaryNode node, EvaluationContext context);
    // ... 其他节点类型
}

// 具体访问者:实现求值逻辑
public class StandardEvaluator implements Evaluator {
    @Override
    public Object visitBinary(BinaryNode node, EvaluationContext context) {
        Object left = evaluate(node.getLeft(), context);
        Object right = evaluate(node.getRight(), context);
        // 根据运算符执行具体运算
    }
}

访问者模式的优势

  1. 可扩展性:新增操作只需新增一个Visitor实现,无需修改节点类

  2. 关注点分离:节点结构(AST)与操作(求值)完全解耦

  3. 双重分派:通过 accept + visit 实现运行时多态


四、上下文参数支持的数据类型访问

EvaluationContext 是表达式引擎的运行时环境,管理表达式求值时所需的所有上下文信息。

4.1 核心能力

java

public interface EvaluationContext {
    // 变量管理
    void setVariable(String name, Object value);
    Object getVariable(String name);
    Map<String, Object> getVariables();
    
    // 函数管理
    void registerFunction(String name, FunctionResolver resolver);
    Object callFunction(String name, Object... args);
    
    // 属性访问
    Object getProperty(Object target, String property);
    void setProperty(Object target, String property, Object value);
    
    PropertyAccessor getPropertyAccessor();
}

4.2 支持的数据类型访问

数据类型 访问方式 示例
变量 context.getVariable(name) context.setVariable("age", 25) → 表达式中使用 age
Map 通过属性访问器直接操作 context.getProperty(userMap, "name")
JavaBean属性 通过getter/setter或字段反射 context.getProperty(user, "userName") → 调用 getUserName()
数组/List 通过索引访问 arr[0] 或 list.get(0)
函数/方法 通过 callFunction 或反射调用 context.callFunction("max", 10, 20)
根对象 作为默认上下文 表达式中可直接访问根对象的属性

4.3 属性访问器(PropertyAccessor)

PropertyAccessor 是上下文访问对象属性的核心组件。ReflectivePropertyAccessor 支持:

  1. 通过getter方法访问user.getName() → 表达式 user.name

  2. 通过isXxx方法访问(布尔类型):user.isActive() → 表达式 user.active

  3. 直接字段访问(私有字段通过 setAccessible(true) 强制访问)

  4. Map类型直接操作map.get("key")

java

// 使用示例
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", new User("张三", 25));
context.setVariable("scores", Map.of("math", 95, "english", 88));

// 表达式:user.name + "的数学成绩是" + scores['math']
// 结果:"张三的数学成绩是95"

五、完整场景示例流程及功能分析

5.1 场景描述

假设我们有一个电商积分计算规则:VIP用户的订单金额超过1000元时,额外赠送10%的积分。

规则表达式:user.vip and order.amount > 1000 ? order.amount * 0.1 : 0

5.2 完整执行流程

text

Step 1: 用户输入表达式字符串
        "user.vip and order.amount > 1000 ? order.amount * 0.1 : 0"

Step 2: 词法分析 (Lexer)
        Token流: IDENTIFIER(user) DOT IDENTIFIER(vip) OPERATOR(and) 
                 IDENTIFIER(order) DOT IDENTIFIER(amount) OPERATOR(>) 
                 NUMBER(1000) OPERATOR(?) ...

Step 3: 语法分析 (Parser) → 构建AST
              TernaryNode
              /    |    \
        BinaryNode  BinaryNode  LiteralNode(0)
        (and)      (*)
        /    \      /    \
    PropAccess PropAccess order.amount LiteralNode(0.1)
    (user.vip) (order.amount > 1000)

Step 4: 求值 (Evaluator)
        ① 访问 TernaryNode → 先求值 condition
        ② 访问 BinaryNode(and) → 求 user.vip 和 order.amount > 1000
        ③ 若 condition 为 true → 求 thenBranch → 返回计算结果
        ④ 若 condition 为 false → 求 elseBranch → 返回 0

Step 5: 返回结果

5.3 功能分析

功能模块 职责 关键类
Lexer(词法分析) 字符串→Token流 Lexer.java
Parser(语法分析) Token流→AST StandardExpressionParser.java
AST节点 表达式的结构化表示 ExpressionNode 及子类
Evaluator(求值器) 遍历AST执行求值 StandardEvaluator.java
EvaluationContext 运行时上下文管理 StandardEvaluationContext.java
PropertyAccessor 对象属性访问 ReflectivePropertyAccessor.java
TypeUtils 类型转换与运算 TypeUtils.java

5.4 使用示例代码

java

// 1. 准备上下文
StandardEvaluationContext context = new StandardEvaluationContext();
User user = new User(true);  // VIP用户
Order order = new Order(1500.0);  // 订单金额1500元
context.setVariable("user", user);
context.setVariable("order", order);

// 2. 解析表达式
ExpressionParser parser = new StandardExpressionParser();
Expression expr = parser.parse("user.vip and order.amount > 1000 ? order.amount * 0.1 : 0");

// 3. 求值
Object result = expr.getValue(context);
System.out.println("赠送积分: " + result);  // 输出: 150.0

六、自定义函数扩展机制

表达式引擎支持通过 EvaluationContext 注册自定义函数,实现业务逻辑的灵活扩展。

6.1 函数注册机制

java

@FunctionalInterface
public interface FunctionResolver {
    Object resolve(Object... args) throws Exception;
}

6.2 注册自定义函数

java

StandardEvaluationContext context = new StandardEvaluationContext();

// 注册自定义函数:计算折扣价
context.registerFunction("discount", args -> {
    double price = toDouble(args[0]);
    double rate = toDouble(args[1]);
    return price * rate;
});

// 注册自定义函数:判断是否为VIP
context.registerFunction("isVIP", args -> {
    User user = (User) args[0];
    return user.getLevel() >= 3;
});

6.3 表达式中调用自定义函数

java

// 表达式:discount(price, 0.8) 表示打8折
Expression expr = parser.parse("discount(order.amount, 0.8)");
Object result = expr.getValue(context);

6.4 内置函数示例

引擎可以预置常用内置函数:

java

public final class BuiltinFunctions {
    public static void registerAll(EvaluationContext context) {
        // 数学函数
        context.registerFunction("abs", args -> Math.abs(toDouble(args[0])));
        context.registerFunction("max", args -> Math.max(toDouble(args[0]), toDouble(args[1])));
        context.registerFunction("sqrt", args -> Math.sqrt(toDouble(args[0])));
        context.registerFunction("pow", args -> Math.pow(toDouble(args[0]), toDouble(args[1])));
        context.registerFunction("random", args -> Math.random());
        
        // 字符串函数
        context.registerFunction("length", args -> args[0].toString().length());
        context.registerFunction("trim", args -> args[0].toString().trim());
        context.registerFunction("toUpperCase", args -> args[0].toString().toUpperCase());
        
        // 类型转换
        context.registerFunction("toInt", args -> Integer.valueOf(args[0].toString()));
        context.registerFunction("toString", args -> args[0].toString());
    }
}

6.5 扩展要点

  1. FunctionResolver 是函数式接口,支持Lambda表达式注册

  2. 函数调用优先级:上下文函数 > 静态方法(ReflectUtils.invokeStaticMethod

  3. 参数处理:函数参数在调用前已被求值,传入的是具体的值而非AST节点

  4. 异常处理:函数内部异常会被捕获并包装为 ExpressionException


总结

本文从零开始设计了一套完整的 Java 表达式引擎,涵盖了:

  1. 递归下降算法:通过函数层级嵌套实现运算符优先级

  2. 执行流程:词法分析→Token流→语法分析→AST→求值

  3. 访问者模式:实现节点结构与操作分离,保证可扩展性

  4. EvaluationContext:管理变量、函数、属性访问

  5. 自定义函数:通过 FunctionResolver 轻松扩展

表达式引擎的核心价值在于让业务逻辑可配置化——无论是规则引擎、公式计算还是动态脚本控制,它都能提供灵活、高效、安全的解决方案。

技术信条回顾:所有高深的技术,最后都能用大白话讲清楚。希望这篇文章让你彻底搞懂了表达式引擎的设计之道。


👍 如果本文对你有帮助,欢迎点赞、收藏、转发!

💬 有任何问题或想法,欢迎在评论区留言交流!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐