解释器模式:语法解析、表达式求值、实战:SQL解析器

解释器模式,说实话,在咱们日常业务开发中用得不算多。但一旦用上,往往就是解决某类复杂问题的利器。我最早接触它,是在做一个规则引擎项目时——需要解析用户自定义的表达式,比如 age > 18 AND score >= 60。当时第一反应是写一堆 if-else,结果越写越崩溃。后来重构时引入了解释器模式,整个代码结构瞬间清晰了。

说白了,解释器模式就是给语言(或表达式)定义一个文法,然后为每种文法规则都配一个解释器。每个解释器负责解析自己那部分,最终组合起来就能处理整个句子。你想想看,这不就是编译器干的事吗?只不过我们把它简化到了应用层面。

模式核心:抽象语法树

解释器模式的核心,是构建一棵抽象语法树(AST)。每个节点都是一个表达式,叶子节点是终结符表达式(比如数字、变量),非叶子节点是非终结符表达式(比如加减乘除、AND/OR)。

我习惯把解释器模式拆成三个角色:

  • 抽象表达式(AbstractExpression):声明一个 interpret 方法,所有节点都要实现它。
  • 终结符表达式(TerminalExpression):叶子节点,直接返回自己的值。
  • 非终结符表达式(NonterminalExpression):组合节点,递归调用子节点的 interpret 方法。

举个例子,解析一个简单的加法表达式 1 + 2 + 3。AST 长这样:

加法表达式(+) 加法表达式(+) 数字(3) 数字(1) 数字(2)

解释器会从根节点开始,递归往下走。每个非终结符节点先让子节点算出结果,再自己组合。最终根节点返回 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