\[\]> 本文是【GoF设计模式】系列第22篇,更多内容欢迎关注公众号:咖啡八杯
前言
为什么需要解释器模式?
假设营销系统要判断用户是否符合优惠活动资格,规则是"用户年龄 > 18 且 积分 > 100"。最直接的写法是 if-else:
java
public boolean check(User user) {
return user.getAge() > 18 && user.getScore() > 100;
}
看起来简单,但规则会变--加一个"或 VIP 用户可参与"分支、改个阈值、加一层嵌套 (A 或 B) 且 C,每改一次就得改这段代码、重新发版。当规则要支持动态配置、让运营自己配时,if-else 写到后面就成了无法维护的乱麻。
问题症结在于把"规则的语法"和"规则的执行"焊在了一起。如果能先把规则抽象成一套"文法",每条规则用一个类表示,运行时动态组合成树、递归执行,加规则就只是加一个类--这就是解释器模式。
概念
解释器模式(Interpreter Pattern)是一种行为型设计模式 ,核心思想是给定一个语言,定义它的文法的一种表示,并定义一个解释器,使用该表示来解释语言中的句子。
文法与AST
要理解这个模式,得先理解两个前置概念------文法 和 AST。
文法就是语法规则。要让计算机理解 3 + 4 * 2,得告诉它规则:一个表达式由若干项用加号连接,一项由若干因子用乘号连接,一个因子就是一个数字。用 BNF(一种描述文法的记号)写出来:
expr -> term ('+' term)*
term -> factor ('*' factor)*
factor -> NUMBER
BNF 的精髓在于分层 --每一层处理一种优先级,乘法在更深的 term 层,所以一定先算乘再算加,靠层级结构而非约定决定计算顺序。
把文法画成一棵树就是 AST(抽象语法树)。3 + 4 * 2 的 AST 长这样:
AddExpression
/ \
NumberExpr(3) MultiplyExpression
/ \
NumberExpr(4) NumberExpr(2)
运算顺序从叶子往根:先算 4 * 2 = 8,再算 3 + 8 = 11。这种从叶子到根的过程天然适合递归。解释器模式就是把"文法规则 + AST + 递归求值"写成代码:每条文法规则对应一个类,每个节点自己知道怎么算自己,组合起来就能解释整个表达式。
详细可看:文法、BNF、AST
解释器模式
解释器模式涉及四个角色:
- AbstractExpression(抽象表达式) :定义解释器的统一接口,声明
interpret方法 - TerminalExpression(终结符表达式):文法中不可再分的基本元素,如数字、变量,直接返回值或从上下文查值
- NonterminalExpression(非终结符表达式) :由子表达式组合而成的复合节点,如加法、AND,通过递归调用子表达式的
interpret完成求值 - Context(上下文):存储解释过程中的全局信息(如变量值),供终结符查询
classDiagram direction BT class AbstractExpression { <<interface>> +interpret(Context) Object } class TerminalExpression { -value: String +interpret(Context) Object } class NonterminalExpression { -left: AbstractExpression -right: AbstractExpression +interpret(Context) Object } class Context { -variables: Map +lookup(String) Object +assign(String, Object) } class Client { +main() } TerminalExpression ..|> AbstractExpression : 实现 NonterminalExpression ..|> AbstractExpression : 实现 NonterminalExpression o--> AbstractExpression : left/right 子表达式 Client --> AbstractExpression : 构建+调用 Client --> Context : 传入
图中各类之间的关系:AbstractExpression 是所有节点的统一接口,TerminalExpression 和 NonterminalExpression 都实现它。NonterminalExpression 用 left/right 持有两个子表达式引用--子表达式既可以是终结符也可以是另一个非终结符,它不关心,只调 interpret 让多态分派。Context 存储变量映射,TerminalExpression 通过它查值。Client 负责按文法构建 AST,把 Context 和根节点一起启动递归求值。
实现
解释器模式的核心是"一个规则一个类 + 递归求值"--每条文法规则对应一个 Expression 子类,非终结符持有子表达式引用,interpret 递归调用子节点。解析(Parser)属于客户端职责,可手动构建 AST,也可用 Parser 自动构建。
基础实现
用布尔表达式演示解释器模式。定义 BooleanExpression 接口声明 interpret(Context);ConstantExpression 作为终结符返回布尔常量,VariableExpression 作为终结符从上下文查变量值;AndExpression、OrExpression 作为非终结符持有左右子表达式,递归求值。客户端构建布尔表达式 AST(如 (x AND y) OR z)并启动递归。
java
import java.util.*;
// 抽象表达式:所有节点的统一接口
interface BooleanExpression {
boolean interpret(Context context);
}
// 终结符表达式:布尔常量
class ConstantExpression implements BooleanExpression {
private boolean value;
public ConstantExpression(boolean value) {
this.value = value;
}
public boolean interpret(Context context) {
return value; // 直接返回常量值
}
}
// 终结符表达式:布尔变量,从上下文查值
class VariableExpression implements BooleanExpression {
private String name;
public VariableExpression(String name) {
this.name = name;
}
public boolean interpret(Context context) {
return context.lookup(name); // 从上下文查变量值
}
}
// 非终结符表达式:AND
class AndExpression implements BooleanExpression {
private BooleanExpression left;
private BooleanExpression right;
public AndExpression(BooleanExpression left, BooleanExpression right) {
this.left = left;
this.right = right;
}
public boolean interpret(Context context) {
return left.interpret(context) && right.interpret(context);
}
}
// 非终结符表达式:OR
class OrExpression implements BooleanExpression {
private BooleanExpression left;
private BooleanExpression right;
public OrExpression(BooleanExpression left, BooleanExpression right) {
this.left = left;
this.right = right;
}
public boolean interpret(Context context) {
return left.interpret(context) || right.interpret(context);
}
}
// 上下文:存储布尔变量映射,供终结符查询
class Context {
private Map<String, Boolean> variables = new HashMap<>();
public void assign(String key, boolean value) {
variables.put(key, value);
}
public boolean lookup(String key) {
return variables.get(key);
}
}
// 客户端:构建 AST 并执行
class Client {
public static void main(String[] args) {
Context context = new Context();
context.assign("x", true);
context.assign("y", false);
context.assign("z", true);
// 构建 (x AND y) OR z 的 AST
BooleanExpression ast = new OrExpression(
new AndExpression(new VariableExpression("x"), new VariableExpression("y")),
new VariableExpression("z")
);
System.out.println(ast.interpret(context)); // (true AND false) OR true = true
}
}
角色对照:
- AbstractExpression(抽象表达式) :
BooleanExpression,声明interpret(Context) - TerminalExpression(终结符表达式) :
ConstantExpression(布尔常量)、VariableExpression(布尔变量) - NonterminalExpression(非终结符表达式) :
AndExpression、OrExpression,逻辑组合 - Context(上下文) :
Context,存布尔变量映射
关键点 :每个非终结符只关心自己这一层的规则,不关心子表达式是什么类型--AndExpression 调 left.interpret(context) 时,left 是常量、变量还是另一个 AND 它无所谓,多态自动分派到正确实现。求值时从根节点 ast.interpret(context) 开始,递归向下到叶子再逐层返回,整个 (x AND y) OR z 就被解释成了 true。这就是 GoF 原版结构:终结符返回值,非终结符组合子表达式,Context 存储变量映射。
引入一个生活比喻:规则求值像公司逐级审批。基层的终结符(ConstantExpression、VariableExpression)是办事员,各自查条件是否满足并上报结论;非终结符(AndExpression、OrExpression)是主管,收集下属结论做逻辑汇总后继续上报。以基础实现的 (x AND y) OR z 为例,x=true、y=false:两个办事员查到值上报,AndExpression 主管汇总得 false,再上报给 OrExpression 总监,和 z=true 汇总得 true。每个节点只关心"下属给我的结论"和"我怎么汇总",不关心下属具体查了什么--这正是递归求值的本质。
同样的思路换到业务场景:营销系统要把 (age > 18) AND (score >= 90) 这样的规则做成可配置的 AST。每条比较条件是终结符,AND/OR 是非终结符,事实数据放在上下文里。
java
import java.util.*;
// 抽象表达式:规则
interface RuleExpression {
boolean interpret(RuleContext context);
}
// 终结符:比较条件(如 age > 18)
class CompareExpression implements RuleExpression {
private String field;
private String operator;
private int value;
public CompareExpression(String field, String operator, int value) {
this.field = field;
this.operator = operator;
this.value = value;
}
public boolean interpret(RuleContext context) {
int factValue = context.getFact(field); // 从上下文取事实
switch (operator) {
case ">": return factValue > value;
case "<": return factValue < value;
case ">=": return factValue >= value;
case "<=": return factValue <= value;
case "=": return factValue == value;
default: throw new IllegalArgumentException("未知运算符: " + operator);
}
}
}
// 非终结符:AND 组合
class AndExpression implements RuleExpression {
private RuleExpression left;
private RuleExpression right;
public AndExpression(RuleExpression left, RuleExpression right) {
this.left = left;
this.right = right;
}
public boolean interpret(RuleContext context) {
return left.interpret(context) && right.interpret(context);
}
}
// 非终结符:OR 组合
class OrExpression implements RuleExpression {
private RuleExpression left;
private RuleExpression right;
public OrExpression(RuleExpression left, RuleExpression right) {
this.left = left;
this.right = right;
}
public boolean interpret(RuleContext context) {
return left.interpret(context) || right.interpret(context);
}
}
// 上下文:存储事实数据
class RuleContext {
private Map<String, Integer> facts = new HashMap<>();
public void setFact(String key, int value) {
facts.put(key, value);
}
public int getFact(String key) {
return facts.get(key);
}
}
// 客户端:构建优惠资格规则 AST 并执行
class PromotionCheck {
public static void main(String[] args) {
// 规则:(age > 18) AND (score >= 90)
RuleExpression rule = new AndExpression(
new CompareExpression("age", ">", 18),
new CompareExpression("score", ">=", 90)
);
RuleContext ctx = new RuleContext();
ctx.setFact("age", 25);
ctx.setFact("score", 95);
System.out.println(rule.interpret(ctx)); // true
}
}
角色对照:
- AbstractExpression(抽象表达式) :
RuleExpression,声明interpret(RuleContext) - TerminalExpression(终结符表达式) :
CompareExpression,一个比较条件 - NonterminalExpression(非终结符表达式) :
AndExpression、OrExpression,组合子规则 - Context(上下文) :
RuleContext,存事实数据
关键点 :规则被表示成一棵 AST,AndExpression/OrExpression 只负责把子规则的布尔结果做逻辑运算,不关心子规则比的是什么字段。新增"NOT"规则只需加一个 NotExpression 类,老规则代码一行不改--这就是开闭原则。规则可以配置化存储(如 JSON 描述的规则树),运行时由 Parser 还原成 AST 执行,运营改规则不用改代码。
总结
本质:把文法规则表示成一个个表达式类,用 AST 组织起来,通过递归求值解释整个句子。
什么时候用:
- 问题能被明确抽象成"文法"(表达式、规则、DSL、协议格式)
- 文法规则会频繁扩展,希望加规则只加类、不改老代码
- 规则需要运行时动态配置和组合,而非编译时写死
什么时候不用:
- 文法复杂、规则极多--类会爆炸式增长,难以管理
- 对性能要求高--递归调用和大量对象创建有开销
- 规则简单且固定,直接
if-else或现成引擎更清爽 - 追求效率而非扩展性,解释器模式的递归求值不如直接编译
简单记忆:文法拆成树,节点各自算;叶子先出值,非叶递归往上返;一规则一个类,组合起来能解释。
相似模式区分
总览
| 模式 | 核心意图 | 典型场景 |
|---|---|---|
| 解释器 | 定义文法并表示为 AST,递归解释执行 | 表达式求值、规则引擎、DSL |
| 组合 | 将对象组合成树形结构,统一处理叶子和容器 | 文件系统、菜单树、组织架构 |
| 访问者 | 不修改元素类的前提下定义新操作 | AST 类型检查、代码生成 |
| 策略 | 封装可互换的算法,运行时选择其一执行 | 支付方式、排序算法切换 |
简单记忆:解释器造树求值,组合管结构,访问者管操作,策略管替换。
解释器 vs 组合模式
两者都用树形结构,但意图不同:
| 维度 | 解释器模式 | 组合模式 |
|---|---|---|
| 核心意图 | 解释语言/文法,递归求值 | 表示"部分-整体"的层次结构,统一操作 |
| 结构差异 | 叶子和组合节点有不同的解释逻辑 | 叶子和组合节点实现相同接口,对外透明 |
| 关注点 | 文法规则的解析和执行 | 对象树的遍历和操作统一化 |
| 典型场景 | 表达式求值、规则引擎 | 文件系统、UI 组件树、组织架构 |
逐步区分法:
- 如果树形结构是为了计算或解释某种语义(表达式求值、规则判断)-> 选解释器
- 如果树形结构是为了统一处理单个对象和组合对象(文件/文件夹、按钮/面板)-> 选组合
简单记忆口诀:"组合管结构,解释管语义"。
推荐:解释器模式的底层结构确实用了类似组合模式的树,但解释器额外多了"文法规则"和"递归求值"的语义层。只组合不解释就用组合模式,既要建树又要按文法求值才用解释器。
解释器 vs 访问者模式
两者都涉及树形结构的遍历和操作:
| 维度 | 解释器模式 | 访问者模式 |
|---|---|---|
| 核心意图 | 定义语言文法并解释执行 | 在不修改元素类的前提下添加新操作 |
| 结构差异 | 每个节点自己知道如何 interpret | 操作逻辑集中在 Visitor 中,节点只负责 accept |
| 关注点 | "怎么算"--语法规则的执行 | "算什么"--对固定结构执行多种不同操作 |
| 典型场景 | 表达式求值、DSL 解析 | 编译器的类型检查、代码生成、AST 变换 |
逐步区分法:
- 如果需要定义一种语言的语法规则并执行它 -> 选解释器
- 如果 AST 结构已固定,想在不改节点类的情况下增加新的遍历操作 -> 选访问者
简单记忆口诀:"解释器造树又求值,访问者只管遍历做操作"。
推荐:解释器同时负责构建 AST 和执行语义;访问者假设树已经存在,只关心如何遍历和处理。编译器里两者常配合使用--解释器负责建树求值,访问者负责后续的类型检查和优化变换。
解释器 vs 策略模式
两者都把"变化的部分"封装成类,实际开发中"一条规则一个策略类"容易被误当成策略模式,但结构完全不同:
| 维度 | 解释器模式 | 策略模式 |
|---|---|---|
| 核心意图 | 定义文法,规则可组合成树,递归求值 | 封装可互换的算法,运行时选一个执行 |
| 结构差异 | 非终结符持有子表达式,形成 AST 树 | 各策略平级独立,互不持有,由 Context 持有一个 |
| 关注点 | "规则怎么组合"--文法的层级与递归 | "用哪个算法"--算法的替换与选择 |
| 典型场景 | 表达式求值、规则引擎(规则可嵌套) | 支付方式选择、排序算法切换 |
逐步区分法:
- 如果规则之间能组合嵌套 (如
A AND (B OR C)),需要树形结构递归求值 -> 选解释器 - 如果规则之间平级独立、互不组合,只是运行时挑一个执行 -> 选策略
简单记忆口诀:"解释器造树组合,策略模式平行替换"。
推荐:当"一条规则一个类"但规则之间没有组合关系时,用策略模式就够了,别上解释器的树形结构。只有规则需要动态嵌套成 AST 才值得用解释器--解释器的复杂度换来的是组合能力。
练习题目
数学表达式计算器
题目描述 :设计一个计算器,用于解释用户输入的简单数学表达式。每个表达式由整数、加法操作符 +、乘法操作符 * 组成,元素之间用空格分隔。请使用解释器模式实现这个系统。
解释器模式要求:
- 定义抽象表达式接口
Expression,包含int interpret()方法 - 终结符表达式 对应数字,非终结符表达式对应运算(加法、乘法)
- 支持多位数 (如
10、135) - 正确处理运算符优先级:乘法优先于加法
- 不允许使用
javax.script.ScriptEngine等内置求值器 - 运算逻辑必须封装在 Expression 子类的
interpret()中,不允许在解析器中用if-else做运算
输入描述 :每行包含一个数学表达式,表达式中包含整数、加法操作符(+)和乘法操作符(*)。表达式中的元素之间用空格分隔。
输出描述:对于每个输入的数学表达式,每行输出一个整数,表示对应表达式的计算结果。
输入示例:
2 + 3
5 * 2
3 + 4 * 2
10 + 20 * 3 + 5
10 * 2 + 3 * 5
输出示例:
5
10
11
75
35
提示:本题对应的文法(BNF 风格)如下:
expr -> term ('+' term)*
term -> factor ('*' factor)*
factor -> NUMBER
其中 NUMBER 表示一个整数。expr 调用 term、term 内部消化了所有乘法,所以乘法优先于加法--优先级靠文法层级保证,而非硬编码顺序。
解题思路 :题目是解释器模式的典型应用--数学表达式是最常见的文法,每条 BNF 规则对应一个 Expression 子类,运算逻辑封装在 interpret() 里。难点不在解释器本身(它只是递归求值),而在 Parser 按文法构建 AST:三个解析方法严格对应三层文法,优先级通过层级嵌套自然体现。去掉解释器模式,把运算塞进 Parser 的 if-else,规则一多就成了面条代码,也无法体现"一规则一类"的扩展性。
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String line = sc.nextLine();
String[] tokens = line.split(" ");
Expression ast = new Parser(tokens).parseExpr();
System.out.println(ast.interpret());
}
}
}
// 解析器:按文法规则将 token 数组构建为 AST
class Parser {
private String[] tokens;
private int index;
public Parser(String[] tokens) {
this.tokens = tokens;
this.index = 0;
}
// expr -> term ('+' term)*
public Expression parseExpr() {
Expression left = parseTerm();
while (index < tokens.length && "+".equals(tokens[index])) {
++index;
Expression right = parseTerm();
left = new AddExpr(left, right); // 加法组装成节点
}
return left;
}
// term -> factor ('*' factor)*
private Expression parseTerm() {
Expression left = parseFactor();
while (index < tokens.length && "*".equals(tokens[index])) {
++index;
Expression right = parseFactor();
left = new MulExpr(left, right); // 乘法组装成节点
}
return left;
}
// factor -> NUMBER
private Expression parseFactor() {
// ⚠️ 易错点:取 token 前必须检查越界,"3 + " 这类残缺表达式会数组越界
if (index >= tokens.length) {
throw new RuntimeException("表达式不完整,缺少操作数");
}
String token = tokens[index];
if ("*+".indexOf(token) == -1) { // 非运算符,按数字处理
++index;
return new NumberExpr(Integer.parseInt(token));
}
throw new RuntimeException("非法语法: 意外的运算符 " + token);
}
}
// 抽象表达式
interface Expression {
int interpret();
}
// 终结符:数字
class NumberExpr implements Expression {
private int value;
public NumberExpr(int value) {
this.value = value;
}
public int interpret() {
return this.value;
}
}
// 非终结符:加法
class AddExpr implements Expression {
private Expression left;
private Expression right;
public AddExpr(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public int interpret() {
return left.interpret() + right.interpret();
}
}
// 非终结符:乘法
class MulExpr implements Expression {
private Expression left;
private Expression right;
public MulExpr(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public int interpret() {
return left.interpret() * right.interpret();
}
}
代码分析 :Parser 的三个方法严格对应三层文法--parseExpr 处理加法、parseTerm 处理乘法、parseFactor 处理数字。乘法在更深的 parseTerm 中被先组装,所以求值时一定先算乘再算加,优先级自然正确。parseExpr 的 while 循环不断把 left 替换成新的 AddExpr,形成右倾的链式树;执行时 interpret() 从根递归到叶子再逐层返回,完成求值。运算逻辑全部在 Expression 子类的 interpret() 里,Parser 只负责建树不做运算,这正是解释器模式"解释与解析分离"的体现。⚠️ parseFactor 开头的越界检查是易错点:残缺输入会直接数组越界,必须先判再取。
Parser 扩展与注意事项
练习题的 Parser 已经展示了递归下降法的两个核心技巧:文法层级决定方法层级(parseExpr/parseTerm/parseFactor 三层),while 循环替换 left 处理左结合(如 left = new AddExpr(left, right))。基于这套方法,扩展更复杂的语法只需加文法规则、加对应方法。
扩展括号 () :在文法最低层 factor 增加一条规则,遇到 ( 就递归调用最上层的 parseExpr,遇到 ) 收尾。括号内的内容被完整解析成一棵子 AST,挂回当前位置。
factor -> NUMBER | '(' expr ')'
扩展负数 :负号是一元运算符,同样在 factor 层处理。遇到 - 先消费它,再递归解析后面的因子,求值时由一个 NegExpr 节点取反。
factor -> NUMBER | '-' factor | '(' expr ')'
多级优先级 :每多一级优先级就多一层文法。要支持加减乘除,expr 管 +/-、term 管 *//,乘除在更深层所以一定先算。优先级靠文法层级保证,不是靠 if-else 的书写顺序。
expr -> term (('+' | '-') term)*
term -> factor (('*' | '/') factor)*
factor -> NUMBER | '(' expr ')'
⚠️ 以下是三个高频踩坑点:
- index 越界 :练习题的
parseFactor取 token 前已检查index < tokens.length,删掉后碰到3 +这种残缺输入会直接数组越界。 - 运算符优先级丢失 :如果把加法和乘法放在同一层解析,
3 + 4 * 2会从左到右算成(3 + 4) * 2 = 14。优先级必须靠"乘法在更深的文法层"来保证。 - 递归过深 :超长表达式或深度嵌套的括号,会让
interpret()的递归调用栈溢出(StackOverflowError)。生产环境要么限制输入长度,要么把递归求值改成迭代。
💡 反模式 :把运算逻辑塞进 Parser 的 if-else 里(如解析到 + 就直接算 left + right),等于把"解析"和"解释"焊死,加一个运算符就得改 Parser。正确做法是 Parser 只建 AST,运算全交给 Expression 子类的 interpret()。
扩展:实际项目中的解释器模式
Spring SpEL 表达式引擎
Spring 的 SpEL(Spring Expression Language)是解释器模式的典型应用。它把表达式字符串解析成 AST,再递归求值,常用于 @Value 注入、@Cacheable 的 key、动态权限校验。SpEL 内部结构正是解释器:SpelExpression 是入口,IntLiteral 是终结符,OpPlus、OpMultiply 是非终结符,各自的 getValue() 就相当于 interpret()。
java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("2 + 3 * 5");
StandardEvaluationContext context = new StandardEvaluationContext();
int result = (int) exp.getValue(context); // 17
Apache Commons JEXL
Apache Commons JEXL 是一个轻量级表达式引擎,支持在 Java 中执行动态表达式,内部同样是解释器模式。相比手写解释器,JEXL 还支持方法调用、集合操作等复杂语法。常用于运营配置动态规则、报表公式计算--把表达式存在配置里,运行时解析执行,改公式不用改代码。
java
JexlEngine jexl = new JexlBuilder().create();
Expression expr = jexl.createExpression("x + y * 2");
JexlContext context = new MapContext();
context.set("x", 10);
context.set("y", 3);
Object result = expr.evaluate(context); // 16
正则表达式引擎
正则表达式引擎(如 Java 的 Pattern)内部也是解释器模式的变体。正则 /a(b|c)d+/ 会被解析成一棵 AST:Sequence 节点下挂着 Literal、Alternation、OneOrMore 等子节点。每个节点负责匹配自己那部分,组合起来就是整个正则的匹配逻辑。这里的 interpret() 不是返回数值,而是返回"是否匹配成功"及消费了多少字符。
java
Pattern p = Pattern.compile("a(b|c)d+");
Matcher m = p.matcher("abd");
System.out.println(m.matches()); // true
// 内部 AST: Sequence -> [Literal(a), Alternation(b|c), OneOrMore(d)]
SQL 解析器
轻量级 SQL 解析器(如 JSqlParser)用解释器模式把 SQL 文本解析成 AST,再遍历 AST 执行或转换。where 子句里的 >、AND、OR 都是非终结符,age、18 是终结符,解析器按 BNF 文法规则构建出这棵 AST。常见于分库分表中间件(解析 SQL 改写路由)、SQL 审计、ORM 方言转换。
java
Statement stmt = CCJSqlParserUtil.parse("SELECT * FROM users WHERE age > 18");
PlainSelect select = (PlainSelect) ((Select) stmt).getSelectBody();
Expression where = select.getWhere(); // 这就是 AST 中的条件表达式节点
MyBatis 动态 SQL 解析(OGNL)
写 MyBatis 时 <if test="name != null and age > 18"> 里的 test 属性就是 OGNL 表达式。OGNL(Object-Graph Navigation Language)把这段字符串解析成 AST,再对传入的参数对象递归求值,决定标签内的 SQL 拼不拼接--这正是解释器模式:and、!=、> 是非终结符,属性访问 name、字面量 null/18 是终结符。日常用 MyBatis 动态 SQL 其实天天在用解释器模式,只是 OGNL 把解析和求值封装好了。需要自定义条件拼接、动态过滤时就会碰到它。
xml
<select id="findUser" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and age > 18">
AND name = #{name} AND age > #{age}
</if>
</where>
</select>
模板引擎
模板引擎(如 FreeMarker、Thymeleaf)解析模板中的变量替换、条件判断语法,本质也是解释器模式。模板里的 ${user.name}、<#if age > 18> 被解析成 AST 节点,渲染时递归求值--变量节点从数据模型取值,条件节点根据子表达式结果决定渲染哪个分支。把"模板语法"抽象成文法后,新增指令只是加一个节点类。
java
// FreeMarker 内部:模板解析为 AST,渲染时递归求值
Map<String, Object> data = new HashMap<>();
data.put("user", new User("张三"));
data.put("age", 25);
template.process(data, writer); // 递归遍历 AST 节点求值
性能优化:AST 缓存与预编译
解释器模式每次求值都要递归遍历整棵 AST,表达式越长、调用越频繁,开销越大。实际项目里常用两个手段加速。AST 缓存:同一段表达式解析出的 AST 是纯结构、无状态,可以缓存复用,第二次执行直接取缓存树、只重新求值,规则引擎和模板渲染等"同一公式反复跑"的场景效果显著。
java
// AST 缓存:同一表达式只解析一次,后续直接复用
private static final Map<String, Expression> astCache = new HashMap<>();
public Expression parse(String expr) {
return astCache.computeIfAbsent(expr, this::doParse);
}
预编译 :把 AST 进一步编译成更高效的形式,如 Spring SpEL 能把高频表达式编译成字节码(SpelCompiler),跳过递归解释直接调用,当执行次数远大于解析次数时开销摊薄到接近零。⚠️ AST 本身可安全共享,但求值依赖的 Context 每次都是新的,不能把求值结果一并缓存,否则会读到脏数据。
技术交流 & 更多原创内容,关注公众号:咖啡八杯