解释器模式是 23 个 GoF 模式里"应用场景最窄但威力最大"的一个 —— 它专门解决"给定一种文法,构造一个能解释这种文法语句的解释器"。听起来很学术,但你工作里早就用过:Excel 的公式、Linux 的正则、SQL 查询、模板引擎、表达式语言、规则引擎、CSS 选择器,本质都是解释器。这篇文章把解释器从原理讲到 ANTLR / Drools / SpEL 这些工程级实现,讲清楚它和编译器、状态机、组合模式的关系。
问题:配置驱动复杂逻辑
看一个真实需求:风控系统要根据规则判断订单是否可疑。规则可能是:
- "金额 > 10000 且用户年龄 < 18"
- "IP 在黑名单 或 设备指纹被标记过欺诈"
- "今日同一用户下单次数 > 5 且总金额 > 50000"
规则会随业务变化频繁调整。如果硬编码到 Java 里,每改一条规则都要发版。理想方案是把规则写成文本,运行时解析执行:
rules:
- "amount > 10000 AND user.age < 18"
- "ip in blacklist OR device.fraud == true"
- "user.todayOrderCount > 5 AND user.todayTotalAmount > 50000"
问题:怎么解析和执行这些文本表达式?这就是解释器模式的舞台。
解释器模式的标准结构
解释器把一段"语言"用一组"表达式对象"表达,然后递归求值。
// 1. 抽象表达式
interface Expression {
Object interpret(Context ctx);
}
// 上下文:解释时需要的"环境数据"(变量、函数等)
class Context {
Map<String, Object> vars;
public Object get(String name) { return vars.get(name); }
}
// 2. 终结符表达式(叶子):字面量、变量
class Literal implements Expression {
private final Object value;
public Literal(Object v) { this.value = v; }
public Object interpret(Context c) { return value; }
}
class Variable implements Expression {
private final String name;
public Variable(String n) { this.name = n; }
public Object interpret(Context c) { return c.get(name); }
}
// 3. 非终结符表达式(组合):由其他表达式组成
class And implements Expression {
private final Expression left, right;
public And(Expression l, Expression r) { this.left = l; this.right = r; }
public Object interpret(Context c) {
return (boolean) left.interpret(c) && (boolean) right.interpret(c);
}
}
class Or implements Expression {
private final Expression left, right;
public Or(Expression l, Expression r) { this.left = l; this.right = r; }
public Object interpret(Context c) {
return (boolean) left.interpret(c) || (boolean) right.interpret(c);
}
}
class GreaterThan implements Expression {
private final Expression left, right;
public GreaterThan(Expression l, Expression r) { this.left = l; this.right = r; }
public Object interpret(Context c) {
return ((Number) left.interpret(c)).doubleValue() > ((Number) right.interpret(c)).doubleValue();
}
}
class Equal implements Expression {
private final Expression left, right;
public Equal(Expression l, Expression r) { this.left = l; this.right = r; }
public Object interpret(Context c) {
Object l = left.interpret(c);
Object r = right.interpret(c);
return Objects.equals(l, r);
}
}
// 客户端:手动构造表达式树
// "amount > 10000 AND user.age < 18"
Expression rule = new And(
new GreaterThan(new Variable("amount"), new Literal(10000)),
new GreaterThan(new Literal(18), new Variable("user.age")) // 等价"<"
);
Context ctx = new Context();
ctx.vars = Map.of("amount", 15000, "user.age", 16);
boolean isSuspicious = (boolean) rule.interpret(ctx); // true
这就是解释器模式的内核。"每个语法节点是一个类,有 interpret 方法" —— 树本身就是程序,执行树就是执行程序。
加上 Parser:从字符串到表达式树
手动构造表达式树不实用。需要一个解析器把文本变成表达式树:
// 简化版递归下降 parser
class RuleParser {
private List<Token> tokens;
private int pos = 0;
public Expression parse(String text) {
tokens = tokenize(text);
pos = 0;
return parseOr();
}
// OR 优先级最低
private Expression parseOr() {
Expression left = parseAnd();
while (match("OR")) {
left = new Or(left, parseAnd());
}
return left;
}
private Expression parseAnd() {
Expression left = parseComparison();
while (match("AND")) {
left = new And(left, parseComparison());
}
return left;
}
private Expression parseComparison() {
Expression left = parsePrimary();
if (match(">")) return new GreaterThan(left, parsePrimary());
if (match("==")) return new Equal(left, parsePrimary());
return left;
}
private Expression parsePrimary() {
Token t = next();
if (t.type == "NUMBER") return new Literal(Double.parseDouble(t.value));
if (t.type == "STRING") return new Literal(t.value);
if (t.type == "IDENT") return new Variable(t.value);
if (t.value.equals("(")) {
Expression e = parseOr();
expect(")");
return e;
}
throw new RuntimeException("unexpected " + t.value);
}
}
// 使用
Expression rule = new RuleParser().parse("amount > 10000 AND user.age < 18");
boolean result = (boolean) rule.interpret(ctx);
真实项目里 parser 通常用 ANTLR / JavaCC 这类工具生成 —— 你写文法定义,工具生成 lexer + parser 代码。手写 parser 只在简单 DSL 时才划算。
实战 1:Spring SpEL(Spring Expression Language)
// SpEL 内置在 Spring,可以在注解里写表达式
@PreAuthorize("hasRole('ADMIN') and #userId == authentication.principal.id")
public void deleteUser(Long userId) { ... }
// 直接用 ExpressionParser
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = exp.getValue(Integer.class); // 11
// 上下文变量
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("user", new User("mores", 30));
parser.parseExpression("#user.name + '-' + #user.age").getValue(ctx, String.class);
// "mores-30"
// 调用方法
parser.parseExpression("T(Math).random() * 100.0").getValue(Double.class);
SpEL 内部就是解释器模式的工业级实现 —— 它有完整的 lexer、parser、AST,以及对 Java 反射的桥接。你可以在 @Value、@Cacheable 的 key、Spring Security 表达式里写各种逻辑。
实战 2:Drools 规则引擎
Drools 是 Java 生态最知名的规则引擎,用一种叫 DRL(Drools Rule Language)的 DSL:
// 规则文件 fraud.drl
rule "Large amount + Young user"
when
$o: Order(amount > 10000)
$u: User(age < 18, id == $o.userId)
then
$o.markSuspicious("LARGE_AMOUNT_YOUNG_USER");
end
// Java 加载并触发
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
KieSession session = kc.newKieSession();
session.insert(order);
session.insert(user);
session.fireAllRules();
Drools 解析 DRL 文件构建规则网络(Rete 算法),运行时匹配事实(facts)触发对应动作。整个过程就是一个高度优化的解释器 + 模式匹配引擎。
实战 3:模板引擎
Velocity、Freemarker、Mustache、Handlebars、Jinja2 —— 所有模板引擎本质都是解释器:
// Mustache 模板
<p>Hello, {{name}}!</p>
{{#items}}
<li>{{name}} - ${{price}}</li>
{{/items}}
// 渲染过程:
// 1. 解析模板成 AST(文本节点 + 变量节点 + 循环节点)
// 2. 用数据(context)解释 AST,产出最终字符串
你写 {{name}} 看似简单,模板引擎内部把它当成一个 VariableExpression,interpret 时去 context 里查 name 替换。循环、条件、过滤器都是不同的表达式节点。
实战 4:简单计算器
// 从无到有写一个数学表达式计算器
// 支持:数字、+ - * /、括号、变量
interface Expr { double eval(Map<String, Double> vars); }
record Num(double v) implements Expr { public double eval(Map m) { return v; } }
record Var(String n) implements Expr { public double eval(Map m) { return (Double) m.get(n); } }
record Bin(char op, Expr l, Expr r) implements Expr {
public double eval(Map<String, Double> m) {
double lv = l.eval(m), rv = r.eval(m);
return switch (op) {
case '+' -> lv + rv; case '-' -> lv - rv;
case '*' -> lv * rv; case '/' -> lv / rv;
default -> throw new RuntimeException();
};
}
}
// "x * 2 + y" -> new Bin('+', new Bin('*', new Var("x"), new Num(2)), new Var("y"))
Expr e = new Bin('+',
new Bin('*', new Var("x"), new Num(2)),
new Var("y")
);
double result = e.eval(Map.of("x", 3.0, "y", 4.0)); // 10
解释器 vs 编译器
本质区别在于执行时机:
- 解释器:每次 interpret 都遍历 AST,边遍历边执行。简单灵活,但慢。
- 编译器:把 AST 转成机器码 / 字节码 / 优化后的中间形式,只做一次,之后直接执行优化产物。快但实现复杂。
JVM 是两者的混合:先解释执行,热点代码触发 JIT 编译。JavaScript 引擎(V8)同样。Drools 也类似 —— 规则越来越多时会有 KIE Compiler 做优化。
性能优化:从纯解释器到 JIT
纯解释器有几个性能问题:每次都要遍历整棵树;每个节点 interpret 都是虚函数调用;变量查找走 Map.get。常见优化:
1. 字节码生成
解释器把 AST 编译成字节码(自定义指令集),然后用一个紧凑的循环执行字节码。这比"遍历对象树调虚函数"快很多。Python / Ruby / PHP 都是这种解释方式。
2. 编译到 JVM 字节码
解释器把表达式直接编译成 JVM 字节码,JIT 帮你做后续优化。Drools 的 Mvel、JOOR 用这种方式,运行性能接近原生 Java。
3. 缓存解析结果
同一条规则文本第一次解析后缓存表达式树,后续直接 interpret。这是最简单的优化,百倍提升。
解释器的常见坑
坑 1:文法增长导致类爆炸。 30 种语法节点 = 30 个 Expression 子类。对小 DSL 还行,对大语言不可控。改用 Visitor 处理 AST 或上 ANTLR。
坑 2:安全性 / 沙箱。 用户提供的表达式可能恶意调用系统方法。SpEL 早期就有 RCE 漏洞 —— 用户写 T(Runtime).getRuntime().exec("rm -rf /") 就能执行命令。规则:用户表达式必须用受限上下文(SimpleEvaluationContext 代替 StandardEvaluationContext),屏蔽 type 引用、反射、IO。
坑 3:无限循环。 表达式支持循环或递归时,用户可能写死循环。生产环境必须加超时机制 —— 单条表达式执行超过 N ms 中断。
坑 4:性能崩溃。 每次请求都重新 parse 一次,QPS 高时 parser 成瓶颈。规则:parse 结果一定要缓存,只 interpret 是 hot path。
坑 5:错误信息差。 用户写错了表达式,解释器报 "ClassCastException at line 30" 这种内部错误。要在 parser 阶段就给出友好错误(哪行哪列,期望什么)。
什么时候用解释器(而不是直接 Java 代码)
- 规则由非开发人员维护(运营、业务方在管理后台改)。
- 规则变化频繁,频繁发版不现实。
- 规则需要持久化(数据库存)、版本化、AB 测试。
- 规则的表达式范围有限(不需要完整图灵完备语言)。
什么时候不用
- 规则逻辑复杂(嵌套循环、调用复杂业务) —— 不如写 Java 代码并热部署。
- 规则变化极少 —— 上 DSL 增加复杂度反而拖累。
- 性能极致敏感 —— 解释器再快也不如 Java 原生快。
不要重造轮子
除非真有特殊需求,优先用现成工具:
- Java:SpEL、Apache Commons JEXL、MVEL、Aviator、QLExpress。
- 规则引擎:Drools、EasyRules。
- Parser 生成器:ANTLR、JavaCC、Coco/R。
- JS:expr-eval、jexl-eval。
- Python:asteval、simpleeval。
自己写解释器的成本极高 —— parser、AST、错误处理、性能优化、安全沙箱,每一项都是几个月的工作。除非作为学习练习,生产环境绝不重造。
写在最后
解释器模式可能是 GoF 23 个模式里"使用门槛最高"的一个 —— 真正从零写解释器的工程师不多。但理解它,意味着你能看穿背后的工具:Excel 公式、SQL、正则、模板、SpEL、规则引擎、CSS 选择器、XPath、JSONPath、REST 的 over/under-fetch。">GraphQL 查询 —— 它们都是某种解释器。一旦你认出"这是一个 mini 语言 + 它的解释器",这些工具的工作方式就不再神秘。
给一个工程心得:当你的业务需要让"非开发人员动态配置复杂逻辑"时,先想清楚要不要做 DSL。能用配置项解决就别上 DSL;DSL 设计不当会变成"半生不熟的编程语言",维护成本高于硬编码。如果非要 DSL,优先用现成的表达式引擎(SpEL、JEXL、Aviator),99% 的需求它们能满足。只有在极特殊的场景下,才考虑用 ANTLR 自己设计文法 —— 那时你才真正在实现解释器模式,而不只是使用它。
—— 别看了 · 2026