【Java 表达式引擎】如何设计一套 Java 表达式引擎:从递归下降到 AST 求值的完整实践
作者介绍
大家好,我是 CodeStats。
一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程) 范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。
我的技术信条:所有高深的技术,最后都能用大白话讲清楚。如果讲不清楚,说明还没真正理解。
本文你能获得什么
-
✅ 理解递归下降算法如何实现运算符优先级
-
✅ 掌握表达式引擎的完整执行流程:词法分析→语法分析→AST构建→求值
-
✅ 理解AST节点体系的设计与访问者模式的应用
-
✅ 掌握EvaluationContext支持的数据类型与访问方式
-
✅ 通过完整场景示例串联全流程
-
✅ 学会如何扩展注册自定义函数
目录
-
递归下降算法与运算符优先级
-
表达式引擎执行流程:词法分析与AST构建
-
表达式节点体系与访问者模式
-
上下文参数支持的数据类型访问
-
完整场景示例流程及功能分析
-
自定义函数扩展机制
一、递归下降算法与运算符优先级
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中的每个节点代表表达式的一个语法单元。节点体系的核心作用是:
-
结构化表示:将扁平的Token流转换为层次化的树结构
-
语义承载:每个节点类型对应一种语言构造(运算、变量、方法调用等)
-
操作分离:节点只负责“是什么”,不负责“怎么做”
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);
// 根据运算符执行具体运算
}
}
访问者模式的优势:
-
可扩展性:新增操作只需新增一个Visitor实现,无需修改节点类
-
关注点分离:节点结构(AST)与操作(求值)完全解耦
-
双重分派:通过
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 支持:
-
通过getter方法访问:
user.getName()→ 表达式user.name -
通过isXxx方法访问(布尔类型):
user.isActive()→ 表达式user.active -
直接字段访问(私有字段通过
setAccessible(true)强制访问) -
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 扩展要点
-
FunctionResolver 是函数式接口,支持Lambda表达式注册
-
函数调用优先级:上下文函数 > 静态方法(
ReflectUtils.invokeStaticMethod) -
参数处理:函数参数在调用前已被求值,传入的是具体的值而非AST节点
-
异常处理:函数内部异常会被捕获并包装为
ExpressionException
总结
本文从零开始设计了一套完整的 Java 表达式引擎,涵盖了:
-
递归下降算法:通过函数层级嵌套实现运算符优先级
-
执行流程:词法分析→Token流→语法分析→AST→求值
-
访问者模式:实现节点结构与操作分离,保证可扩展性
-
EvaluationContext:管理变量、函数、属性访问
-
自定义函数:通过
FunctionResolver轻松扩展
表达式引擎的核心价值在于让业务逻辑可配置化——无论是规则引擎、公式计算还是动态脚本控制,它都能提供灵活、高效、安全的解决方案。
技术信条回顾:所有高深的技术,最后都能用大白话讲清楚。希望这篇文章让你彻底搞懂了表达式引擎的设计之道。
👍 如果本文对你有帮助,欢迎点赞、收藏、转发!
💬 有任何问题或想法,欢迎在评论区留言交流!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)