解释器模式:语法解析、表达式求值、实战:SQL解析器
解释器模式,说实话,在咱们日常业务开发中用得不算多。但一旦用上,往往就是解决某类复杂问题的利器。我最早接触它,是在做一个规则引擎项目时——需要解析用户自定义的表达式,比如 age > 18 AND score >= 60。当时第一反应是写一堆 if-else,结果越写越崩溃。后来重构时引入了解释器模式,整个代码结构瞬间清晰了。
说白了,解释器模式就是给语言(或表达式)定义一个文法,然后为每种文法规则都配一个解释器。每个解释器负责解析自己那部分,最终组合起来就能处理整个句子。你想想看,这不就是编译器干的事吗?只不过我们把它简化到了应用层面。
模式核心:抽象语法树
解释器模式的核心,是构建一棵抽象语法树(AST)。每个节点都是一个表达式,叶子节点是终结符表达式(比如数字、变量),非叶子节点是非终结符表达式(比如加减乘除、AND/OR)。
我习惯把解释器模式拆成三个角色:
- 抽象表达式(AbstractExpression):声明一个 interpret 方法,所有节点都要实现它。
- 终结符表达式(TerminalExpression):叶子节点,直接返回自己的值。
- 非终结符表达式(NonterminalExpression):组合节点,递归调用子节点的 interpret 方法。
举个例子,解析一个简单的加法表达式 1 + 2 + 3。AST 长这样:
解释器会从根节点开始,递归往下走。每个非终结符节点先让子节点算出结果,再自己组合。最终根节点返回 1 + 2 + 3 = 6。
代码实现:一个简单的四则运算解释器
咱们直接上代码。先定义抽象表达式接口:
public interface Expression {
int interpret();
}
终结符表达式——数字:
public class NumberExpression implements Expression {
private int value;
public NumberExpression(int value) {
this.value = value;
}
@Override
public int interpret() {
return value;
}
}
非终结符表达式——加法:
public class AddExpression implements Expression {
private Expression left;
private Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}
减法、乘法、除法同理,我就不重复写了。关键看客户端怎么用:
// 构建 AST: (1 + 2) + 3
Expression one = new NumberExpression(1);
Expression two = new NumberExpression(2);
Expression three = new NumberExpression(3);
Expression add1 = new AddExpression(one, two);
Expression add2 = new AddExpression(add1, three);
int result = add2.interpret(); // 6
System.out.println(result);
嗯,这里要注意:实际项目中,我们不会手动构建 AST,而是通过词法分析器 + 语法分析器自动生成。但解释器模式的核心思想是一样的——每个节点都知道怎么解释自己。
实战:SQL 解析器
我在项目中遇到过最典型的解释器模式应用,就是 SQL 解析。比如要解析 SELECT name FROM users WHERE age > 18,我们可以把 SQL 拆成几个部分:
- SelectExpression:解析 SELECT 子句,提取字段列表
- FromExpression:解析 FROM 子句,提取表名
- WhereExpression:解析 WHERE 子句,提取过滤条件
- ConditionExpression:解析具体的条件,比如
age > 18
每个部分都是一个解释器,组合起来就能完整解析一条 SQL。我曾经用这个思路写过一个轻量级的 SQL 校验工具,专门用来检查用户输入的 SQL 是否安全——比如有没有注入风险,字段是否在白名单内。
核心思路:把 SQL 的每个语法单元抽象成一个表达式节点,递归解析。这样新增一种 SQL 语法(比如 JOIN),只需要新增一个表达式类,完全符合开闭原则。
来看一个简化版的 WHERE 条件解析器:
// 条件表达式接口
public interface Condition {
boolean evaluate(Map<String, Object> context);
}
// 大于条件
public class GreaterThanCondition implements Condition {
private String field;
private int value;
public GreaterThanCondition(String field, int value) {
this.field = field;
this.value = value;
}
@Override
public boolean evaluate(Map<String, Object> context) {
int fieldValue = (int) context.get(field);
return fieldValue > value;
}
}
// AND 组合条件
public class AndCondition implements Condition {
private Condition left;
private Condition right;
public AndCondition(Condition left, Condition right) {
this.left = left;
this.right = right;
}
@Override
public boolean evaluate(Map<String, Object> context) {
return left.evaluate(context) && right.evaluate(context);
}
}
使用的时候,先解析 WHERE 子句构建条件树,然后传入数据上下文,调用 evaluate 就能得到结果。是不是很像规则引擎?
我的经验:解释器模式虽然优雅,但性能不是它的强项。每个表达式都要递归调用,如果表达式树很深,会有一定的性能开销。我一般只在规则数量可控(几百条以内)的场景下使用。如果规则上万条,建议考虑编译成字节码或者用 ANTLR 生成解析器。
避坑指南
我曾经踩过一个坑:在解释器里混入了状态。比如在 NumberExpression 里缓存计算结果,想着能加速。结果多线程环境下出了诡异的问题。后来我强制要求所有解释器都是无状态的——每次 interpret 都重新计算,绝不缓存任何中间结果。如果真要缓存,用外部 Flyweight 模式来做。
另一个坑是递归深度。SQL 嵌套子查询时,AST 可能很深。Java 默认栈深度有限,我遇到过 StackOverflowError。解决方案有两个:一是用迭代代替递归(但实现复杂),二是调大 JVM 栈大小(临时方案)。我个人更倾向于限制嵌套层数,比如最多支持 5 层嵌套。
注意事项:解释器模式不适合解析复杂语言。比如完整的 SQL 语法、JSON 解析、模板引擎,这些场景请直接用 ANTLR、JavaCC 等成熟的解析器生成工具。解释器模式更适合领域特定语言(DSL),规则简单、语法有限的那种。
什么时候用解释器模式?
我总结了三类典型场景:
| 场景 | 例子 | 推荐程度 |
|---|---|---|
| 简单数学表达式求值 | 计算器、公式引擎 | ★★★★★ |
| 规则引擎/条件过滤 | 风控规则、权限校验 | ★★★★☆ |
| 轻量级 DSL 解析 | SQL 子集、配置文件 | ★★★☆☆ |
说白了,如果你发现自己在写一堆 if-else 来解析某种结构化文本,而且这种文本的语法是固定的、有限的,那就可以考虑用解释器模式重构了。
最后说一句:解释器模式不是银弹。它让代码变得优雅、可扩展,但代价是引入了更多的类和递归调用。我个人的习惯是,先用手写解析器快速验证,等确定语法稳定了,再重构为解释器模式。别一上来就搞 AST,容易过度设计。
公众号:蓝海资料掘金营,微信deep3321