你写的规则引擎 if-else 堆成山——解释器模式三行代码就能让表达式自己解析自己
有个真实的场景:产品经理说我们要做一个「动态规则」功能,运营可以在后台配规则,比如「注册超过 30 天 AND 消费金额 > 500 OR VIP 等级 >= 3」的用户自动发优惠券。
你心想:这不就是个 if-else 吗。于是:
java
if (user.getRegisterDays() > 30) {
if (user.getTotalAmount() > 500) {
sendCoupon(user);
} else if (user.getVipLevel() >= 3) {
sendCoupon(user);
}
} else if (user.getVipLevel() >= 3) {
sendCoupon(user);
}
一周后产品说:「再加个条件:最近 7 天有登录。」你的 if-else 开始往右缩进。
两周后产品说:「再加个括号:"(A AND B) OR (C AND NOT D)"。」你开始写 SQL 拼接。
一个月后产品说:「能不能支持自定义函数?比如 distance(user.location, store.location) < 5km。」你看着那段已经三百行的 if-else,开始怀疑人生。
解释器模式没有你想的那么玄
解释器模式在 GoF 书里占的篇幅不长,但代码是最难读的。不是因为概念难,是因为所有教程一上来就教你怎么写 BNF 范式、怎么画语法树、怎么实现终结符和非终结符。看完你就觉得这东西属于大学编译原理课,跟我写业务有什么关系。
我用一个你每天都接触的东西来解释:SQL。
你写 SELECT * FROM users WHERE age > 18 AND city = 'Beijing',MySQL 怎么理解这行字符串?它需要一个「解释器」来把这段文本翻译成它能执行的指令。这个解释器里面用了词法分析(把字符串拆成 token)、语法分析(把 token 组织成树)、语义分析(检查表名和字段名是否存在),最后生成执行计划。
解释器模式的本质:为一种语言定义文法,然后写一个解释器来执行该语言表示的句子。
这里的「语言」不是编程语言,而是任何有规则的字符串表达方式——SQL、正则表达式、数学公式、JSONPath、SpEL,甚至是你产品经理定义的「规则表达」。
一个能跑的解释器,50 行代码就够了
先放下书上那些抽象类图。我们直接实现一个最简单的算术表达式解释器,支持加减乘除和括号:
```java // Token 定义 record Token(TokenType type, String value) {} enum TokenType { NUMBER, PLUS, MINUS, MULTIPLY, DIVIDE, LPAREN, RPAREN, EOF }
// 词法分析器:把字符串拆成 Token 流 class Lexer { private final String input; private int pos = 0;
Lexer(String input) { this.input = input; }
Token nextToken() {
while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) pos++;
if (pos >= input.length()) return new Token(TokenType.EOF, "");
char c = input.charAt(pos);
if (Character.isDigit(c)) { // 数字
int start = pos;
while (pos < input.length() && Character.isDigit(input.charAt(pos))) pos++;
return new Token(TokenType.NUMBER, input.substring(start, pos));
}
// 运算符和括号
pos++;
return switch (c) {
case '+' -> new Token(TokenType.PLUS, "+");
case '-' -> new Token(TokenType.MINUS, "-");
case '*' -> new Token(TokenType.MULTIPLY, "*");
case '/' -> new Token(TokenType.DIVIDE, "/");
case '(' -> new Token(TokenType.LPAREN, "(");
case ')' -> new Token(TokenType.RPAREN, ")");
default -> throw new IllegalArgumentException("非法字符: " + c);
};
}
} ```
词法分析器做了把字符串变成 Token 这一件事。你给 "3 + 5 * 2",它返回 [NUMBER(3), PLUS, NUMBER(5), MULTIPLY, NUMBER(2), EOF]。
接下来是语法分析。我们用递归下降,每个文法规则对应一个方法:
```java class Parser { private final Lexer lexer; private Token currentToken;
Parser(Lexer lexer) {
this.lexer = lexer;
this.currentToken = lexer.nextToken();
}
// 表达式 → 项 (('+' | '-') 项)*
int parseExpression() {
int result = parseTerm();
while (currentToken.type() == TokenType.PLUS || currentToken.type() == TokenType.MINUS) {
Token op = currentToken;
eat(op.type());
int right = parseTerm();
result = op.type() == TokenType.PLUS ? result + right : result - right;
}
return result;
}
// 项 → 因子 (('*' | '/') 因子)*
int parseTerm() {
int result = parseFactor();
while (currentToken.type() == TokenType.MULTIPLY || currentToken.type() == TokenType.DIVIDE) {
Token op = currentToken;
eat(op.type());
int right = parseFactor();
result = op.type() == TokenType.MULTIPLY ? result * right : result / right;
}
return result;
}
// 因子 → NUMBER | '(' 表达式 ')'
int parseFactor() {
if (currentToken.type() == TokenType.NUMBER) {
int value = Integer.parseInt(currentToken.value());
eat(TokenType.NUMBER);
return value;
}
if (currentToken.type() == TokenType.LPAREN) {
eat(TokenType.LPAREN);
int result = parseExpression();
eat(TokenType.RPAREN);
return result;
}
throw new IllegalArgumentException("意外的 token: " + currentToken);
}
void eat(TokenType type) {
if (currentToken.type() == type) currentToken = lexer.nextToken();
else throw new IllegalArgumentException("期望 " + type + ",实际 " + currentToken.type());
}
} ```
验证:
java
int result = new Parser(new Lexer("(3 + 5) * 2 - 10 / 2")).parseExpression();
// result = 11 即 (8) * 2 - 5 = 16 - 5 = 11
50 行代码实现了一个支持四则运算和括号的解释器。解释器模式不是什么神秘的东西,就是把「解析规则」从「执行逻辑」里拆出来。
SpEL:Spring 对解释器模式最务实的应用
Spring 表达式语言(SpEL)就是解释器模式在业务系统里最典型的落地。你写过这个:
```java @Value("#{systemProperties['user.dir']}") private String userDir;
@PreAuthorize("hasRole('ADMIN') and #user.id == authentication.principal.id") public void deleteUser(User user) { ... }
// 动态查询条件 ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("name == '张三' and age > 18"); boolean result = exp.getValue(context, Boolean.class); ```
SpEL 把你写的那段文本 "name == '张三' and age > 18" 解析成抽象语法树,然后在运行时执行。你不需要自己写词法分析和语法分析——Spring 帮你做了。
回到开头产品经理那个需求,真正的解法不是写 if-else,而是:
java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(ruleStringFromDatabase);
boolean shouldSend = exp.getValue(userContext, Boolean.class);
if (shouldSend) {
sendCoupon(user);
}
规则存在数据库里,运营在后台配置,你一行代码都不用改。这就是解释器模式让你不用改代码就能扩展「语言」的威力。
但有个重要的提醒:不要直接让用户输入的字符串变成 SpEL 表达式然后执行。SpEL 能调用方法、访问静态字段、创建对象。T(java.lang.Runtime).getRuntime().exec('rm -rf /') 这种恶意表达式如果被执行,整个服务器就没了。
安全的做法:
1. 限制 SpEL 的能力(用 SimpleEvaluationContext 替代默认的 StandardEvaluationContext)
2. 只暴露你允许的变量和方法
3. 如果规则语法是给外部用户用的,自己写个更受限的解释器
解释器模式的边界
解释器模式最大的弱点:文法越复杂,类就越多。一个完整的 SQL 解析器可能需要几百个类,正则表达式引擎的语法树深度可能让递归爆栈。所以 GoF 书上明确说了——只适合简单的文法。
复杂的文法怎么办?用现成的。ANTLR、JavaCC 这种编译器生成工具,你定义文法规则,它帮你生成解释器代码。但那是另一篇文章的事了。
重要的是你怎么选: - 简单的表达式(数学、布尔逻辑、简单规则)→ 手写递归下降解释器,50-200 行代码 - 中等复杂(类 SQL 查询、业务规则引擎)→ 用 SpEL 或 MVEL - 复杂文法(完整编程语言、SQL 全量解析)→ ANTLR / JavaCC
解释器模式最被低估的价值不是「让你能写自己的解释器」,而是「让你理解 Spring 的 @Value、MyBatis 的 OGNL、Security 的 SpEL 权限表达式到底在干什么」。你不需要自己写一个,但你必须认识它。
说起来,我在做一个叫「爪爪代码冒险记」的微信小程序,把 23 个设计模式用卡皮巴拉漫画的方式讲。解释器模式这一章就用了一个规则引擎从 if-else 变成表达式驱动的真实案例。你要是觉得这类内容有用,微信搜一下「爪爪代码冒险记」就能找到。
2万+

被折叠的 条评论
为什么被折叠?



